第3章_布尔运算操作

第三章 布尔运算操作

3.1 引言

布尔运算是 Clipper1 最核心、最重要的功能。在计算机图形学和几何处理中,布尔运算指的是对两个或多个几何图形进行集合运算,包括交集(Intersection)、并集(Union)、差集(Difference)和异或(XOR)。这些运算在 CAD 设计、GIS 分析、游戏开发、制造业等众多领域都有广泛应用。

Clipper1 实现的布尔运算算法基于改进的 Vatti 算法,具有极高的鲁棒性和可靠性。它能够正确处理各种复杂情况,包括自相交多边形、重叠边、退化情况等。本章将深入讲解如何使用 Clipper1 执行各种布尔运算,并通过大量实例帮助您掌握其应用技巧。

3.2 布尔运算基础

3.2.1 四种基本运算

Clipper1 支持四种基本的布尔运算,通过 ClipType 枚举来指定:

交集(Intersection)- ctIntersection

  • 计算两个或多个多边形的重叠区域
  • 结果是所有输入多边形共同覆盖的部分
  • 应用场景:求两个区域的公共部分、冲突检测等

并集(Union)- ctUnion

  • 合并两个或多个多边形
  • 结果是所有输入多边形覆盖的总区域
  • 应用场景:区域合并、缓冲区合并、简化重叠区域等

差集(Difference)- ctDifference

  • 从主题多边形中减去裁剪多边形
  • 结果是主题多边形中不与裁剪多边形重叠的部分
  • 应用场景:挖孔操作、排除区域、布尔减法等

异或(XOR)- ctXor

  • 计算两个多边形不重叠的部分
  • 结果是属于其中一个多边形但不同时属于两者的区域
  • 应用场景:查找差异区域、对称差集等

3.2.2 Clipper 类的基本使用流程

使用 Clipper 类执行布尔运算的标准流程如下:

using ClipperLib;
using Path = System.Collections.Generic.List<ClipperLib.IntPoint>;
using Paths = System.Collections.Generic.List<System.Collections.Generic.List<ClipperLib.IntPoint>>;

// 1. 创建 Clipper 对象
Clipper clipper = new Clipper();

// 2. 添加主题多边形(Subject)
clipper.AddPath(subjectPath, PolyType.ptSubject, true);
// 或批量添加
clipper.AddPaths(subjectPaths, PolyType.ptSubject, true);

// 3. 添加裁剪多边形(Clip)
clipper.AddPath(clipPath, PolyType.ptClip, true);
// 或批量添加
clipper.AddPaths(clipPaths, PolyType.ptClip, true);

// 4. 执行运算
Paths solution = new Paths();
bool success = clipper.Execute(
    ClipType.ctIntersection,      // 运算类型
    solution,                      // 输出结果
    PolyFillType.pftNonZero,      // Subject 填充规则
    PolyFillType.pftNonZero       // Clip 填充规则
);

// 5. 处理结果
if (success)
{
    foreach (var path in solution)
    {
        // 处理每个结果多边形
    }
}

3.2.3 AddPath 和 AddPaths 方法

AddPath 方法签名

public bool AddPath(Path path, PolyType polyType, bool closed)

参数说明:

  • path:要添加的路径(点的列表)
  • polyType:路径类型(ptSubject 或 ptClip)
  • closed:是否为封闭路径(true 表示多边形,false 表示开放线段)

AddPaths 方法签名

public bool AddPaths(Paths paths, PolyType polyType, bool closed)

批量添加多个路径,参数含义相同。

返回值

  • 返回 true 表示成功添加
  • 返回 false 表示路径无效(如顶点数少于 2,或封闭路径顶点数少于 3)

3.2.4 Execute 方法

基本签名(输出为 Paths)

public bool Execute(
    ClipType clipType,
    Paths solution,
    PolyFillType subjFillType = PolyFillType.pftEvenOdd,
    PolyFillType clipFillType = PolyFillType.pftEvenOdd
)

层次结构签名(输出为 PolyTree)

public bool Execute(
    ClipType clipType,
    PolyTree polytree,
    PolyFillType subjFillType = PolyFillType.pftEvenOdd,
    PolyFillType clipFillType = PolyFillType.pftEvenOdd
)

参数说明:

  • clipType:布尔运算类型
  • solution / polytree:输出结果的容器
  • subjFillType:主题多边形的填充规则
  • clipFillType:裁剪多边形的填充规则

返回值

  • true:运算成功
  • false:运算失败(通常不会发生)

3.3 交集运算(Intersection)

3.3.1 基本概念

交集运算计算两个或多个多边形的共同覆盖区域。结果只包含同时被所有输入多边形覆盖的部分。

3.3.2 简单交集示例

using System;
using System.Collections.Generic;
using ClipperLib;

using Path = System.Collections.Generic.List<ClipperLib.IntPoint>;
using Paths = System.Collections.Generic.List<System.Collections.Generic.List<ClipperLib.IntPoint>>;

class IntersectionExample
{
    static void BasicIntersection()
    {
        const double SCALE = 1000.0;

        // 创建第一个正方形 (0,0) 到 (100,100)
        Path square1 = new Path
        {
            new IntPoint(0, 0),
            new IntPoint((long)(100 * SCALE), 0),
            new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
            new IntPoint(0, (long)(100 * SCALE))
        };

        // 创建第二个正方形 (50,50) 到 (150,150)
        Path square2 = new Path
        {
            new IntPoint((long)(50 * SCALE), (long)(50 * SCALE)),
            new IntPoint((long)(150 * SCALE), (long)(50 * SCALE)),
            new IntPoint((long)(150 * SCALE), (long)(150 * SCALE)),
            new IntPoint((long)(50 * SCALE), (long)(150 * SCALE))
        };

        // 执行交集运算
        Clipper clipper = new Clipper();
        clipper.AddPath(square1, PolyType.ptSubject, true);
        clipper.AddPath(square2, PolyType.ptClip, true);

        Paths solution = new Paths();
        bool success = clipper.Execute(
            ClipType.ctIntersection,
            solution,
            PolyFillType.pftNonZero,
            PolyFillType.pftNonZero
        );

        if (success && solution.Count > 0)
        {
            Console.WriteLine($"交集产生 {solution.Count} 个多边形");

            foreach (var path in solution)
            {
                double area = Math.Abs(Clipper.Area(path)) / (SCALE * SCALE);
                Console.WriteLine($"多边形面积: {area}");
                Console.WriteLine("顶点坐标:");
                foreach (var pt in path)
                {
                    Console.WriteLine($"  ({pt.X / SCALE}, {pt.Y / SCALE})");
                }
            }
        }
        else
        {
            Console.WriteLine("两个多边形没有交集");
        }
    }
}

预期输出:

交集产生 1 个多边形
多边形面积: 2500
顶点坐标:
  (50, 50)
  (100, 50)
  (100, 100)
  (50, 100)

3.3.3 多个多边形的交集

Clipper1 可以同时计算多个多边形的交集:

static void MultipleIntersection()
{
    const double SCALE = 1000.0;

    // 创建三个圆形(用多边形近似)
    Path circle1 = CreateCircle(50, 50, 40, 32, SCALE);
    Path circle2 = CreateCircle(80, 50, 40, 32, SCALE);
    Path circle3 = CreateCircle(65, 75, 40, 32, SCALE);

    // 将所有圆作为主题多边形添加
    Clipper clipper = new Clipper();
    clipper.AddPath(circle1, PolyType.ptSubject, true);
    clipper.AddPath(circle2, PolyType.ptSubject, true);
    clipper.AddPath(circle3, PolyType.ptSubject, true);

    // 执行交集运算
    Paths solution = new Paths();
    clipper.Execute(
        ClipType.ctIntersection,
        solution,
        PolyFillType.pftNonZero,
        PolyFillType.pftNonZero
    );

    Console.WriteLine($"三个圆的公共交集产生 {solution.Count} 个区域");
    if (solution.Count > 0)
    {
        double totalArea = 0;
        foreach (var path in solution)
        {
            double area = Math.Abs(Clipper.Area(path)) / (SCALE * SCALE);
            totalArea += area;
        }
        Console.WriteLine($"总交集面积: {totalArea:F2}");
    }
}

static Path CreateCircle(double cx, double cy, double radius, int segments, double scale)
{
    Path circle = new Path();
    for (int i = 0; i < segments; i++)
    {
        double angle = 2 * Math.PI * i / segments;
        double x = cx + radius * Math.Cos(angle);
        double y = cy + radius * Math.Sin(angle);
        circle.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
    }
    return circle;
}

3.3.4 交集的实际应用

应用1:碰撞检测

class CollisionDetector
{
    private const double SCALE = 1000.0;

    /// <summary>
    /// 检测两个游戏对象是否碰撞
    /// </summary>
    public static bool CheckCollision(Path shape1, Path shape2)
    {
        Clipper clipper = new Clipper();
        clipper.AddPath(shape1, PolyType.ptSubject, true);
        clipper.AddPath(shape2, PolyType.ptClip, true);

        Paths intersection = new Paths();
        clipper.Execute(ClipType.ctIntersection, intersection);

        // 如果有交集,说明碰撞了
        return intersection.Count > 0;
    }

    /// <summary>
    /// 计算碰撞区域的面积
    /// </summary>
    public static double GetCollisionArea(Path shape1, Path shape2)
    {
        Clipper clipper = new Clipper();
        clipper.AddPath(shape1, PolyType.ptSubject, true);
        clipper.AddPath(shape2, PolyType.ptClip, true);

        Paths intersection = new Paths();
        clipper.Execute(ClipType.ctIntersection, intersection);

        double totalArea = 0;
        foreach (var path in intersection)
        {
            totalArea += Math.Abs(Clipper.Area(path));
        }

        return totalArea / (SCALE * SCALE);
    }
}

应用2:地理信息系统(GIS)中的空间查询

class GISOperations
{
    private const double SCALE = 1000000.0;  // 更高精度用于地理坐标

    /// <summary>
    /// 查找在指定区域内的所有地块
    /// </summary>
    public static List<(int id, Path intersection, double area)> FindParcelsInRegion(
        Path queryRegion,
        Dictionary<int, Path> parcels)
    {
        var results = new List<(int id, Path intersection, double area)>();

        foreach (var parcel in parcels)
        {
            Clipper clipper = new Clipper();
            clipper.AddPath(queryRegion, PolyType.ptSubject, true);
            clipper.AddPath(parcel.Value, PolyType.ptClip, true);

            Paths intersection = new Paths();
            clipper.Execute(ClipType.ctIntersection, intersection);

            if (intersection.Count > 0)
            {
                double area = 0;
                foreach (var path in intersection)
                {
                    area += Math.Abs(Clipper.Area(path));
                }
                area /= (SCALE * SCALE);

                results.Add((parcel.Key, intersection[0], area));
            }
        }

        return results;
    }
}

3.4 并集运算(Union)

3.4.1 基本概念

并集运算合并所有输入多边形,生成一个覆盖所有输入区域的结果。重叠部分会自动合并,内部边界会被消除。

3.4.2 基本并集示例

static void BasicUnion()
{
    const double SCALE = 1000.0;

    // 创建两个相邻的正方形
    Path square1 = new Path
    {
        new IntPoint(0, 0),
        new IntPoint((long)(50 * SCALE), 0),
        new IntPoint((long)(50 * SCALE), (long)(50 * SCALE)),
        new IntPoint(0, (long)(50 * SCALE))
    };

    Path square2 = new Path
    {
        new IntPoint((long)(40 * SCALE), 0),
        new IntPoint((long)(90 * SCALE), 0),
        new IntPoint((long)(90 * SCALE), (long)(50 * SCALE)),
        new IntPoint((long)(40 * SCALE), (long)(50 * SCALE))
    };

    // 执行并集运算
    Clipper clipper = new Clipper();
    clipper.AddPath(square1, PolyType.ptSubject, true);
    clipper.AddPath(square2, PolyType.ptClip, true);

    Paths solution = new Paths();
    clipper.Execute(
        ClipType.ctUnion,
        solution,
        PolyFillType.pftNonZero,
        PolyFillType.pftNonZero
    );

    Console.WriteLine($"并集产生 {solution.Count} 个多边形");

    foreach (var path in solution)
    {
        double area = Math.Abs(Clipper.Area(path)) / (SCALE * SCALE);
        Console.WriteLine($"合并后面积: {area}");
        Console.WriteLine($"顶点数: {path.Count}");
    }
}

3.4.3 多个多边形的合并

并集运算特别适合合并多个多边形:

static void MergeMultiplePolygons()
{
    const double SCALE = 1000.0;

    // 创建一组相互重叠的矩形
    Paths rectangles = new Paths();

    for (int i = 0; i < 10; i++)
    {
        double x = i * 15.0;  // 有重叠
        Path rect = new Path
        {
            new IntPoint((long)(x * SCALE), 0),
            new IntPoint((long)((x + 20) * SCALE), 0),
            new IntPoint((long)((x + 20) * SCALE), (long)(30 * SCALE)),
            new IntPoint((long)(x * SCALE), (long)(30 * SCALE))
        };
        rectangles.Add(rect);
    }

    Console.WriteLine($"原始矩形数量: {rectangles.Count}");

    // 合并所有矩形
    Clipper clipper = new Clipper();
    clipper.AddPaths(rectangles, PolyType.ptSubject, true);

    Paths merged = new Paths();
    clipper.Execute(
        ClipType.ctUnion,
        merged,
        PolyFillType.pftNonZero,
        PolyFillType.pftNonZero
    );

    Console.WriteLine($"合并后多边形数量: {merged.Count}");

    foreach (var path in merged)
    {
        double area = Math.Abs(Clipper.Area(path)) / (SCALE * SCALE);
        Console.WriteLine($"面积: {area:F2}, 顶点数: {path.Count}");
    }
}

3.4.4 并集的实际应用

应用1:地块合并

class LandMerger
{
    private const double SCALE = 1000000.0;

    /// <summary>
    /// 合并相邻地块
    /// </summary>
    public static Paths MergeAdjacentParcels(List<Path> parcels)
    {
        if (parcels.Count == 0)
            return new Paths();

        Clipper clipper = new Clipper();

        // 添加所有地块
        foreach (var parcel in parcels)
        {
            clipper.AddPath(parcel, PolyType.ptSubject, true);
        }

        // 执行并集运算
        Paths merged = new Paths();
        clipper.Execute(
            ClipType.ctUnion,
            merged,
            PolyFillType.pftNonZero,
            PolyFillType.pftNonZero
        );

        return merged;
    }

    /// <summary>
    /// 合并后计算总面积和节省的边界长度
    /// </summary>
    public static (double totalArea, double boundaryReduction) AnalyzeMerge(
        List<Path> originalParcels,
        Paths mergedParcels)
    {
        // 计算原始边界总长度
        double originalBoundary = 0;
        foreach (var parcel in originalParcels)
        {
            originalBoundary += CalculatePerimeter(parcel);
        }

        // 计算合并后边界总长度
        double mergedBoundary = 0;
        foreach (var parcel in mergedParcels)
        {
            mergedBoundary += CalculatePerimeter(parcel);
        }

        // 计算总面积
        double totalArea = 0;
        foreach (var parcel in mergedParcels)
        {
            totalArea += Math.Abs(Clipper.Area(parcel));
        }
        totalArea /= (SCALE * SCALE);

        double reduction = originalBoundary - mergedBoundary;

        return (totalArea, reduction / SCALE);
    }

    private static double CalculatePerimeter(Path path)
    {
        if (path.Count < 2) return 0;

        double perimeter = 0;
        for (int i = 0; i < path.Count; i++)
        {
            int next = (i + 1) % path.Count;
            long dx = path[next].X - path[i].X;
            long dy = path[next].Y - path[i].Y;
            perimeter += Math.Sqrt(dx * dx + dy * dy);
        }
        return perimeter;
    }
}

应用2:缓冲区合并

class BufferMerger
{
    private const double SCALE = 1000.0;

    /// <summary>
    /// 创建多个点的缓冲区并合并
    /// </summary>
    public static Paths CreateMergedBuffers(
        List<(double x, double y)> points,
        double bufferRadius)
    {
        Paths buffers = new Paths();

        // 为每个点创建缓冲区(圆形)
        foreach (var point in points)
        {
            Path circle = CreateCircle(point.x, point.y, bufferRadius, 32, SCALE);
            buffers.Add(circle);
        }

        // 合并所有缓冲区
        Clipper clipper = new Clipper();
        clipper.AddPaths(buffers, PolyType.ptSubject, true);

        Paths merged = new Paths();
        clipper.Execute(
            ClipType.ctUnion,
            merged,
            PolyFillType.pftNonZero,
            PolyFillType.pftNonZero
        );

        return merged;
    }

    private static Path CreateCircle(double cx, double cy, double radius,
        int segments, double scale)
    {
        Path circle = new Path();
        for (int i = 0; i < segments; i++)
        {
            double angle = 2 * Math.PI * i / segments;
            double x = cx + radius * Math.Cos(angle);
            double y = cy + radius * Math.Sin(angle);
            circle.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
        }
        return circle;
    }
}

3.5 差集运算(Difference)

3.5.1 基本概念

差集运算从主题多边形中减去裁剪多边形,结果是主题多边形中不与裁剪多边形重叠的部分。注意:主题和裁剪的角色很重要。

3.5.2 基本差集示例

static void BasicDifference()
{
    const double SCALE = 1000.0;

    // 创建一个大矩形
    Path bigRect = new Path
    {
        new IntPoint(0, 0),
        new IntPoint((long)(100 * SCALE), 0),
        new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
        new IntPoint(0, (long)(100 * SCALE))
    };

    // 创建一个小圆形(将从大矩形中挖除)
    Path smallCircle = CreateCircle(50, 50, 20, 32, SCALE);

    // 执行差集运算:大矩形 - 小圆形
    Clipper clipper = new Clipper();
    clipper.AddPath(bigRect, PolyType.ptSubject, true);    // 主题
    clipper.AddPath(smallCircle, PolyType.ptClip, true);   // 裁剪

    Paths solution = new Paths();
    clipper.Execute(
        ClipType.ctDifference,
        solution,
        PolyFillType.pftNonZero,
        PolyFillType.pftNonZero
    );

    Console.WriteLine($"差集产生 {solution.Count} 个多边形");

    foreach (var path in solution)
    {
        double area = Math.Abs(Clipper.Area(path)) / (SCALE * SCALE);
        Console.WriteLine($"面积: {area:F2}");
        Console.WriteLine($"顶点数: {path.Count}");
    }
}

static Path CreateCircle(double cx, double cy, double radius, int segments, double scale)
{
    Path circle = new Path();
    for (int i = 0; i < segments; i++)
    {
        double angle = 2 * Math.PI * i / segments;
        double x = cx + radius * Math.Cos(angle);
        double y = cy + radius * Math.Sin(angle);
        circle.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
    }
    return circle;
}

3.5.3 多重挖孔

可以从一个多边形中挖除多个孔洞:

static void MultipleHoles()
{
    const double SCALE = 1000.0;

    // 创建基础矩形
    Path baseRect = new Path
    {
        new IntPoint(0, 0),
        new IntPoint((long)(200 * SCALE), 0),
        new IntPoint((long)(200 * SCALE), (long)(200 * SCALE)),
        new IntPoint(0, (long)(200 * SCALE))
    };

    // 创建多个要挖除的圆形孔洞
    Paths holes = new Paths();
    int[,] holePositions = { { 50, 50 }, { 150, 50 }, { 50, 150 }, { 150, 150 } };

    for (int i = 0; i < holePositions.GetLength(0); i++)
    {
        Path hole = CreateCircle(
            holePositions[i, 0],
            holePositions[i, 1],
            15, 32, SCALE
        );
        holes.Add(hole);
    }

    // 执行差集运算
    Clipper clipper = new Clipper();
    clipper.AddPath(baseRect, PolyType.ptSubject, true);
    clipper.AddPaths(holes, PolyType.ptClip, true);

    // 使用 PolyTree 以保持层次结构
    PolyTree solution = new PolyTree();
    clipper.Execute(
        ClipType.ctDifference,
        solution,
        PolyFillType.pftNonZero,
        PolyFillType.pftNonZero
    );

    Console.WriteLine("多重挖孔结果:");
    PrintPolyTree(solution, 0);
}

static void PrintPolyTree(PolyNode node, int level)
{
    const double SCALE = 1000.0;
    string indent = new string(' ', level * 2);

    foreach (var child in node.Childs)
    {
        double area = Math.Abs(Clipper.Area(child.Contour)) / (SCALE * SCALE);
        Console.WriteLine($"{indent}{(child.IsHole ? "孔洞" : "外轮廓")}");
        Console.WriteLine($"{indent}  面积: {area:F2}");
        Console.WriteLine($"{indent}  顶点数: {child.Contour.Count}");

        if (child.Childs.Count > 0)
        {
            PrintPolyTree(child, level + 1);
        }
    }
}

3.5.4 差集的实际应用

应用1:零件挖孔

class PartManufacturing
{
    private const double SCALE = 1000.0;

    /// <summary>
    /// 在零件上创建孔洞
    /// </summary>
    public static Path CreatePartWithHoles(
        Path partOutline,
        List<(double x, double y, double radius)> holes)
    {
        Clipper clipper = new Clipper();
        clipper.AddPath(partOutline, PolyType.ptSubject, true);

        // 添加所有孔洞
        foreach (var hole in holes)
        {
            Path holeCircle = CreateCircle(
                hole.x, hole.y, hole.radius, 32, SCALE
            );
            clipper.AddPath(holeCircle, PolyType.ptClip, true);
        }

        Paths result = new Paths();
        clipper.Execute(
            ClipType.ctDifference,
            result,
            PolyFillType.pftNonZero,
            PolyFillType.pftNonZero
        );

        return result.Count > 0 ? result[0] : new Path();
    }

    /// <summary>
    /// 计算材料利用率
    /// </summary>
    public static double CalculateMaterialUtilization(
        Path originalPart,
        Path partWithHoles)
    {
        double originalArea = Math.Abs(Clipper.Area(originalPart));
        double finalArea = Math.Abs(Clipper.Area(partWithHoles));

        return (finalArea / originalArea) * 100.0;
    }

    private static Path CreateCircle(double cx, double cy, double radius,
        int segments, double scale)
    {
        Path circle = new Path();
        for (int i = 0; i < segments; i++)
        {
            double angle = 2 * Math.PI * i / segments;
            double x = cx + radius * Math.Cos(angle);
            double y = cy + radius * Math.Sin(angle);
            circle.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
        }
        return circle;
    }
}

应用2:禁入区域计算

class RestrictedAreaCalculator
{
    private const double SCALE = 1000000.0;

    /// <summary>
    /// 计算可用区域(总区域减去禁入区域)
    /// </summary>
    public static Paths CalculateAvailableArea(
        Path totalArea,
        List<Path> restrictedAreas)
    {
        Clipper clipper = new Clipper();
        clipper.AddPath(totalArea, PolyType.ptSubject, true);
        clipper.AddPaths(restrictedAreas, PolyType.ptClip, true);

        Paths available = new Paths();
        clipper.Execute(
            ClipType.ctDifference,
            available,
            PolyFillType.pftNonZero,
            PolyFillType.pftNonZero
        );

        return available;
    }

    /// <summary>
    /// 生成报告
    /// </summary>
    public static void GenerateAreaReport(
        Path totalArea,
        List<Path> restrictedAreas,
        Paths availableAreas)
    {
        double total = Math.Abs(Clipper.Area(totalArea)) / (SCALE * SCALE);

        double restricted = 0;
        foreach (var area in restrictedAreas)
        {
            restricted += Math.Abs(Clipper.Area(area));
        }
        restricted /= (SCALE * SCALE);

        double available = 0;
        foreach (var area in availableAreas)
        {
            available += Math.Abs(Clipper.Area(area));
        }
        available /= (SCALE * SCALE);

        Console.WriteLine("=== 区域分析报告 ===");
        Console.WriteLine($"总面积: {total:F2} 平方单位");
        Console.WriteLine($"禁入区域面积: {restricted:F2} 平方单位 ({restricted / total * 100:F1}%)");
        Console.WriteLine($"可用面积: {available:F2} 平方单位 ({available / total * 100:F1}%)");
        Console.WriteLine($"可用区域数量: {availableAreas.Count}");
    }
}

3.6 异或运算(XOR)

3.6.1 基本概念

异或运算计算两个多边形不重叠的部分,即属于其中一个但不同时属于两者的区域。结果通常会产生多个独立的多边形。

3.6.2 基本异或示例

static void BasicXor()
{
    const double SCALE = 1000.0;

    // 创建两个重叠的圆形
    Path circle1 = CreateCircle(40, 50, 30, 32, SCALE);
    Path circle2 = CreateCircle(60, 50, 30, 32, SCALE);

    // 执行异或运算
    Clipper clipper = new Clipper();
    clipper.AddPath(circle1, PolyType.ptSubject, true);
    clipper.AddPath(circle2, PolyType.ptClip, true);

    Paths solution = new Paths();
    clipper.Execute(
        ClipType.ctXor,
        solution,
        PolyFillType.pftNonZero,
        PolyFillType.pftNonZero
    );

    Console.WriteLine($"异或产生 {solution.Count} 个多边形");

    for (int i = 0; i < solution.Count; i++)
    {
        double area = Math.Abs(Clipper.Area(solution[i])) / (SCALE * SCALE);
        Console.WriteLine($"多边形 {i + 1}: 面积 = {area:F2}, 顶点数 = {solution[i].Count}");
    }
}

static Path CreateCircle(double cx, double cy, double radius, int segments, double scale)
{
    Path circle = new Path();
    for (int i = 0; i < segments; i++)
    {
        double angle = 2 * Math.PI * i / segments;
        double x = cx + radius * Math.Cos(angle);
        double y = cy + radius * Math.Sin(angle);
        circle.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
    }
    return circle;
}

3.6.3 异或的实际应用

应用:差异区域分析

class DifferenceAnalyzer
{
    private const double SCALE = 1000000.0;

    /// <summary>
    /// 分析两个规划方案的差异
    /// </summary>
    public static void AnalyzePlanDifferences(
        Path plan1,
        Path plan2,
        string plan1Name,
        string plan2Name)
    {
        // 计算异或(差异区域)
        Clipper clipper = new Clipper();
        clipper.AddPath(plan1, PolyType.ptSubject, true);
        clipper.AddPath(plan2, PolyType.ptClip, true);

        Paths differences = new Paths();
        clipper.Execute(
            ClipType.ctXor,
            differences,
            PolyFillType.pftNonZero,
            PolyFillType.pftNonZero
        );

        // 计算交集(相同区域)
        clipper.Clear();
        clipper.AddPath(plan1, PolyType.ptSubject, true);
        clipper.AddPath(plan2, PolyType.ptClip, true);

        Paths intersection = new Paths();
        clipper.Execute(
            ClipType.ctIntersection,
            intersection,
            PolyFillType.pftNonZero,
            PolyFillType.pftNonZero
        );

        // 计算面积
        double area1 = Math.Abs(Clipper.Area(plan1)) / (SCALE * SCALE);
        double area2 = Math.Abs(Clipper.Area(plan2)) / (SCALE * SCALE);

        double diffArea = 0;
        foreach (var path in differences)
        {
            diffArea += Math.Abs(Clipper.Area(path));
        }
        diffArea /= (SCALE * SCALE);

        double commonArea = 0;
        foreach (var path in intersection)
        {
            commonArea += Math.Abs(Clipper.Area(path));
        }
        commonArea /= (SCALE * SCALE);

        // 生成报告
        Console.WriteLine("=== 方案差异分析 ===");
        Console.WriteLine($"{plan1Name} 面积: {area1:F2}");
        Console.WriteLine($"{plan2Name} 面积: {area2:F2}");
        Console.WriteLine($"共同区域面积: {commonArea:F2}");
        Console.WriteLine($"差异区域面积: {diffArea:F2}");
        Console.WriteLine($"相似度: {(commonArea / Math.Max(area1, area2) * 100):F1}%");
        Console.WriteLine($"差异区域数量: {differences.Count}");
    }
}

3.7 填充规则详解

3.7.1 填充规则的重要性

填充规则决定了如何判断一个区域是多边形的"内部"还是"外部",这对布尔运算的结果有重要影响。

3.7.2 四种填充规则

NonZero(非零规则)

  • 最常用的规则
  • 基于环绕数判断内外
  • 适合大多数场景

EvenOdd(奇偶规则)

  • 基于射线交点数的奇偶性
  • 对自相交多边形的处理不同
  • 适合某些特殊图形

Positive(正数规则)

  • 只考虑正环绕数
  • 用于特定的几何处理

Negative(负数规则)

  • 只考虑负环绕数
  • 较少使用

3.7.3 填充规则对比示例

static void CompareFillingRules()
{
    const double SCALE = 1000.0;

    // 创建一个自相交的多边形(八字形)
    Path figureEight = new Path
    {
        new IntPoint(0, 0),
        new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
        new IntPoint((long)(100 * SCALE), 0),
        new IntPoint(0, (long)(100 * SCALE))
    };

    // 测试不同的填充规则
    PolyFillType[] fillTypes = {
        PolyFillType.pftNonZero,
        PolyFillType.pftEvenOdd,
        PolyFillType.pftPositive,
        PolyFillType.pftNegative
    };

    string[] fillNames = { "NonZero", "EvenOdd", "Positive", "Negative" };

    for (int i = 0; i < fillTypes.Length; i++)
    {
        Clipper clipper = new Clipper();
        clipper.AddPath(figureEight, PolyType.ptSubject, true);

        Paths solution = new Paths();
        clipper.Execute(
            ClipType.ctUnion,  // 使用并集来观察填充规则的效果
            solution,
            fillTypes[i],
            fillTypes[i]
        );

        Console.WriteLine($"\n填充规则: {fillNames[i]}");
        Console.WriteLine($"  产生多边形数量: {solution.Count}");

        double totalArea = 0;
        foreach (var path in solution)
        {
            double area = Math.Abs(Clipper.Area(path)) / (SCALE * SCALE);
            totalArea += area;
            Console.WriteLine($"  多边形面积: {area:F2}");
        }
        Console.WriteLine($"  总面积: {totalArea:F2}");
    }
}

3.8 高级技巧和最佳实践

3.8.1 性能优化

class PerformanceOptimizer
{
    /// <summary>
    /// 批量处理多个运算
    /// </summary>
    public static List<Paths> BatchOperations(
        List<(Path subject, Path clip, ClipType operation)> operations)
    {
        var results = new List<Paths>();

        // 复用 Clipper 对象
        Clipper clipper = new Clipper();

        foreach (var op in operations)
        {
            clipper.Clear();  // 清除之前的数据

            clipper.AddPath(op.subject, PolyType.ptSubject, true);
            clipper.AddPath(op.clip, PolyType.ptClip, true);

            Paths solution = new Paths();
            clipper.Execute(
                op.operation,
                solution,
                PolyFillType.pftNonZero,
                PolyFillType.pftNonZero
            );

            results.Add(solution);
        }

        return results;
    }

    /// <summary>
    /// 预处理:简化输入多边形
    /// </summary>
    public static Path PreprocessPolygon(Path input, double tolerance = 1.0)
    {
        // 移除共线点
        Path cleaned = new Path(input);
        Clipper.CleanPolygon(cleaned, tolerance);

        // 简化自相交
        Paths simplified = Clipper.SimplifyPolygon(
            cleaned,
            PolyFillType.pftNonZero
        );

        return simplified.Count > 0 ? simplified[0] : cleaned;
    }
}

3.8.2 错误处理和验证

class ValidationHelper
{
    /// <summary>
    /// 验证路径有效性
    /// </summary>
    public static bool IsValidPath(Path path, bool closed)
    {
        if (path == null)
            return false;

        // 封闭路径至少需要3个点
        if (closed && path.Count < 3)
            return false;

        // 开放路径至少需要2个点
        if (!closed && path.Count < 2)
            return false;

        return true;
    }

    /// <summary>
    /// 安全执行布尔运算
    /// </summary>
    public static (bool success, Paths result, string error) SafeExecute(
        Path subject,
        Path clip,
        ClipType operation)
    {
        try
        {
            if (!IsValidPath(subject, true))
                return (false, null, "主题多边形无效");

            if (!IsValidPath(clip, true))
                return (false, null, "裁剪多边形无效");

            Clipper clipper = new Clipper();
            clipper.AddPath(subject, PolyType.ptSubject, true);
            clipper.AddPath(clip, PolyType.ptClip, true);

            Paths solution = new Paths();
            bool success = clipper.Execute(
                operation,
                solution,
                PolyFillType.pftNonZero,
                PolyFillType.pftNonZero
            );

            if (!success)
                return (false, null, "运算执行失败");

            return (true, solution, null);
        }
        catch (Exception ex)
        {
            return (false, null, $"异常: {ex.Message}");
        }
    }
}

3.8.3 结果后处理

class ResultProcessor
{
    /// <summary>
    /// 过滤小面积多边形
    /// </summary>
    public static Paths FilterByArea(Paths input, double minArea, double scale)
    {
        Paths filtered = new Paths();
        double threshold = minArea * scale * scale;

        foreach (var path in input)
        {
            double area = Math.Abs(Clipper.Area(path));
            if (area >= threshold)
            {
                filtered.Add(path);
            }
        }

        return filtered;
    }

    /// <summary>
    /// 按面积排序
    /// </summary>
    public static Paths SortByArea(Paths input, bool descending = true)
    {
        var sorted = input.OrderBy(p => Math.Abs(Clipper.Area(p))).ToList();
        if (descending)
            sorted.Reverse();
        return sorted;
    }

    /// <summary>
    /// 清理和优化结果
    /// </summary>
    public static Paths CleanResult(Paths input, double tolerance = 1.415)
    {
        Paths cleaned = new Paths();

        foreach (var path in input)
        {
            Path cleanPath = new Path(path);
            Clipper.CleanPolygon(cleanPath, tolerance);

            if (cleanPath.Count >= 3)
            {
                cleaned.Add(cleanPath);
            }
        }

        return cleaned;
    }
}

3.9 综合实战示例

让我们通过一个完整的实际应用来综合运用本章所学:

using System;
using System.Collections.Generic;
using System.Linq;
using ClipperLib;

using Path = System.Collections.Generic.List<ClipperLib.IntPoint>;
using Paths = System.Collections.Generic.List<System.Collections.Generic.List<ClipperLib.IntPoint>>;

namespace ClipperBooleanDemo
{
    /// <summary>
    /// 建筑用地规划系统
    /// </summary>
    class LandPlanningSystem
    {
        private const double SCALE = 1000000.0;

        public static void Main(string[] args)
        {
            Console.WriteLine("=== 建筑用地规划系统 ===\n");

            // 1. 定义总用地范围
            Path totalLand = CreateRectangle(0, 0, 200, 150);
            Console.WriteLine($"总用地面积: {GetArea(totalLand):F2} 平方米");

            // 2. 定义限制区域(道路、河流等)
            List<Path> restrictions = new List<Path>
            {
                CreateRectangle(0, 70, 200, 80),      // 横向道路
                CreateRectangle(95, 0, 105, 150),     // 纵向道路
                CreateCircle(50, 40, 15, 32)          // 水池
            };

            double restrictedArea = restrictions.Sum(p => GetArea(p));
            Console.WriteLine($"限制区域面积: {restrictedArea:F2} 平方米");

            // 3. 计算可用建设区域(总用地 - 限制区域)
            Paths availableLand = SubtractAreas(totalLand, restrictions);
            double availableArea = availableLand.Sum(p => GetArea(p));
            Console.WriteLine($"\n可用建设区域:");
            Console.WriteLine($"  区域数量: {availableLand.Count}");
            Console.WriteLine($"  总面积: {availableArea:F2} 平方米");
            Console.WriteLine($"  利用率: {(availableArea / GetArea(totalLand) * 100):F1}%");

            // 4. 规划建筑物
            List<Path> buildings = new List<Path>
            {
                CreateRectangle(10, 10, 40, 60),      // 建筑1
                CreateRectangle(50, 10, 80, 60),      // 建筑2
                CreateRectangle(110, 10, 140, 60),    // 建筑3
                CreateRectangle(150, 10, 180, 60),    // 建筑4
                CreateRectangle(10, 90, 40, 140),     // 建筑5
                CreateRectangle(110, 90, 140, 140),   // 建筑6
                CreateRectangle(150, 90, 180, 140)    // 建筑7
            };

            Console.WriteLine($"\n规划建筑数量: {buildings.Count}");
            double buildingArea = buildings.Sum(p => GetArea(p));
            Console.WriteLine($"建筑占地面积: {buildingArea:F2} 平方米");

            // 5. 检查建筑是否在可用区域内
            Console.WriteLine("\n建筑合规性检查:");
            int validCount = 0;
            for (int i = 0; i < buildings.Count; i++)
            {
                bool isValid = CheckBuildingCompliance(buildings[i], availableLand);
                Console.WriteLine($"  建筑{i + 1}: {(isValid ? "✓ 合规" : "✗ 不合规")}");
                if (isValid) validCount++;
            }

            // 6. 计算容积率
            Console.WriteLine($"\n规划指标:");
            double floorArea = buildingArea * 5;  // 假设5层建筑
            double plotRatio = floorArea / GetArea(totalLand);
            Console.WriteLine($"  建筑密度: {(buildingArea / GetArea(totalLand) * 100):F1}%");
            Console.WriteLine($"  容积率: {plotRatio:F2}");

            // 7. 创建绿化带(在可用区域内减去建筑)
            Paths greenSpace = CalculateGreenSpace(availableLand, buildings);
            double greenArea = greenSpace.Sum(p => GetArea(p));
            Console.WriteLine($"  绿化面积: {greenArea:F2} 平方米");
            Console.WriteLine($"  绿化率: {(greenArea / GetArea(totalLand) * 100):F1}%");

            // 8. 生成建筑物缓冲区(安全距离)
            double safetyDistance = 5.0;  // 5米安全距离
            Paths safetyZones = CreateSafetyBuffers(buildings, safetyDistance);
            Console.WriteLine($"\n安全缓冲区:");
            Console.WriteLine($"  缓冲距离: {safetyDistance} 米");

            // 9. 检查建筑间距
            Console.WriteLine("\n建筑间距检查:");
            CheckBuildingSpacing(buildings, safetyDistance);

            Console.WriteLine("\n按任意键退出...");
            Console.ReadKey();
        }

        // 创建矩形
        static Path CreateRectangle(double x1, double y1, double x2, double y2)
        {
            return new Path
            {
                new IntPoint((long)(x1 * SCALE), (long)(y1 * SCALE)),
                new IntPoint((long)(x2 * SCALE), (long)(y1 * SCALE)),
                new IntPoint((long)(x2 * SCALE), (long)(y2 * SCALE)),
                new IntPoint((long)(x1 * SCALE), (long)(y2 * SCALE))
            };
        }

        // 创建圆形
        static Path CreateCircle(double cx, double cy, double radius, int segments)
        {
            Path circle = new Path();
            for (int i = 0; i < segments; i++)
            {
                double angle = 2 * Math.PI * i / segments;
                double x = cx + radius * Math.Cos(angle);
                double y = cy + radius * Math.Sin(angle);
                circle.Add(new IntPoint((long)(x * SCALE), (long)(y * SCALE)));
            }
            return circle;
        }

        // 计算面积
        static double GetArea(Path path)
        {
            return Math.Abs(Clipper.Area(path)) / (SCALE * SCALE);
        }

        // 差集运算:从总区域减去限制区域
        static Paths SubtractAreas(Path total, List<Path> restrictions)
        {
            Clipper clipper = new Clipper();
            clipper.AddPath(total, PolyType.ptSubject, true);
            clipper.AddPaths(restrictions, PolyType.ptClip, true);

            Paths result = new Paths();
            clipper.Execute(ClipType.ctDifference, result);
            return result;
        }

        // 检查建筑是否在可用区域内
        static bool CheckBuildingCompliance(Path building, Paths availableAreas)
        {
            // 计算建筑与可用区域的交集
            Clipper clipper = new Clipper();
            clipper.AddPaths(availableAreas, PolyType.ptSubject, true);
            clipper.AddPath(building, PolyType.ptClip, true);

            Paths intersection = new Paths();
            clipper.Execute(ClipType.ctIntersection, intersection);

            if (intersection.Count == 0)
                return false;

            // 检查交集面积是否等于建筑面积(允许小误差)
            double buildingArea = GetArea(building);
            double intersectionArea = intersection.Sum(p => GetArea(p));

            return Math.Abs(buildingArea - intersectionArea) < 0.01;
        }

        // 计算绿化空间
        static Paths CalculateGreenSpace(Paths availableAreas, List<Path> buildings)
        {
            // 先合并所有建筑
            Clipper clipper = new Clipper();
            clipper.AddPaths(buildings, PolyType.ptSubject, true);

            Paths mergedBuildings = new Paths();
            clipper.Execute(ClipType.ctUnion, mergedBuildings);

            // 从可用区域减去建筑
            clipper.Clear();
            clipper.AddPaths(availableAreas, PolyType.ptSubject, true);
            clipper.AddPaths(mergedBuildings, PolyType.ptClip, true);

            Paths greenSpace = new Paths();
            clipper.Execute(ClipType.ctDifference, greenSpace);

            return greenSpace;
        }

        // 创建安全缓冲区
        static Paths CreateSafetyBuffers(List<Path> buildings, double distance)
        {
            ClipperOffset offset = new ClipperOffset();
            offset.AddPaths(buildings, JoinType.jtRound, EndType.etClosedPolygon);

            Paths buffered = new Paths();
            offset.Execute(ref buffered, distance * SCALE);

            return buffered;
        }

        // 检查建筑间距
        static void CheckBuildingSpacing(List<Path> buildings, double minDistance)
        {
            for (int i = 0; i < buildings.Count; i++)
            {
                for (int j = i + 1; j < buildings.Count; j++)
                {
                    // 创建缓冲区
                    ClipperOffset offset = new ClipperOffset();
                    offset.AddPath(buildings[i], JoinType.jtRound, EndType.etClosedPolygon);

                    Paths buffered = new Paths();
                    offset.Execute(ref buffered, minDistance * SCALE);

                    // 检查是否与另一建筑相交
                    Clipper clipper = new Clipper();
                    clipper.AddPaths(buffered, PolyType.ptSubject, true);
                    clipper.AddPath(buildings[j], PolyType.ptClip, true);

                    Paths intersection = new Paths();
                    clipper.Execute(ClipType.ctIntersection, intersection);

                    bool hasConflict = intersection.Count > 0;
                    if (hasConflict)
                    {
                        Console.WriteLine($"  建筑{i + 1} 与 建筑{j + 1}: ✗ 间距不足");
                    }
                }
            }
        }
    }
}

3.10 本章小结

在本章中,我们深入学习了 Clipper1 的布尔运算功能:

  1. 四种基本运算:交集、并集、差集、异或
  2. 标准使用流程:创建 Clipper 对象、添加路径、执行运算、处理结果
  3. 填充规则:NonZero、EvenOdd 及其影响
  4. 实际应用:碰撞检测、地块合并、零件挖孔、区域分析等
  5. 高级技巧:性能优化、错误处理、结果后处理

重点掌握:

  • 理解每种运算的几何含义
  • 正确区分主题和裁剪多边形
  • 选择合适的填充规则
  • 处理复杂的多层嵌套结构
  • 在实际项目中灵活应用

在下一章中,我们将学习 Clipper1 的另一个重要功能:多边形偏移操作,包括膨胀、收缩以及各种连接和端点类型的使用。

3.11 练习题

  1. 基础练习:创建两个任意多边形,分别执行四种布尔运算,观察结果

  2. 碰撞检测:实现一个简单的2D游戏碰撞检测系统

  3. 地图合并:给定多个重叠的行政区划边界,合并为一个完整的区域

  4. 挖孔操作:在一个复杂多边形上创建多个不规则孔洞

  5. 差异分析:比较两个版本的设计图,找出修改的部分

  6. 综合应用:实现一个简单的CAD布尔运算工具,支持多个图形的复杂运算

posted @ 2025-12-18 09:26  我才是银古  阅读(30)  评论(0)    收藏  举报