第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 类型)来存储坐标值,这带来了几个重要优势:
- 精确性:整数运算是完全精确的,没有浮点数的舍入误差
- 一致性:在所有平台上的行为完全一致
- 范围:long 类型的范围是 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807,足以应对大多数应用场景
- 性能:整数运算通常比浮点运算更快
值类型(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);
面积计算的注意事项
- 带符号的面积:面积值可能为正或负,取决于路径方向
- 缩放系数:记得根据您的缩放系数调整结果
- 自相交多边形:对于自相交的多边形,面积计算遵循数学定义
// 实际应用中的面积计算
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 类型不足以表达复杂的多边形层次关系:
- 多层嵌套:一个孔洞内还有岛屿,岛屿内又有孔洞
- 父子关系:需要明确知道哪个孔洞属于哪个外轮廓
- 开放路径:区分开放路径和封闭路径
PolyTree 是一个树形结构,能够完整表达这些复杂关系。
2.5.2 PolyNode 结构
PolyNode 是 PolyTree 的节点:
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 - 开放的线段:根据需要选择
etOpenSquare、etOpenRound或etOpenButt
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 坐标缩放
-
选择合适的缩放系数
- 根据精度需求选择(通常 10^6 左右)
- 避免过大导致溢出
- 避免过小损失精度
-
一致性
- 在整个应用中使用相同的缩放系数
- 创建统一的转换工具类
2.9.2 方向性管理
- 外轮廓逆时针,孔洞顺时针
- 使用 Orientation() 检查方向
- 必要时使用 ReversePath() 调整
2.9.3 性能优化
-
复用对象
Clipper clipper = new Clipper(); for (...) { clipper.Clear(); // 而不是每次创建新对象 // 使用 clipper } -
预分配容量
Path path = new Path(expectedSize); -
选择合适的数据结构
- 简单场景使用 Paths
- 需要层次关系时使用 PolyTree
2.9.4 数据验证
-
检查路径有效性
if (path.Count < 3) { // 至少需要3个点才能构成多边形 throw new ArgumentException("路径顶点数不足"); } -
清理冗余点
Clipper.CleanPolygon(path); -
简化复杂路径
Paths simplified = Clipper.SimplifyPolygons(paths);
2.10 本章小结
在本章中,我们深入学习了 Clipper1 的核心数据结构:
- IntPoint:基于长整型的精确坐标表示
- Path:表示多边形轮廓或路径
- Paths:多个路径的集合
- PolyTree/PolyNode:层次化的多边形结构
- 枚举类型:各种操作选项
重点掌握:
- 坐标缩放的原理和实践
- 多边形方向性的重要性
- Path 和 Paths 的使用场景
- PolyTree 在处理复杂层次时的优势
- 各种枚举类型的选择
在下一章中,我们将学习如何使用这些数据结构执行布尔运算,包括交集、并集、差集和异或等操作。
2.11 练习题
-
坐标转换练习:实现一个完整的坐标转换工具类,支持多种缩放系数
-
路径操作:创建一个五角星路径,计算其面积和周长
-
方向判断:编写程序判断给定路径的方向,并根据需要自动调整
-
复杂多边形:创建一个带有多个孔洞的复杂多边形,使用 PolyTree 表示其结构
-
边界框应用:编写函数判断两个多边形的边界框是否相交
-
数据清理:实现一个函数,移除路径中的重复点和共线点

浙公网安备 33010602011771号