znlgis 博客

GIS开发与技术分享

第三章 布尔运算操作(C#版)

3.1 引言

布尔运算是Clipper2最核心的功能之一,它允许我们对多边形进行交集、并集、差集和异或等操作。这些操作在计算机图形学、CAD设计、GIS分析等领域有着广泛的应用。本章将深入介绍Clipper2 C#版本中布尔运算的原理、使用方法和最佳实践。

3.2 布尔运算的基本概念

3.2.1 什么是布尔运算

布尔运算源自数学中的集合论,用于组合两个或多个几何形状。在二维几何中,布尔运算主要包括四种基本操作:

交集(Intersection)

交集运算返回两个多边形的重叠区域。只有同时属于两个输入多边形的点才会出现在结果中。

数学表示:A ∩ B

应用场景:

并集(Union)

并集运算将两个多边形合并为一个,得到它们的完整覆盖区域。

数学表示:A ∪ B

应用场景:

差集(Difference)

差集运算从第一个多边形中减去第二个多边形的区域。

数学表示:A - B 或 A \ B

应用场景:

异或(XOR)

异或运算返回两个多边形不重叠的区域,即只属于其中一个多边形的部分。

数学表示:A ⊕ B 或 (A ∪ B) - (A ∩ B)

应用场景:

3.2.2 主体与裁剪

在Clipper2中,参与布尔运算的多边形被分为两类:

主体(Subject):布尔运算的主要对象,差集运算中被减去的一方

裁剪(Clip):用于与主体进行运算的多边形,差集运算中作为”刀”的一方

using Clipper2Lib;

class SubjectClipExample
{
    static void Main()
    {
        Paths64 subject = new Paths64();  // 主体多边形
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));

        Paths64 clip = new Paths64();     // 裁剪多边形
        clip.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));

        // 对于交集、并集、异或,subject和clip的顺序不影响结果
        // 对于差集,subject - clip 和 clip - subject 的结果不同
    }
}

3.2.3 填充规则的作用

填充规则决定了多边形的哪些区域被认为是”内部”。这对于处理自交多边形和复杂的嵌套结构至关重要。

Clipper2支持四种填充规则:

public enum FillRule
{
    EvenOdd,   // 偶奇规则
    NonZero,   // 非零规则
    Positive,  // 正数规则
    Negative   // 负数规则
}

详细的填充规则说明请参见第一章。

3.3 使用简化API进行布尔运算

Clipper2提供了一组简化的静态函数,可以用一行代码完成布尔运算。

3.3.1 交集运算

using System;
using Clipper2Lib;

class IntersectionExample
{
    static void Main()
    {
        // 创建两个相交的正方形
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));
        
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));
        
        // 执行交集运算
        Paths64 result = Clipper.Intersect(subject, clip, FillRule.NonZero);
        
        // result 包含一个50x50的正方形:(50,50) → (100,50) → (100,100) → (50,100)
        Console.WriteLine($"交集结果包含 {result.Count} 个多边形");
        
        foreach (Path64 path in result)
        {
            Console.Write("顶点: ");
            foreach (Point64 pt in path)
            {
                Console.Write($"({pt.X},{pt.Y}) ");
            }
            Console.WriteLine();
        }
    }
}

3.3.2 并集运算

using Clipper2Lib;

class UnionExample
{
    static void Main()
    {
        // 合并两个正方形
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));

        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));

        Paths64 result = Clipper.Union(subject, clip, FillRule.NonZero);
        // result 包含一个L形多边形,覆盖两个正方形的区域
        
        Console.WriteLine($"并集结果包含 {result.Count} 个多边形");
    }
}
**单一集合的并集**

对于只有主体多边形(没有裁剪多边形)的情况,Union函数可以用于:
- 合并重叠的多边形
- 简化自交多边形
- 分解复杂多边形为简单多边形

```csharp
using Clipper2Lib;

class UnionSingleSetExample
{
    static void Main()
    {
        // 合并多个重叠的多边形
        Paths64 circles = new Paths64();
        circles.Add(Clipper.MakePath(new long[] { 0, 0, 50, 0, 50, 50, 0, 50 }));
        circles.Add(Clipper.MakePath(new long[] { 40, 0, 90, 0, 90, 50, 40, 50 }));
        circles.Add(Clipper.MakePath(new long[] { 80, 0, 130, 0, 130, 50, 80, 50 }));

        // 使用单参数版本的Union
        Paths64 merged = Clipper.Union(circles, FillRule.NonZero);
        Console.WriteLine($"合并后结果包含 {merged.Count} 个多边形");
    }
}

3.3.3 差集运算

using Clipper2Lib;

class DifferenceExample
{
    static void Main()
    {
        // 从大正方形中减去小正方形
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));

        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 25, 25, 75, 25, 75, 75, 25, 75 }));

        Paths64 result = Clipper.Difference(subject, clip, FillRule.NonZero);
        // result 包含一个带有中心孔洞的正方形
        Console.WriteLine($"差集结果包含 {result.Count} 个路径");

        // 注意:差集运算的顺序很重要
        // subject - clip ≠ clip - subject
        Paths64 result2 = Clipper.Difference(clip, subject, FillRule.NonZero);
        // result2 是空的,因为clip完全在subject内部
        Console.WriteLine($"反向差集结果包含 {result2.Count} 个路径");
    }
}

3.3.4 异或运算

using Clipper2Lib;

class XorExample
{
    static void Main()
    {
        // 两个部分重叠的正方形进行异或运算
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));

        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));

        Paths64 result = Clipper.Xor(subject, clip, FillRule.NonZero);
        // result 包含两个L形区域
        Console.WriteLine($"异或结果包含 {result.Count} 个多边形");
    }
}

3.3.5 使用浮点数坐标

对于浮点数坐标,可以使用PathsD类型:

using Clipper2Lib;

class FloatCoordinatesExample
{
    static void Main()
    {
        // 使用浮点数坐标
        PathsD subject = new PathsD();
        subject.Add(Clipper.MakePath(new double[] { 0.0, 0.0, 10.5, 0.0, 10.5, 10.5, 0.0, 10.5 }));

        PathsD clip = new PathsD();
        clip.Add(Clipper.MakePath(new double[] { 5.25, 5.25, 15.75, 5.25, 15.75, 15.75, 5.25, 15.75 }));

        // 第四个参数指定精度(小数位数)
        PathsD result = Clipper.Intersect(subject, clip, FillRule.NonZero, 2);
        Console.WriteLine($"结果包含 {result.Count} 个多边形");
    }
}

3.4 使用Clipper类进行高级控制

对于需要更多控制的场景,可以使用Clipper64或ClipperD类。

3.4.1 Clipper64类

using System;
using Clipper2Lib;

class Clipper64Example
{
    static void Main()
    {
        Clipper64 clipper = new Clipper64();
        
        // 添加主体多边形
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));
        clipper.AddSubject(subject);
        
        // 添加裁剪多边形
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));
        clipper.AddClip(clip);
        
        // 执行布尔运算
        Paths64 result = new Paths64();
        clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
        
        Console.WriteLine($"结果包含 {result.Count} 个多边形");
    }
}

3.4.2 ClipType枚举

public enum ClipType
{
    None,
    Intersection,
    Union,
    Difference,
    Xor
}

3.4.3 添加开放路径

除了闭合的多边形,Clipper2还支持开放的折线:

using Clipper2Lib;

class OpenPathExample
{
    static void Main()
    {
        Clipper64 clipper = new Clipper64();

        // 添加闭合的主体多边形
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));
        clipper.AddSubject(subject);

        // 添加开放的主体折线
        Paths64 subjectOpen = new Paths64();
        subjectOpen.Add(Clipper.MakePath(new long[] { 0, 50, 150, 50 }));  // 一条水平线
        clipper.AddOpenSubject(subjectOpen);

        // 添加裁剪多边形
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 25, 25, 75, 25, 75, 75, 25, 75 }));
        clipper.AddClip(clip);

        // 执行运算并分别获取闭合和开放结果
        Paths64 closedResult = new Paths64();
        Paths64 openResult = new Paths64();
        clipper.Execute(ClipType.Difference, FillRule.NonZero, closedResult, openResult);
        // closedResult 包含裁剪后的多边形
        // openResult 包含裁剪后的折线
        
        Console.WriteLine($"闭合路径结果: {closedResult.Count} 个");
        Console.WriteLine($"开放路径结果: {openResult.Count} 个");
    }
}

3.4.4 使用PolyTree输出

当需要保留多边形的层次结构时,使用PolyTree输出:

using Clipper2Lib;

class PolyTreeOutputExample
{
    static void Main()
    {
        Clipper64 clipper = new Clipper64();
        
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));
        clipper.AddSubject(subject);
        
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 25, 25, 75, 25, 75, 75, 25, 75 }));
        clipper.AddClip(clip);

        // 使用PolyTree输出
        PolyTree64 tree = new PolyTree64();
        Paths64 openPaths = new Paths64();
        clipper.Execute(ClipType.Intersection, FillRule.NonZero, tree, openPaths);

        // 遍历PolyTree获取层次结构
        for (int i = 0; i < tree.Count; i++)
        {
            PolyPath64 child = tree[i];
            if (!child.IsHole)
            {
                // 这是一个外边界多边形
                Path64 exterior = child.Polygon;
                Console.WriteLine($"外边界多边形顶点数: {exterior.Count}");
                
                // 获取其孔洞
                for (int j = 0; j < child.Count; j++)
                {
                    Path64 holePath = child[j].Polygon;
                    Console.WriteLine($"  孔洞顶点数: {holePath.Count}");
                }
            }
        }
    }
}

3.4.5 复用Clipper对象

在循环中复用Clipper对象可以提高性能:

using Clipper2Lib;

class ReuseClipperExample
{
    static void Main()
    {
        Clipper64 clipper = new Clipper64();
        
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));

        for (int i = 0; i < 1000; i++)
        {
            // 清空之前的数据,但保留已分配的内存
            clipper.Clear();
            
            // 添加新的多边形
            Paths64 subject = new Paths64();
            subject.Add(Clipper.MakePath(new long[] { i, 0, i + 100, 0, i + 100, 100, i, 100 }));
            
            clipper.AddSubject(subject);
            clipper.AddClip(clip);
            
            // 执行运算
            Paths64 result = new Paths64();
            clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
            
            // 处理结果...
        }
    }
}

3.5 处理复杂多边形

3.5.1 带孔洞的多边形

using Clipper2Lib;

class PolygonWithHoleExample
{
    static void Main()
    {
        // 创建一个带孔洞的多边形
        Paths64 subject = new Paths64();
        // 外边界(逆时针方向)
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));
        // 孔洞(顺时针方向)
        subject.Add(Clipper.MakePath(new long[] { 25, 25, 25, 75, 75, 75, 75, 25 }));

        // 裁剪多边形
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 50, 0, 150, 0, 150, 100, 50, 100 }));

        // 执行交集运算
        Paths64 result = Clipper.Intersect(subject, clip, FillRule.NonZero);
        // 结果会正确处理孔洞
        
        Console.WriteLine($"结果包含 {result.Count} 个路径");
    }
}

3.5.2 自交多边形

自交多边形是指边界与自身相交的多边形。不同的填充规则会产生不同的结果:

using Clipper2Lib;

class SelfIntersectingExample
{
    static void Main()
    {
        // 创建一个8字形的自交多边形
        Paths64 figure8 = new Paths64();
        figure8.Add(Clipper.MakePath(new long[] { 0, 0, 100, 100, 100, 0, 0, 100 }));

        // 使用NonZero规则 - 两个半部分都被填充
        Paths64 resultNonZero = Clipper.Union(figure8, FillRule.NonZero);
        Console.WriteLine($"NonZero规则结果: {resultNonZero.Count} 个多边形");

        // 使用EvenOdd规则 - 中间交叉点处会有孔洞
        Paths64 resultEvenOdd = Clipper.Union(figure8, FillRule.EvenOdd);
        Console.WriteLine($"EvenOdd规则结果: {resultEvenOdd.Count} 个多边形");
    }
}

3.5.3 多个分离的多边形

using Clipper2Lib;

class MultiplePolygonsExample
{
    static void Main()
    {
        // 主体包含多个分离的多边形
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 50, 0, 50, 50, 0, 50 }));
        subject.Add(Clipper.MakePath(new long[] { 100, 0, 150, 0, 150, 50, 100, 50 }));
        subject.Add(Clipper.MakePath(new long[] { 200, 0, 250, 0, 250, 50, 200, 50 }));

        // 裁剪多边形跨越多个主体
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 25, -25, 225, -25, 225, 75, 25, 75 }));

        // 交集运算会正确处理每个主体
        Paths64 result = Clipper.Intersect(subject, clip, FillRule.NonZero);
        // result 包含3个多边形
        Console.WriteLine($"结果包含 {result.Count} 个多边形");
    }
}

3.5.4 重叠的边

当两个多边形有共享的边时,Clipper2能够正确处理:

using Clipper2Lib;

class OverlappingEdgesExample
{
    static void Main()
    {
        // 两个共享一条边的正方形
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));

        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 100, 0, 200, 0, 200, 100, 100, 100 }));
        // clip的左边与subject的右边重合

        // 并集运算会正确合并它们
        Paths64 result = Clipper.Union(subject, clip, FillRule.NonZero);
        // result 是一个200x100的矩形
        Console.WriteLine($"结果包含 {result.Count} 个多边形");
    }
}

3.6 布尔运算的性能优化

3.6.1 减少顶点数量

顶点数量是影响性能的主要因素。可以使用路径简化来减少顶点:

using Clipper2Lib;

class SimplifyExample
{
    static void Main()
    {
        Paths64 complexPaths = new Paths64();
        // ... 添加复杂路径
        
        Paths64 clip = new Paths64();
        clip.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));
        
        // 在布尔运算前简化路径
        PathsD complexPathsD = Clipper.Paths64ToPathsD(complexPaths);
        PathsD simplified = Clipper.SimplifyPaths(complexPathsD, 2.0);
        Paths64 simplifiedPaths = Clipper.PathsDToPaths64(simplified);
        
        Paths64 result = Clipper.Intersect(simplifiedPaths, clip, FillRule.NonZero);
    }
}

3.6.2 使用矩形裁剪

当裁剪区域是矩形时,使用专门的RectClip函数:

using Clipper2Lib;

class RectClipPerformanceExample
{
    static void Main()
    {
        Paths64 subject = new Paths64();
        subject.Add(Clipper.MakePath(new long[] { 0, 0, 200, 0, 200, 200, 0, 200 }));
        
        // 常规布尔运算裁剪
        Paths64 clipPolygon = new Paths64();
        clipPolygon.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));
        Paths64 result1 = Clipper.Intersect(subject, clipPolygon, FillRule.NonZero);

        // 使用优化的矩形裁剪
        Rect64 clipRect = new Rect64(50, 50, 150, 150);
        Paths64 result2 = Clipper.RectClip(clipRect, subject);
        // result2 与 result1 相同,但速度更快
    }
}

3.6.3 批量处理

对于大量相同裁剪区域的操作,可以复用Clipper对象:

using Clipper2Lib;

class BatchProcessingExample
{
    static void ProcessBatch(List<Paths64> subjects, Paths64 clip)
    {
        Clipper64 clipper = new Clipper64();

        foreach (Paths64 subject in subjects)
        {
            clipper.Clear();
            clipper.AddClip(clip);
            clipper.AddSubject(subject);
            
            Paths64 result = new Paths64();
            clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
            
            // 处理结果...
        }
    }
}

3.6.4 多线程处理

Clipper2的Clipper对象不是线程安全的,但可以在不同线程中使用不同的Clipper实例:

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using Clipper2Lib;

class MultiThreadExample
{
    static void ProcessInParallel(List<Paths64> allSubjects, Paths64 clip)
    {
        int processorCount = Environment.ProcessorCount;
        
        Parallel.ForEach(allSubjects, subject =>
        {
            // 每个任务有自己的Clipper实例
            Clipper64 clipper = new Clipper64();
            clipper.AddSubject(subject);
            clipper.AddClip(clip);
            
            Paths64 result = new Paths64();
            clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
            
            // 处理结果...
        });
    }
}

3.7 布尔运算的常见问题

3.7.1 结果为空

问题:执行布尔运算后结果为空。

可能的原因

  1. 两个多边形不相交(对于交集运算)
  2. 多边形的方向不正确
  3. 坐标精度问题

解决方法

using Clipper2Lib;

class EmptyResultTroubleshooting
{
    static void DiagnoseEmptyResult(Paths64 subject, Paths64 clip)
    {
        // 检查多边形是否有效
        bool valid = subject.Count > 0 && clip.Count > 0;
        Console.WriteLine($"多边形有效: {valid}");

        // 检查边界框是否相交
        Rect64 subjectBounds = Clipper.GetBounds(subject);
        Rect64 clipBounds = Clipper.GetBounds(clip);
        
        bool mayIntersect = !(subjectBounds.right < clipBounds.left || 
                              clipBounds.right < subjectBounds.left ||
                              subjectBounds.bottom < clipBounds.top ||
                              clipBounds.bottom < subjectBounds.top);
        Console.WriteLine($"边界框可能相交: {mayIntersect}");

        // 确保多边形方向正确
        for (int i = 0; i < subject.Count; i++)
        {
            bool isPositive = Clipper.IsPositive(subject[i]);
            Console.WriteLine($"Subject[{i}] 是正方向: {isPositive}");
        }
    }
}

3.7.2 意外的孔洞

问题:并集运算后出现意外的孔洞。

原因:填充规则与多边形方向不匹配。

解决方法

using Clipper2Lib;

class UnexpectedHolesSolution
{
    static Paths64 UnionWithCorrectOrientation(Paths64 paths)
    {
        // 使用EvenOdd规则时,方向不重要
        Paths64 result1 = Clipper.Union(paths, FillRule.EvenOdd);

        // 或者使用NonZero规则并确保方向一致
        Paths64 correctedPaths = new Paths64();
        foreach (Path64 path in paths)
        {
            if (!Clipper.IsPositive(path))
            {
                correctedPaths.Add(Clipper.ReversePath(path));
            }
            else
            {
                correctedPaths.Add(path);
            }
        }
        Paths64 result2 = Clipper.Union(correctedPaths, FillRule.NonZero);
        
        return result2;
    }
}

3.7.3 自交多边形的处理

问题:自交多边形产生意外的结果。

解决方法

using Clipper2Lib;

class SelfIntersectionHandling
{
    static Paths64 SimplifySelfIntersecting(Paths64 selfIntersecting)
    {
        // 使用Union简化自交多边形
        Paths64 simplified = Clipper.Union(selfIntersecting, FillRule.NonZero);
        return simplified;
    }

3.7.4 精度损失

问题:浮点数转换导致精度损失。

解决方法

using Clipper2Lib;

class PrecisionHandling
{
    static void HandlePrecision()
    {
        // 使用足够大的缩放因子
        double scale = 1000000.0;  // 6位小数精度

        PathsD subjectD = new PathsD();
        subjectD.Add(Clipper.MakePath(new double[] { 0.0, 0.0, 10.123456, 0.0, 10.123456, 10.123456, 0.0, 10.123456 }));
        
        PathsD clipD = new PathsD();
        clipD.Add(Clipper.MakePath(new double[] { 5.0, 5.0, 15.0, 5.0, 15.0, 15.0, 5.0, 15.0 }));

        // 手动缩放
        Paths64 scaledSubject = Clipper.ScalePaths64(subjectD, scale);
        Paths64 scaledClip = Clipper.ScalePaths64(clipD, scale);

        Paths64 result = Clipper.Intersect(scaledSubject, scaledClip, FillRule.NonZero);

        // 缩放回原始尺寸
        PathsD resultD = Clipper.ScalePathsD(result, 1.0 / scale);
    }
}

3.8 实际应用示例

3.8.1 创建复合形状

using Clipper2Lib;

class CompositeShapeExample
{
    static Path64 MakeCircle(Point64 center, int radius, int segments = 32)
    {
        Path64 circle = new Path64();
        for (int i = 0; i < segments; i++)
        {
            double angle = 2 * Math.PI * i / segments;
            circle.Add(new Point64(
                center.X + (long)(radius * Math.Cos(angle)),
                center.Y + (long)(radius * Math.Sin(angle))
            ));
        }
        return circle;
    }
    
    static void Main()
    {
        // 创建圆形
        Paths64 circle = new Paths64();
        circle.Add(MakeCircle(new Point64(100, 100), 80));

        // 创建内部小圆
        Paths64 innerCircle = new Paths64();
        innerCircle.Add(MakeCircle(new Point64(100, 100), 40));

        Paths64 result = Clipper.Difference(circle, innerCircle, FillRule.NonZero);
        // result 是一个圆环
        Console.WriteLine($"圆环包含 {result.Count} 个路径");
    }
}

3.8.2 区域合并

using Clipper2Lib;

class RegionMergeExample
{
    static void Main()
    {
        // 合并多个重叠的区域
        Paths64 regions = new Paths64();
        regions.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));
        regions.Add(Clipper.MakePath(new long[] { 80, 0, 180, 0, 180, 100, 80, 100 }));
        regions.Add(Clipper.MakePath(new long[] { 160, 0, 260, 0, 260, 100, 160, 100 }));

        Paths64 merged = Clipper.Union(regions, FillRule.NonZero);
        // merged 是一个连续的区域
        Console.WriteLine($"合并后: {merged.Count} 个多边形");
    }
}

3.8.3 遮罩裁剪

using Clipper2Lib;

class MaskClipExample
{
    static void Main()
    {
        // 使用遮罩裁剪图像区域
        Paths64 image = new Paths64();
        image.Add(Clipper.MakePath(new long[] { 0, 0, 800, 0, 800, 600, 0, 600 }));

        Paths64 mask = new Paths64();
        mask.Add(Clipper.MakePath(new long[] { 100, 100, 700, 100, 700, 500, 100, 500 }));
        // 添加一个孔洞
        mask.Add(Clipper.MakePath(new long[] { 200, 200, 200, 400, 600, 400, 600, 200 }));

        Paths64 result = Clipper.Intersect(image, mask, FillRule.NonZero);
        // result 是带有中心孔洞的遮罩区域
        Console.WriteLine($"遮罩裁剪结果: {result.Count} 个路径");
    }
}

3.8.4 地块拆分

using Clipper2Lib;

class ParcelSplitExample
{
    static void Main()
    {
        // 用一条线将地块拆分为两部分
        Paths64 parcel = new Paths64();
        parcel.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }));

        // 创建一个非常薄的矩形作为分割线
        Paths64 splitLine = new Paths64();
        splitLine.Add(Clipper.MakePath(new long[] { 50, -10, 51, -10, 51, 110, 50, 110 }));

        // 左半部分裁剪区域
        Paths64 leftClip = new Paths64();
        leftClip.Add(Clipper.MakePath(new long[] { -10, -10, 50, -10, 50, 110, -10, 110 }));

        // 右半部分裁剪区域
        Paths64 rightClip = new Paths64();
        rightClip.Add(Clipper.MakePath(new long[] { 51, -10, 110, -10, 110, 110, 51, 110 }));

        Paths64 left = Clipper.Intersect(parcel, leftClip, FillRule.NonZero);
        Paths64 right = Clipper.Intersect(parcel, rightClip, FillRule.NonZero);
        
        Console.WriteLine($"左半部分: {left.Count} 个多边形");
        Console.WriteLine($"右半部分: {right.Count} 个多边形");
    }
}

3.8.5 碰撞区域计算

using Clipper2Lib;

class CollisionDetectionExample
{
    static Paths64 GetObjectBounds(Point64 position, int size)
    {
        Paths64 bounds = new Paths64();
        bounds.Add(Clipper.MakePath(new long[] { 
            position.X, position.Y, 
            position.X + size, position.Y, 
            position.X + size, position.Y + size, 
            position.X, position.Y + size 
        }));
        return bounds;
    }
    
    static void Main()
    {
        // 计算两个移动物体的碰撞区域
        Paths64 objectA = GetObjectBounds(new Point64(0, 0), 100);
        Paths64 objectB = GetObjectBounds(new Point64(50, 50), 100);

        Paths64 collision = Clipper.Intersect(objectA, objectB, FillRule.NonZero);
        if (collision.Count > 0)
        {
            // 发生碰撞
            double collisionArea = Clipper.Area(collision);
            Console.WriteLine($"碰撞面积: {collisionArea}");
            // 根据碰撞面积决定反应
        }
        else
        {
            Console.WriteLine("未发生碰撞");
        }
    }
}

3.9 与其他库的比较

3.9.1 与NetTopologySuite的比较

特性 Clipper2 NetTopologySuite
精度 整数运算,精确 浮点运算,可能有误差
性能 非常快 较快
鲁棒性 极佳 良好
拓扑处理 需要后处理 内置
曲线支持
.NET集成 NuGet包 NuGet包

3.9.2 与其他C#几何库的比较

特性 Clipper2 其他库
学习曲线 简单 各异
依赖 各异
精度 64位整数 各异
功能范围 2D多边形裁剪 综合几何功能
许可证 Boost 各异

3.10 算法原理简介

Clipper2使用改进的Vatti算法进行布尔运算。这里简要介绍其工作原理。

3.10.1 扫描线算法

Vatti算法是一种扫描线算法。它通过一条水平扫描线从下向上扫过所有的多边形,在扫描过程中维护当前扫描线与多边形边的交点。

          y
          ↑
          |     ┌────────┐
          |     │        │
扫描线 ───┼─────┼────────┼───→
          |     │   ∩    │
          |  ┌──┴────────┴──┐
          |  │              │
          └──┴──────────────┴────→ x

3.10.2 事件处理

算法在以下事件点进行处理:

3.10.3 活动边列表

算法维护一个”活动边列表”(Active Edge List),包含当前与扫描线相交的所有边。随着扫描线移动,边被添加或移除。

// 伪代码示意
while (eventQueue.Count > 0)
{
    Event evt = eventQueue.Dequeue();
    
    if (evt.Type == EventType.Vertex)
    {
        // 处理顶点事件
        UpdateActiveEdges(evt.Vertex);
    }
    else if (evt.Type == EventType.Intersection)
    {
        // 处理交点事件
        SwapEdges(evt.Edge1, evt.Edge2);
    }
    
    // 根据填充规则和裁剪类型决定输出
    DetermineOutput(activeEdges, clipType, fillRule);
}

3.11 本章小结

本章我们深入学习了Clipper2 C#版本的布尔运算功能:

  1. 基本概念:交集、并集、差集、异或四种运算类型
  2. 简化API:Clipper.Intersect、Clipper.Union、Clipper.Difference、Clipper.Xor静态方法
  3. Clipper类:Clipper64和ClipperD提供更多控制选项
  4. 复杂多边形处理:孔洞、自交、重叠边的处理
  5. 性能优化:路径简化、矩形裁剪、批量处理、多线程
  6. 常见问题:结果为空、意外孔洞、自交处理、精度损失
  7. 实际应用:复合形状、区域合并、遮罩裁剪、碰撞检测等

在下一章中,我们将学习Clipper2的另一个核心功能——多边形偏移操作。