第2章_核心数据结构

第二章 核心数据结构

2.1 引言

在上一章中,我们了解了 Clipper1 的基本概念和安装方法,并运行了第一个示例程序。在本章中,我们将深入学习 Clipper1 的核心数据结构。理解这些数据结构是掌握 Clipper1 的关键,因为所有的几何运算都是基于这些数据结构进行的。

Clipper1 的数据结构设计简洁而高效,主要包括:

  • IntPoint:表示二维空间中的点
  • Path:表示一个开放或封闭的路径(多边形轮廓)
  • Paths:表示多个路径的集合
  • PolyTree 和 PolyNode:表示具有层次关系的多边形结构
  • 相关枚举类型:定义各种操作选项

本章将详细讲解每种数据结构的定义、用法、特点以及最佳实践,并通过大量示例帮助您深入理解。

2.2 IntPoint 结构

2.2.1 IntPoint 的定义

IntPoint 是 Clipper1 中最基础的数据结构,用于表示二维平面上的一个点。它的定义非常简单:

namespace ClipperLib
{
    public struct IntPoint
    {
        public long X;
        public long Y;

        public IntPoint(long x, long y)
        {
            X = x;
            Y = y;
        }

        public IntPoint(double x, double y)
        {
            X = (long)x;
            Y = (long)y;
        }

        public IntPoint(IntPoint pt)
        {
            X = pt.X;
            Y = pt.Y;
        }
    }
}

2.2.2 IntPoint 的特点

使用长整型(long)

IntPoint 使用 64 位有符号整数(long 类型)来存储坐标值,这带来了几个重要优势:

  1. 精确性:整数运算是完全精确的,没有浮点数的舍入误差
  2. 一致性:在所有平台上的行为完全一致
  3. 范围:long 类型的范围是 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807,足以应对大多数应用场景
  4. 性能:整数运算通常比浮点运算更快

值类型(struct)

IntPoint 被定义为值类型(struct),这意味着:

  • 在栈上分配,性能更好
  • 赋值时进行复制,而不是引用
  • 不能为 null

2.2.3 创建 IntPoint

有多种方式可以创建 IntPoint 对象:

// 方法1:使用构造函数(long 参数)
IntPoint p1 = new IntPoint(100, 200);

// 方法2:使用构造函数(double 参数,会自动转换)
IntPoint p2 = new IntPoint(100.5, 200.7);  // 转换为 (100, 200)

// 方法3:直接赋值
IntPoint p3;
p3.X = 300;
p3.Y = 400;

// 方法4:从另一个 IntPoint 复制
IntPoint p4 = new IntPoint(p1);

2.2.4 坐标缩放策略

由于 IntPoint 使用整数坐标,而实际应用中经常需要处理浮点数坐标,因此需要进行坐标缩放。

选择合适的缩放系数

缩放系数的选择取决于您的精度需求:

// 精度到整数单位(如整数像素、整数米)
const double SCALE = 1.0;

// 精度到0.1单位(一位小数)
const double SCALE = 10.0;

// 精度到0.01单位(两位小数)
const double SCALE = 100.0;

// 精度到0.001单位(三位小数)
const double SCALE = 1000.0;

// 常用:精度到微米级别
const double SCALE = 1000000.0;

实用的转换辅助类

public static class PointConverter
{
    // 根据应用需求设置缩放系数
    private const double SCALE_FACTOR = 1000000.0;

    /// <summary>
    /// 将浮点数坐标转换为 IntPoint
    /// </summary>
    public static IntPoint FromDouble(double x, double y)
    {
        return new IntPoint(
            (long)Math.Round(x * SCALE_FACTOR),
            (long)Math.Round(y * SCALE_FACTOR)
        );
    }

    /// <summary>
    /// 将 IntPoint 转换为浮点数坐标
    /// </summary>
    public static (double x, double y) ToDouble(IntPoint pt)
    {
        return (
            pt.X / SCALE_FACTOR,
            pt.Y / SCALE_FACTOR
        );
    }

    /// <summary>
    /// 批量转换:浮点数数组到 IntPoint 列表
    /// </summary>
    public static List<IntPoint> FromDoubleArray(double[][] points)
    {
        var result = new List<IntPoint>();
        foreach (var point in points)
        {
            if (point.Length >= 2)
            {
                result.Add(FromDouble(point[0], point[1]));
            }
        }
        return result;
    }

    /// <summary>
    /// 批量转换:IntPoint 列表到浮点数数组
    /// </summary>
    public static double[][] ToDoubleArray(List<IntPoint> points)
    {
        var result = new double[points.Count][];
        for (int i = 0; i < points.Count; i++)
        {
            var (x, y) = ToDouble(points[i]);
            result[i] = new double[] { x, y };
        }
        return result;
    }
}

使用示例:

// 从浮点数创建点
double x = 123.456789;
double y = 987.654321;
IntPoint pt = PointConverter.FromDouble(x, y);

// 转换回浮点数
var (resultX, resultY) = PointConverter.ToDouble(pt);
Console.WriteLine($"原始: ({x}, {y})");
Console.WriteLine($"转换后: ({resultX}, {resultY})");
// 输出会显示保留了6位小数的精度

2.2.5 IntPoint 的操作

虽然 IntPoint 结构本身只提供基本的坐标存储,但我们可以扩展它的功能:

public static class IntPointExtensions
{
    /// <summary>
    /// 计算两点之间的距离平方(避免开方运算)
    /// </summary>
    public static long DistanceSquared(this IntPoint p1, IntPoint p2)
    {
        long dx = p2.X - p1.X;
        long dy = p2.Y - p1.Y;
        return dx * dx + dy * dy;
    }

    /// <summary>
    /// 计算两点之间的实际距离
    /// </summary>
    public static double Distance(this IntPoint p1, IntPoint p2)
    {
        return Math.Sqrt(DistanceSquared(p1, p2));
    }

    /// <summary>
    /// 判断两点是否相等
    /// </summary>
    public static bool Equals(this IntPoint p1, IntPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y;
    }

    /// <summary>
    /// 计算向量的叉积(用于判断点的相对位置)
    /// </summary>
    public static long CrossProduct(IntPoint o, IntPoint a, IntPoint b)
    {
        return (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X);
    }

    /// <summary>
    /// 判断三点的转向(正:逆时针,负:顺时针,零:共线)
    /// </summary>
    public static int Orientation(IntPoint p1, IntPoint p2, IntPoint p3)
    {
        long cross = CrossProduct(p1, p2, p3);
        if (cross > 0) return 1;   // 逆时针
        if (cross < 0) return -1;  // 顺时针
        return 0;                  // 共线
    }
}

使用示例:

IntPoint p1 = new IntPoint(0, 0);
IntPoint p2 = new IntPoint(100, 0);
IntPoint p3 = new IntPoint(100, 100);

// 计算距离
double dist = p1.Distance(p2);
Console.WriteLine($"距离: {dist}");

// 判断转向
int orient = IntPointExtensions.Orientation(p1, p2, p3);
Console.WriteLine($"转向: {(orient > 0 ? "逆时针" : orient < 0 ? "顺时针" : "共线")}");

2.3 Path 类型

2.3.1 Path 的定义

在 Clipper1 中,Path 是一个类型别名,定义为:

using Path = List<IntPoint>;

Path 表示一系列有序的点,可以用来表示:

  • 开放路径:如线段、折线(起点和终点不连接)
  • 封闭路径:如多边形轮廓(起点和终点隐式连接)

2.3.2 创建 Path

有多种方式可以创建和初始化 Path

using Path = List<IntPoint>;

// 方法1:使用集合初始化器
Path square = new Path
{
    new IntPoint(0, 0),
    new IntPoint(100, 0),
    new IntPoint(100, 100),
    new IntPoint(0, 100)
};

// 方法2:逐个添加点
Path triangle = new Path();
triangle.Add(new IntPoint(0, 0));
triangle.Add(new IntPoint(50, 100));
triangle.Add(new IntPoint(100, 0));

// 方法3:从数组创建
IntPoint[] points = {
    new IntPoint(0, 0),
    new IntPoint(10, 0),
    new IntPoint(10, 10),
    new IntPoint(0, 10)
};
Path polygon = new Path(points);

// 方法4:从其他集合创建
var pointsList = new List<(int x, int y)> { (0, 0), (10, 0), (10, 10) };
Path path = new Path(pointsList.Select(p => new IntPoint(p.x, p.y)));

2.3.3 Path 的方向性

多边形的方向性在 Clipper1 中非常重要,它决定了多边形的"内"和"外"。

判断 Path 的方向

Clipper1 提供了 Orientation() 方法来判断路径的方向:

Path polygon = new Path
{
    new IntPoint(0, 0),
    new IntPoint(100, 0),
    new IntPoint(100, 100),
    new IntPoint(0, 100)
};

bool isClockwise = !Clipper.Orientation(polygon);
Console.WriteLine($"路径方向: {(isClockwise ? "顺时针" : "逆时针")}");

注意Orientation() 返回 true 表示逆时针,false 表示顺时针。

方向性规则

在 Clipper1 中:

  • 外轮廓通常使用逆时针方向(正方向)
  • 孔洞使用顺时针方向(负方向)
  • 执行布尔运算后,Clipper 会自动调整方向以符合这个约定

反转 Path 的方向

Path polygon = new Path
{
    new IntPoint(0, 0),
    new IntPoint(100, 0),
    new IntPoint(100, 100),
    new IntPoint(0, 100)
};

// 方法1:使用 Clipper 的方法
Clipper.ReversePath(polygon);

// 方法2:使用 List 的 Reverse 方法
polygon.Reverse();

// 两种方法效果相同

2.3.4 Path 的面积计算

Clipper1 提供了 Area() 方法来计算 Path 的面积:

Path square = new Path
{
    new IntPoint(0, 0),
    new IntPoint(1000, 0),
    new IntPoint(1000, 1000),
    new IntPoint(0, 1000)
};

double area = Clipper.Area(square);
Console.WriteLine($"面积: {area}");  // 输出: 1000000

// 注意:面积带有符号
// 逆时针方向:正面积
// 顺时针方向:负面积
double absArea = Math.Abs(area);

面积计算的注意事项

  1. 带符号的面积:面积值可能为正或负,取决于路径方向
  2. 缩放系数:记得根据您的缩放系数调整结果
  3. 自相交多边形:对于自相交的多边形,面积计算遵循数学定义
// 实际应用中的面积计算
const double SCALE = 1000.0;  // 如果坐标已经乘以1000

Path realWorldPolygon = new Path
{
    new IntPoint((long)(0 * SCALE), (long)(0 * SCALE)),
    new IntPoint((long)(10.5 * SCALE), (long)(0 * SCALE)),
    new IntPoint((long)(10.5 * SCALE), (long)(8.3 * SCALE)),
    new IntPoint((long)(0 * SCALE), (long)(8.3 * SCALE))
};

double scaledArea = Clipper.Area(realWorldPolygon);
double realArea = Math.Abs(scaledArea) / (SCALE * SCALE);
Console.WriteLine($"实际面积: {realArea} 平方单位");  // 输出: 87.15

2.3.5 Path 的简化

有时从外部输入或生成的路径可能包含冗余点或自相交,Clipper1 提供了简化功能:

// 原始路径(可能包含自相交)
Path complexPath = new Path
{
    new IntPoint(0, 0),
    new IntPoint(100, 0),
    new IntPoint(100, 100),
    new IntPoint(50, 50),   // 导致自相交
    new IntPoint(0, 100)
};

// 简化路径,移除自相交
Paths simplified = Clipper.SimplifyPolygon(
    complexPath,
    PolyFillType.pftNonZero  // 填充规则
);

// SimplifyPolygon 返回 Paths,因为简化后可能产生多个多边形
Console.WriteLine($"简化后产生 {simplified.Count} 个多边形");

2.3.6 Path 的实用操作

public static class PathHelper
{
    /// <summary>
    /// 计算路径的周长
    /// </summary>
    public static double Perimeter(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;
            perimeter += path[i].Distance(path[next]);
        }
        return perimeter;
    }

    /// <summary>
    /// 计算路径的边界框
    /// </summary>
    public static IntRect GetBounds(Path path)
    {
        if (path.Count == 0)
            return new IntRect(0, 0, 0, 0);

        long minX = path[0].X, maxX = path[0].X;
        long minY = path[0].Y, maxY = path[0].Y;

        foreach (var pt in path)
        {
            if (pt.X < minX) minX = pt.X;
            if (pt.X > maxX) maxX = pt.X;
            if (pt.Y < minY) minY = pt.Y;
            if (pt.Y > maxY) maxY = pt.Y;
        }

        return new IntRect(minX, minY, maxX, maxY);
    }

    /// <summary>
    /// 判断点是否在路径内部
    /// </summary>
    public static int PointInPolygon(IntPoint pt, Path path)
    {
        return Clipper.PointInPolygon(pt, path);
        // 返回值:0 = 外部,1 = 内部,-1 = 在边上
    }

    /// <summary>
    /// 移除共线的点
    /// </summary>
    public static Path RemoveCollinearPoints(Path path, double tolerance = 0)
    {
        Path result = new Path(path);
        Clipper.CleanPolygon(result, tolerance);
        return result;
    }

    /// <summary>
    /// 计算路径的质心(中心点)
    /// </summary>
    public static IntPoint Centroid(Path path)
    {
        if (path.Count == 0)
            return new IntPoint(0, 0);

        long sumX = 0, sumY = 0;
        foreach (var pt in path)
        {
            sumX += pt.X;
            sumY += pt.Y;
        }

        return new IntPoint(
            sumX / path.Count,
            sumY / path.Count
        );
    }
}

使用示例:

Path polygon = new Path
{
    new IntPoint(0, 0),
    new IntPoint(100, 0),
    new IntPoint(100, 100),
    new IntPoint(0, 100)
};

// 计算周长
double perimeter = PathHelper.Perimeter(polygon);
Console.WriteLine($"周长: {perimeter}");

// 获取边界框
IntRect bounds = PathHelper.GetBounds(polygon);
Console.WriteLine($"边界框: ({bounds.left}, {bounds.top}) - ({bounds.right}, {bounds.bottom})");

// 判断点是否在内部
IntPoint testPoint = new IntPoint(50, 50);
int position = PathHelper.PointInPolygon(testPoint, polygon);
Console.WriteLine($"点位置: {(position == 1 ? "内部" : position == -1 ? "边上" : "外部")}");

// 计算质心
IntPoint center = PathHelper.Centroid(polygon);
Console.WriteLine($"质心: ({center.X}, {center.Y})");

2.4 Paths 类型

2.4.1 Paths 的定义

Paths 是多个 Path 的集合:

using Paths = List<List<IntPoint>>;

Paths 用于表示:

  • 多个独立的多边形
  • 一个带有孔洞的复杂多边形(外轮廓 + 多个孔洞)
  • 布尔运算的输入和输出

2.4.2 创建 Paths

using Paths = List<List<IntPoint>>;
using Path = List<IntPoint>;

// 方法1:创建多个独立多边形
Paths multiplePolygons = new Paths();

Path square1 = new Path
{
    new IntPoint(0, 0),
    new IntPoint(100, 0),
    new IntPoint(100, 100),
    new IntPoint(0, 100)
};

Path square2 = new Path
{
    new IntPoint(200, 200),
    new IntPoint(300, 200),
    new IntPoint(300, 300),
    new IntPoint(200, 300)
};

multiplePolygons.Add(square1);
multiplePolygons.Add(square2);

// 方法2:使用集合初始化器
Paths paths = new Paths
{
    new Path
    {
        new IntPoint(0, 0),
        new IntPoint(100, 0),
        new IntPoint(100, 100),
        new IntPoint(0, 100)
    },
    new Path
    {
        new IntPoint(200, 0),
        new IntPoint(300, 0),
        new IntPoint(300, 100),
        new IntPoint(200, 100)
    }
};

2.4.3 带孔洞的多边形

使用 Paths 表示带孔洞的多边形时,需要注意方向性:

// 创建一个带孔洞的正方形
Paths polygonWithHole = new Paths();

// 外轮廓(逆时针)
Path outer = new Path
{
    new IntPoint(0, 0),
    new IntPoint(100, 0),
    new IntPoint(100, 100),
    new IntPoint(0, 100)
};

// 内孔洞(顺时针,方向相反)
Path hole = new Path
{
    new IntPoint(25, 25),
    new IntPoint(25, 75),
    new IntPoint(75, 75),
    new IntPoint(75, 25)
};

polygonWithHole.Add(outer);
polygonWithHole.Add(hole);

// 计算面积(会自动扣除孔洞)
double totalArea = 0;
foreach (var path in polygonWithHole)
{
    totalArea += Clipper.Area(path);
}
Console.WriteLine($"净面积: {Math.Abs(totalArea)}");
// 输出: 7500 (10000 - 2500)

2.4.4 Paths 的操作

public static class PathsHelper
{
    /// <summary>
    /// 计算所有路径的总面积
    /// </summary>
    public static double TotalArea(Paths paths)
    {
        double total = 0;
        foreach (var path in paths)
        {
            total += Math.Abs(Clipper.Area(path));
        }
        return total;
    }

    /// <summary>
    /// 获取所有路径的总边界框
    /// </summary>
    public static IntRect GetBounds(Paths paths)
    {
        if (paths.Count == 0 || paths[0].Count == 0)
            return new IntRect(0, 0, 0, 0);

        long minX = long.MaxValue, maxX = long.MinValue;
        long minY = long.MaxValue, maxY = long.MinValue;

        foreach (var path in paths)
        {
            foreach (var pt in path)
            {
                if (pt.X < minX) minX = pt.X;
                if (pt.X > maxX) maxX = pt.X;
                if (pt.Y < minY) minY = pt.Y;
                if (pt.Y > maxY) maxY = pt.Y;
            }
        }

        return new IntRect(minX, minY, maxX, maxY);
    }

    /// <summary>
    /// 统计路径信息
    /// </summary>
    public static void PrintStatistics(Paths paths)
    {
        Console.WriteLine($"路径数量: {paths.Count}");
        int totalPoints = 0;
        for (int i = 0; i < paths.Count; i++)
        {
            int points = paths[i].Count;
            totalPoints += points;
            Console.WriteLine($"  路径 {i + 1}: {points} 个点");
        }
        Console.WriteLine($"总顶点数: {totalPoints}");
        Console.WriteLine($"总面积: {TotalArea(paths)}");
    }

    /// <summary>
    /// 清理所有路径
    /// </summary>
    public static Paths CleanPaths(Paths paths, double distance = 1.415)
    {
        Paths result = new Paths();
        foreach (var path in paths)
        {
            Path cleaned = new Path(path);
            Clipper.CleanPolygon(cleaned, distance);
            if (cleaned.Count >= 3)  // 至少需要3个点才能形成多边形
            {
                result.Add(cleaned);
            }
        }
        return result;
    }

    /// <summary>
    /// 确保所有外轮廓为逆时针,孔洞为顺时针
    /// </summary>
    public static void NormalizePaths(Paths paths)
    {
        foreach (var path in paths)
        {
            double area = Clipper.Area(path);
            bool isOuter = area > 0;  // 假设正面积为外轮廓
            bool isCounterClockwise = Clipper.Orientation(path);

            if (isOuter && !isCounterClockwise)
            {
                path.Reverse();
            }
            else if (!isOuter && isCounterClockwise)
            {
                path.Reverse();
            }
        }
    }
}

2.5 PolyTree 和 PolyNode

2.5.1 为什么需要 PolyTree

在某些情况下,简单的 Paths 类型不足以表达复杂的多边形层次关系:

  1. 多层嵌套:一个孔洞内还有岛屿,岛屿内又有孔洞
  2. 父子关系:需要明确知道哪个孔洞属于哪个外轮廓
  3. 开放路径:区分开放路径和封闭路径

PolyTree 是一个树形结构,能够完整表达这些复杂关系。

2.5.2 PolyNode 结构

PolyNodePolyTree 的节点:

public class PolyNode
{
    public PolyNode Parent { get; }           // 父节点
    public Path Contour { get; }              // 轮廓路径
    public List<PolyNode> Childs { get; }     // 子节点列表
    public bool IsHole { get; }               // 是否为孔洞
    public bool IsOpen { get; }               // 是否为开放路径
    public int ChildCount { get; }            // 子节点数量
}

层次关系

  • 偶数层(0, 2, 4...):外轮廓(IsHole = false)
  • 奇数层(1, 3, 5...):孔洞(IsHole = true)

2.5.3 使用 PolyTree

// 创建复杂的多边形结构
Paths subjects = new Paths
{
    // 大的外轮廓
    new Path
    {
        new IntPoint(0, 0),
        new IntPoint(200, 0),
        new IntPoint(200, 200),
        new IntPoint(0, 200)
    },
    // 孔洞
    new Path
    {
        new IntPoint(50, 50),
        new IntPoint(50, 150),
        new IntPoint(150, 150),
        new IntPoint(150, 50)
    },
    // 孔洞内的岛屿
    new Path
    {
        new IntPoint(75, 75),
        new IntPoint(125, 75),
        new IntPoint(125, 125),
        new IntPoint(75, 125)
    }
};

// 使用 PolyTree 接收结果
Clipper clipper = new Clipper();
clipper.AddPaths(subjects, PolyType.ptSubject, true);

PolyTree solution = new PolyTree();
clipper.Execute(ClipType.ctUnion, solution);

// 遍历 PolyTree
PrintPolyTree(solution, 0);

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

    foreach (var child in node.Childs)
    {
        Console.WriteLine($"{indent}节点: {(child.IsHole ? "孔洞" : "外轮廓")}");
        Console.WriteLine($"{indent}  顶点数: {child.Contour.Count}");
        Console.WriteLine($"{indent}  子节点数: {child.ChildCount}");
        Console.WriteLine($"{indent}  面积: {Math.Abs(Clipper.Area(child.Contour))}");

        // 递归遍历子节点
        if (child.Childs.Count > 0)
        {
            PrintPolyTree(child, level + 1);
        }
    }
}

2.5.4 PolyTree 转换为 Paths

有时需要将 PolyTree 转换回 Paths

public static class PolyTreeHelper
{
    /// <summary>
    /// 从 PolyTree 提取所有封闭路径
    /// </summary>
    public static Paths ClosedPathsFromPolyTree(PolyTree polytree)
    {
        Paths result = new Paths();
        AddPolyNodeToPaths(polytree, result, false);
        return result;
    }

    /// <summary>
    /// 从 PolyTree 提取所有开放路径
    /// </summary>
    public static Paths OpenPathsFromPolyTree(PolyTree polytree)
    {
        Paths result = new Paths();
        foreach (var child in polytree.Childs)
        {
            if (child.IsOpen)
            {
                result.Add(child.Contour);
            }
        }
        return result;
    }

    /// <summary>
    /// 递归添加节点到路径集合
    /// </summary>
    private static void AddPolyNodeToPaths(PolyNode node, Paths paths, bool includeOpen)
    {
        bool match = true;
        if (!includeOpen)
        {
            match = !node.IsOpen;
        }

        if (match && node.Contour.Count > 0)
        {
            paths.Add(node.Contour);
        }

        foreach (var child in node.Childs)
        {
            AddPolyNodeToPaths(child, paths, includeOpen);
        }
    }

    /// <summary>
    /// 按层次分组(外轮廓和其孔洞分组)
    /// </summary>
    public static List<(Path outer, List<Path> holes)> GroupByOuterAndHoles(PolyTree polytree)
    {
        var result = new List<(Path outer, List<Path> holes)>();

        foreach (var outerNode in polytree.Childs)
        {
            if (!outerNode.IsHole)
            {
                var holes = new List<Path>();
                foreach (var holeNode in outerNode.Childs)
                {
                    if (holeNode.IsHole)
                    {
                        holes.Add(holeNode.Contour);
                    }
                }
                result.Add((outerNode.Contour, holes));
            }
        }

        return result;
    }
}

使用示例:

PolyTree polytree = new PolyTree();
clipper.Execute(ClipType.ctUnion, polytree);

// 提取所有封闭路径
Paths closedPaths = PolyTreeHelper.ClosedPathsFromPolyTree(polytree);
Console.WriteLine($"封闭路径数量: {closedPaths.Count}");

// 分组为外轮廓和孔洞
var groups = PolyTreeHelper.GroupByOuterAndHoles(polytree);
foreach (var group in groups)
{
    Console.WriteLine($"外轮廓顶点数: {group.outer.Count}");
    Console.WriteLine($"孔洞数量: {group.holes.Count}");
}

2.6 枚举类型

2.6.1 ClipType 枚举

定义布尔运算的类型:

public enum ClipType
{
    ctIntersection,  // 交集
    ctUnion,         // 并集
    ctDifference,    // 差集
    ctXor            // 异或
}

使用示例

// 交集:两个多边形的重叠部分
clipper.Execute(ClipType.ctIntersection, solution);

// 并集:合并两个多边形
clipper.Execute(ClipType.ctUnion, solution);

// 差集:从subject中减去clip
clipper.Execute(ClipType.ctDifference, solution);

// 异或:两个多边形不重叠的部分
clipper.Execute(ClipType.ctXor, solution);

2.6.2 PolyType 枚举

指定添加路径时的角色:

public enum PolyType
{
    ptSubject,  // 主题多边形
    ptClip      // 裁剪多边形
}

使用规则

  • 在执行布尔运算时,需要区分哪些是主题多边形,哪些是裁剪多边形
  • 对于并集和异或运算,两者的区别不明显
  • 对于交集和差集,区分很重要
Clipper clipper = new Clipper();

// 添加主题多边形
clipper.AddPath(polygon1, PolyType.ptSubject, true);

// 添加裁剪多边形
clipper.AddPath(polygon2, PolyType.ptClip, true);

// 执行差集:polygon1 - polygon2
Paths difference = new Paths();
clipper.Execute(ClipType.ctDifference, difference);

2.6.3 PolyFillType 枚举

定义填充规则,影响如何判断一个区域是"内部"还是"外部":

public enum PolyFillType
{
    pftEvenOdd,   // 奇偶规则
    pftNonZero,   // 非零规则
    pftPositive,  // 正数规则
    pftNegative   // 负数规则
}

奇偶规则(EvenOdd)

  • 从任意点向外发射射线
  • 计算与多边形边的交点数
  • 奇数次交点:内部,偶数次:外部

非零规则(NonZero)

  • 考虑多边形的方向
  • 根据环绕数判断内外
  • 这是最常用的规则

使用示例

Clipper clipper = new Clipper();
clipper.AddPaths(subjects, PolyType.ptSubject, true);
clipper.AddPaths(clips, PolyType.ptClip, true);

Paths solution = new Paths();

// 使用非零规则(推荐)
clipper.Execute(
    ClipType.ctUnion,
    solution,
    PolyFillType.pftNonZero,
    PolyFillType.pftNonZero
);

2.6.4 JoinType 枚举

用于偏移操作,定义边的连接方式:

public enum JoinType
{
    jtSquare,  // 方形连接
    jtRound,   // 圆形连接
    jtMiter    // 斜接连接
}

应用场景

  • jtRound:产生平滑的圆角,适合需要美观效果的场景
  • jtSquare:产生直角,计算快速
  • jtMiter:产生尖角,适合建筑图纸等需要精确角度的场景

2.6.5 EndType 枚举

用于偏移操作,定义路径端点的处理方式:

public enum EndType
{
    etClosedPolygon,  // 封闭多边形
    etClosedLine,     // 封闭线(作为线处理,但首尾相连)
    etOpenSquare,     // 开放路径,方形端点
    etOpenRound,      // 开放路径,圆形端点
    etOpenButt        // 开放路径,平切端点
}

选择指南

  • 多边形:使用 etClosedPolygon
  • 封闭的线条:使用 etClosedLine
  • 开放的线段:根据需要选择 etOpenSquareetOpenRoundetOpenButt

2.7 IntRect 结构

IntRect 用于表示矩形区域:

public struct IntRect
{
    public long left;
    public long top;
    public long right;
    public long bottom;

    public IntRect(long l, long t, long r, long b)
    {
        left = l;
        top = t;
        right = r;
        bottom = b;
    }
}

使用示例

// 获取路径的边界框
Path polygon = new Path
{
    new IntPoint(10, 20),
    new IntPoint(100, 20),
    new IntPoint(100, 150),
    new IntPoint(10, 150)
};

IntRect bounds = Clipper.GetBounds(new Paths { polygon });
Console.WriteLine($"左: {bounds.left}, 上: {bounds.top}");
Console.WriteLine($"右: {bounds.right}, 下: {bounds.bottom}");
Console.WriteLine($"宽度: {bounds.right - bounds.left}");
Console.WriteLine($"高度: {bounds.bottom - bounds.top}");

2.8 实战示例:复杂多边形操作

让我们通过一个综合示例来运用本章学到的所有数据结构:

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>>;

namespace ClipperDataStructuresDemo
{
    class Program
    {
        // 缩放系数
        const double SCALE = 1000.0;

        static void Main(string[] args)
        {
            Console.WriteLine("=== Clipper1 数据结构综合示例 ===\n");

            // 1. 创建一个复杂的区域:大矩形包含两个孔洞
            Path outerRect = CreateScaledRectangle(0, 0, 100, 100);
            Path hole1 = CreateScaledRectangle(10, 10, 30, 30);
            Path hole2 = CreateScaledRectangle(60, 60, 90, 90);

            // 确保方向正确
            Clipper.ReversePath(hole1);  // 孔洞需要反向
            Clipper.ReversePath(hole2);

            Paths complexRegion = new Paths { outerRect, hole1, hole2 };

            Console.WriteLine("原始复杂区域:");
            PrintPathsInfo(complexRegion);

            // 2. 创建一个相交的圆形(用多边形近似)
            Path circle = CreateCircle(70, 70, 25, 32);
            Paths circleRegion = new Paths { circle };

            Console.WriteLine("\n圆形区域:");
            PrintPathsInfo(circleRegion);

            // 3. 执行布尔运算
            Clipper clipper = new Clipper();

            // 3.1 交集
            clipper.AddPaths(complexRegion, PolyType.ptSubject, true);
            clipper.AddPaths(circleRegion, PolyType.ptClip, true);

            PolyTree intersectionTree = new PolyTree();
            clipper.Execute(ClipType.ctIntersection, intersectionTree,
                PolyFillType.pftNonZero, PolyFillType.pftNonZero);

            Console.WriteLine("\n=== 交集运算结果(使用PolyTree)===");
            PrintPolyTree(intersectionTree, 0);

            // 3.2 并集
            clipper.Clear();
            clipper.AddPaths(complexRegion, PolyType.ptSubject, true);
            clipper.AddPaths(circleRegion, PolyType.ptClip, true);

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

            Console.WriteLine("\n=== 并集运算结果 ===");
            PrintPathsInfo(unionResult);

            // 4. 偏移操作
            Console.WriteLine("\n=== 偏移操作 ===");
            ClipperOffset offset = new ClipperOffset();
            offset.AddPaths(unionResult, JoinType.jtRound, EndType.etClosedPolygon);

            Paths offsetResult = new Paths();
            offset.Execute(ref offsetResult, 5 * SCALE);  // 向外偏移5个单位

            Console.WriteLine("偏移后区域:");
            PrintPathsInfo(offsetResult);

            // 5. 边界框计算
            IntRect bounds = Clipper.GetBounds(offsetResult);
            Console.WriteLine($"\n边界框:");
            Console.WriteLine($"  左下: ({bounds.left / SCALE}, {bounds.bottom / SCALE})");
            Console.WriteLine($"  右上: ({bounds.right / SCALE}, {bounds.top / SCALE})");

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

        // 创建缩放后的矩形
        static Path CreateScaledRectangle(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 centerX, double centerY, double radius, int segments)
        {
            Path circle = new Path();
            for (int i = 0; i < segments; i++)
            {
                double angle = 2 * Math.PI * i / segments;
                double x = centerX + radius * Math.Cos(angle);
                double y = centerY + radius * Math.Sin(angle);
                circle.Add(new IntPoint((long)(x * SCALE), (long)(y * SCALE)));
            }
            return circle;
        }

        // 打印路径信息
        static void PrintPathsInfo(Paths paths)
        {
            Console.WriteLine($"  路径数量: {paths.Count}");
            double totalArea = 0;
            for (int i = 0; i < paths.Count; i++)
            {
                double area = Clipper.Area(paths[i]) / (SCALE * SCALE);
                totalArea += Math.Abs(area);
                Console.WriteLine($"  路径 {i + 1}: {paths[i].Count} 个顶点, " +
                    $"面积 = {Math.Abs(area):F2}, " +
                    $"方向 = {(area > 0 ? "逆时针" : "顺时针")}");
            }
            Console.WriteLine($"  总面积: {totalArea:F2}");
        }

        // 打印 PolyTree 结构
        static void PrintPolyTree(PolyNode node, int level)
        {
            string indent = new string(' ', level * 2);

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

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

2.9 最佳实践和注意事项

2.9.1 坐标缩放

  1. 选择合适的缩放系数

    • 根据精度需求选择(通常 10^6 左右)
    • 避免过大导致溢出
    • 避免过小损失精度
  2. 一致性

    • 在整个应用中使用相同的缩放系数
    • 创建统一的转换工具类

2.9.2 方向性管理

  1. 外轮廓逆时针,孔洞顺时针
  2. 使用 Orientation() 检查方向
  3. 必要时使用 ReversePath() 调整

2.9.3 性能优化

  1. 复用对象

    Clipper clipper = new Clipper();
    for (...) {
        clipper.Clear();  // 而不是每次创建新对象
        // 使用 clipper
    }
    
  2. 预分配容量

    Path path = new Path(expectedSize);
    
  3. 选择合适的数据结构

    • 简单场景使用 Paths
    • 需要层次关系时使用 PolyTree

2.9.4 数据验证

  1. 检查路径有效性

    if (path.Count < 3)
    {
        // 至少需要3个点才能构成多边形
        throw new ArgumentException("路径顶点数不足");
    }
    
  2. 清理冗余点

    Clipper.CleanPolygon(path);
    
  3. 简化复杂路径

    Paths simplified = Clipper.SimplifyPolygons(paths);
    

2.10 本章小结

在本章中,我们深入学习了 Clipper1 的核心数据结构:

  1. IntPoint:基于长整型的精确坐标表示
  2. Path:表示多边形轮廓或路径
  3. Paths:多个路径的集合
  4. PolyTree/PolyNode:层次化的多边形结构
  5. 枚举类型:各种操作选项

重点掌握:

  • 坐标缩放的原理和实践
  • 多边形方向性的重要性
  • Path 和 Paths 的使用场景
  • PolyTree 在处理复杂层次时的优势
  • 各种枚举类型的选择

在下一章中,我们将学习如何使用这些数据结构执行布尔运算,包括交集、并集、差集和异或等操作。

2.11 练习题

  1. 坐标转换练习:实现一个完整的坐标转换工具类,支持多种缩放系数

  2. 路径操作:创建一个五角星路径,计算其面积和周长

  3. 方向判断:编写程序判断给定路径的方向,并根据需要自动调整

  4. 复杂多边形:创建一个带有多个孔洞的复杂多边形,使用 PolyTree 表示其结构

  5. 边界框应用:编写函数判断两个多边形的边界框是否相交

  6. 数据清理:实现一个函数,移除路径中的重复点和共线点

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