




























Clipper1 定义了三个基础的几何数据结构:IntPoint(整数点)、DoublePoint(浮点点)和 IntRect(整数矩形)。这些结构是整个库的基石,用于表示坐标点和边界框。
IntPoint 是 Clipper 中最核心的数据结构,用于表示一个二维(或三维)整数坐标点。
public struct IntPoint
{
public cInt X;
public cInt Y;
#if use_xyz
public cInt Z;
#endif
}
关键设计决策:
使用 struct 而非 class:
使用 cInt 类型别名:
Int64,提供更大的坐标范围当 use_xyz 未定义时,IntPoint 只有 X、Y 两个坐标:
public IntPoint(cInt X, cInt Y)
{
this.X = X;
this.Y = Y;
}
public IntPoint(double x, double y)
{
this.X = (cInt)x;
this.Y = (cInt)y;
}
public IntPoint(IntPoint pt)
{
this.X = pt.X;
this.Y = pt.Y;
}
构造函数分析:
double 转换为 cInt,注意这是截断而非四舍五入当定义了 use_xyz 时,IntPoint 增加 Z 坐标:
#if use_xyz
public IntPoint(cInt x, cInt y, cInt z = 0)
{
this.X = x;
this.Y = y;
this.Z = z;
}
public IntPoint(double x, double y, double z = 0)
{
this.X = (cInt)x;
this.Y = (cInt)y;
this.Z = (cInt)z;
}
public IntPoint(DoublePoint dp)
{
this.X = (cInt)dp.X;
this.Y = (cInt)dp.Y;
this.Z = 0;
}
public IntPoint(IntPoint pt)
{
this.X = pt.X;
this.Y = pt.Y;
this.Z = pt.Z;
}
#endif
Z 坐标的用途:
public static bool operator ==(IntPoint a, IntPoint b)
{
return a.X == b.X && a.Y == b.Y;
}
public static bool operator !=(IntPoint a, IntPoint b)
{
return a.X != b.X || a.Y != b.Y;
}
设计要点:
public override bool Equals(object obj)
{
if (obj == null) return false;
if (obj is IntPoint)
{
IntPoint a = (IntPoint)obj;
return (X == a.X) && (Y == a.Y);
}
else return false;
}
实现细节:
is 关键字检查类型== 运算符行为一致public override int GetHashCode()
{
//simply prevents a compiler warning
return base.GetHashCode();
}
注意:这里的实现只是为了消除编译器警告(重写 Equals 时必须重写 GetHashCode)。实际使用中,如果需要将 IntPoint 用作字典键或放入 HashSet,应该提供更好的哈希实现。
更好的哈希实现建议:
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 31 + X.GetHashCode();
hash = hash * 31 + Y.GetHashCode();
return hash;
}
}
// 创建点
IntPoint p1 = new IntPoint(100, 200);
IntPoint p2 = new IntPoint(100.7, 200.9); // 截断为 (100, 200)
IntPoint p3 = new IntPoint(p1); // 复制
// 比较
bool equal = (p1 == p2); // true,因为截断后相等
bool notEqual = (p1 != new IntPoint(101, 200)); // true
// 作为 Path 的元素
Path polygon = new Path();
polygon.Add(new IntPoint(0, 0));
polygon.Add(new IntPoint(100, 0));
polygon.Add(new IntPoint(100, 100));
polygon.Add(new IntPoint(0, 100));
DoublePoint 用于表示双精度浮点坐标点,主要在内部计算和偏移操作中使用。
public struct DoublePoint
{
public double X;
public double Y;
public DoublePoint(double x = 0, double y = 0)
{
this.X = x;
this.Y = y;
}
public DoublePoint(DoublePoint dp)
{
this.X = dp.X;
this.Y = dp.Y;
}
public DoublePoint(IntPoint ip)
{
this.X = ip.X;
this.Y = ip.Y;
}
}
// IntPoint -> DoublePoint(无损)
IntPoint ip = new IntPoint(100, 200);
DoublePoint dp = new DoublePoint(ip); // (100.0, 200.0)
// DoublePoint -> IntPoint(可能有精度损失)
DoublePoint dp2 = new DoublePoint(100.7, 200.9);
IntPoint ip2 = new IntPoint((cInt)dp2.X, (cInt)dp2.Y); // (100, 200)
// ClipperOffset 内部使用 DoublePoint 存储法向量
internal static DoublePoint GetUnitNormal(IntPoint pt1, IntPoint pt2)
{
double dx = (pt2.X - pt1.X);
double dy = (pt2.Y - pt1.Y);
if ((dx == 0) && (dy == 0)) return new DoublePoint();
double f = 1 * 1.0 / Math.Sqrt(dx * dx + dy * dy);
dx *= f;
dy *= f;
return new DoublePoint(dy, -dx);
}
算法解析:
(dx, dy)f = 1/√(dx² + dy²)(dx*f, dy*f)(dy*f, -dx*f)IntRect 表示一个轴对齐的整数边界矩形(Axis-Aligned Bounding Box, AABB)。
public struct IntRect
{
public cInt left;
public cInt top;
public cInt right;
public cInt bottom;
public IntRect(cInt l, cInt t, cInt r, cInt b)
{
this.left = l;
this.top = t;
this.right = r;
this.bottom = b;
}
public IntRect(IntRect ir)
{
this.left = ir.left;
this.top = ir.top;
this.right = ir.right;
this.bottom = ir.bottom;
}
}
Clipper 使用的坐标系统中:
这与计算机图形学中常用的屏幕坐标系一致。
ClipperBase 类提供了计算路径集合边界框的方法:
public static IntRect GetBounds(Paths paths)
{
int i = 0, cnt = paths.Count;
// 跳过空路径
while (i < cnt && paths[i].Count == 0) i++;
if (i == cnt) return new IntRect(0, 0, 0, 0);
IntRect result = new IntRect();
// 初始化为第一个点
result.left = paths[i][0].X;
result.right = result.left;
result.top = paths[i][0].Y;
result.bottom = result.top;
// 遍历所有点,更新边界
for (; i < cnt; i++)
for (int j = 0; j < paths[i].Count; j++)
{
if (paths[i][j].X < result.left)
result.left = paths[i][j].X;
else if (paths[i][j].X > result.right)
result.right = paths[i][j].X;
if (paths[i][j].Y < result.top)
result.top = paths[i][j].Y;
else if (paths[i][j].Y > result.bottom)
result.bottom = paths[i][j].Y;
}
return result;
}
算法分析:
// 创建一些路径
Paths paths = new Paths();
Path p1 = new Path();
p1.Add(new IntPoint(10, 20));
p1.Add(new IntPoint(100, 30));
p1.Add(new IntPoint(50, 80));
paths.Add(p1);
Path p2 = new Path();
p2.Add(new IntPoint(200, 10));
p2.Add(new IntPoint(250, 100));
paths.Add(p2);
// 计算边界框
IntRect bounds = ClipperBase.GetBounds(paths);
// 结果: left=10, top=10, right=250, bottom=100
Console.WriteLine($"边界: ({bounds.left}, {bounds.top}) - ({bounds.right}, {bounds.bottom})");
Console.WriteLine($"宽度: {bounds.right - bounds.left}");
Console.WriteLine($"高度: {bounds.bottom - bounds.top}");
虽然不是结构体,但 Path 和 Paths 是 Clipper 中最常用的类型:
using Path = List<IntPoint>;
using Paths = List<List<IntPoint>>;
Path polygon = new Path();
// 添加顶点(顺时针方向 = 外轮廓)
polygon.Add(new IntPoint(0, 0));
polygon.Add(new IntPoint(100, 0));
polygon.Add(new IntPoint(100, 100));
polygon.Add(new IntPoint(0, 100));
// 访问顶点
IntPoint first = polygon[0];
int count = polygon.Count;
// 遍历
foreach (IntPoint pt in polygon)
{
Console.WriteLine($"({pt.X}, {pt.Y})");
}
// 反转方向
polygon.Reverse();
Paths polygons = new Paths();
// 添加外轮廓
Path outer = new Path();
outer.Add(new IntPoint(0, 0));
outer.Add(new IntPoint(200, 0));
outer.Add(new IntPoint(200, 200));
outer.Add(new IntPoint(0, 200));
polygons.Add(outer);
// 添加内孔(逆时针方向)
Path hole = new Path();
hole.Add(new IntPoint(50, 50));
hole.Add(new IntPoint(50, 150));
hole.Add(new IntPoint(150, 150));
hole.Add(new IntPoint(150, 50));
polygons.Add(hole);
// 访问
int pathCount = polygons.Count;
Path firstPath = polygons[0];
Clipper 遵循以下方向约定(在 Y 轴向上的坐标系中):
| 类型 | 方向 | 面积符号 |
|---|---|---|
| 外轮廓 | 逆时针 | 正 |
| 孔洞 | 顺时针 | 负 |
注意:在 Y 轴向下的屏幕坐标系中,方向相反。
// 使用 Orientation 方法
bool isCounterClockwise = Clipper.Orientation(polygon);
// 使用 Area 方法
double area = Clipper.Area(polygon);
bool isOuter = area > 0; // 正面积 = 逆时针 = 外轮廓
bool isHole = area < 0; // 负面积 = 顺时针 = 孔洞
32位模式 (use_int32):
+---+---+
| X | Y | = 8 bytes
+---+---+
(4 bytes each)
64位模式 (默认):
+-------+-------+
| X | Y | = 16 bytes
+-------+-------+
(8 bytes each)
64位模式 + use_xyz:
+-------+-------+-------+
| X | Y | Z | = 24 bytes
+-------+-------+-------+
(8 bytes each)
// 值类型复制 - 每次赋值都会复制所有字段
IntPoint p1 = new IntPoint(100, 200);
IntPoint p2 = p1; // 复制 16 字节
p2.X = 50; // 不影响 p1
// 引用传递优化
void ProcessPoint(ref IntPoint pt)
{
pt.X = pt.X * 2;
}
// 避免在循环中频繁创建新实例
Path optimized = new Path(1000); // 预分配容量
for (int i = 0; i < 1000; i++)
{
optimized.Add(new IntPoint(i, i * 2));
}
using System.Drawing;
// Point -> IntPoint
Point sysPoint = new Point(100, 200);
IntPoint clipperPoint = new IntPoint(sysPoint.X, sysPoint.Y);
// IntPoint -> Point
IntPoint ip = new IntPoint(100, 200);
Point sp = new Point((int)ip.X, (int)ip.Y);
// PointF -> IntPoint(带缩放)
const double scale = 1000.0;
PointF pf = new PointF(1.5f, 2.7f);
IntPoint scaled = new IntPoint((cInt)(pf.X * scale), (cInt)(pf.Y * scale));
由于 IntPoint 是简单的结构体,可以轻松序列化:
// JSON 序列化示例
using System.Text.Json;
Path polygon = new Path();
polygon.Add(new IntPoint(0, 0));
polygon.Add(new IntPoint(100, 0));
polygon.Add(new IntPoint(100, 100));
// 转换为 JSON 友好的格式
var jsonData = polygon.Select(p => new { x = p.X, y = p.Y }).ToArray();
string json = JsonSerializer.Serialize(jsonData);
// [{"x":0,"y":0},{"x":100,"y":0},{"x":100,"y":100}]
本章详细分析了 Clipper1 的三个基础数据结构:
IntPoint:
DoublePoint:
IntRect:
Path 和 Paths:
这些基础结构的设计体现了 Clipper 追求精确性和性能的设计理念,为后续的复杂算法提供了坚实的基础。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。