

























Clipper1 提供了两种表示多边形结果的方式:
Paths(路径列表),简单但不包含层次信息PolyTree(多边形树),包含完整的父子关系和孔洞信息本章将深入分析这两种表示方式的源码实现。
PolyNode 是多边形树的基本节点,代表一个多边形轮廓。
public class PolyNode
{
internal PolyNode m_Parent;
internal Path m_polygon = new Path();
internal int m_Index;
internal JoinType m_jointype;
internal EndType m_endtype;
internal List<PolyNode> m_Childs = new List<PolyNode>();
// ... 方法定义 ...
}
| 成员 | 类型 | 可见性 | 说明 |
|---|---|---|---|
m_Parent |
PolyNode |
internal | 父节点引用 |
m_polygon |
Path |
internal | 多边形顶点列表 |
m_Index |
int |
internal | 在父节点子列表中的索引 |
m_jointype |
JoinType |
internal | 连接类型(用于偏移) |
m_endtype |
EndType |
internal | 端点类型(用于偏移) |
m_Childs |
List<PolyNode> |
internal | 子节点列表 |
判断当前节点是否为孔洞:
private bool IsHoleNode()
{
bool result = true;
PolyNode node = m_Parent;
while (node != null)
{
result = !result;
node = node.m_Parent;
}
return result;
}
public bool IsHole
{
get { return IsHoleNode(); }
}
算法原理:
树中的层次决定了多边形的性质:
PolyTree(根,层0)
├── 外轮廓A(层1)→ IsHole = false(根的子节点从层1开始)
│ └── 孔洞A1(层2)→ IsHole = true
│ └── 岛屿A1a(层3)→ IsHole = false
└── 外轮廓B(层1)→ IsHole = false
注意:从代码逻辑看,算法从当前节点开始向上遍历,每遍历一层就翻转结果。初始值为 true,到达根节点(Parent=null)时停止。这意味着根节点的直接子节点(第1层)会经历一次翻转,结果为 false(非孔洞)。
public int ChildCount
{
get { return m_Childs.Count; }
}
public Path Contour
{
get { return m_polygon; }
}
public List<PolyNode> Childs
{
get { return m_Childs; }
}
public PolyNode Parent
{
get { return m_Parent; }
}
public bool IsOpen { get; set; }
| 属性 | 说明 |
|---|---|
ChildCount |
子节点数量 |
Contour |
多边形轮廓(顶点列表) |
Childs |
子节点列表 |
Parent |
父节点 |
IsOpen |
是否为开放路径(线段) |
internal void AddChild(PolyNode Child)
{
int cnt = m_Childs.Count;
m_Childs.Add(Child);
Child.m_Parent = this;
Child.m_Index = cnt;
}
功能:
public PolyNode GetNext()
{
if (m_Childs.Count > 0)
return m_Childs[0];
else
return GetNextSiblingUp();
}
internal PolyNode GetNextSiblingUp()
{
if (m_Parent == null)
return null;
else if (m_Index == m_Parent.m_Childs.Count - 1)
return m_Parent.GetNextSiblingUp();
else
return m_Parent.m_Childs[m_Index + 1];
}
遍历算法:深度优先遍历
GetNext():
GetNextSiblingUp():
遍历示例:
// 遍历整个树
PolyNode node = polyTree.GetFirst();
while (node != null)
{
Console.WriteLine($"轮廓顶点数: {node.Contour.Count}, IsHole: {node.IsHole}");
node = node.GetNext();
}
PolyTree 继承自 PolyNode,作为多边形树的根节点。
public class PolyTree : PolyNode
{
internal List<PolyNode> m_AllPolys = new List<PolyNode>();
public void Clear()
{
for (int i = 0; i < m_AllPolys.Count; i++)
m_AllPolys[i] = null;
m_AllPolys.Clear();
m_Childs.Clear();
}
public PolyNode GetFirst()
{
if (m_Childs.Count > 0)
return m_Childs[0];
else
return null;
}
public int Total
{
get
{
int result = m_AllPolys.Count;
//with negative offsets, ignore the hidden outer polygon ...
if (result > 0 && m_Childs[0] != m_AllPolys[0]) result--;
return result;
}
}
}
m_AllPolys 存储树中所有 PolyNode 的扁平列表,便于快速访问:
// 在 BuildResult2 方法中填充
polytree.m_AllPolys.Capacity = m_PolyOuts.Count;
for (int i = 0; i < m_PolyOuts.Count; i++)
{
// ... 创建 PolyNode ...
PolyNode pn = new PolyNode();
polytree.m_AllPolys.Add(pn);
// ...
}
public void Clear()
{
for (int i = 0; i < m_AllPolys.Count; i++)
m_AllPolys[i] = null; // 帮助垃圾回收
m_AllPolys.Clear();
m_Childs.Clear();
}
设计考虑:显式将引用设为 null 可以帮助垃圾回收器更快回收内存。
public PolyNode GetFirst()
{
if (m_Childs.Count > 0)
return m_Childs[0];
else
return null;
}
返回第一个顶级多边形(外轮廓),用于开始遍历。
public int Total
{
get
{
int result = m_AllPolys.Count;
//with negative offsets, ignore the hidden outer polygon ...
if (result > 0 && m_Childs[0] != m_AllPolys[0]) result--;
return result;
}
}
特殊处理:负偏移可能会产生一个隐藏的外部多边形,需要从计数中排除。
using Path = List<IntPoint>;
using Paths = List<List<IntPoint>>;
// 创建正方形
Path CreateSquare(int size)
{
Path square = new Path(4); // 预分配容量
square.Add(new IntPoint(0, 0));
square.Add(new IntPoint(size, 0));
square.Add(new IntPoint(size, size));
square.Add(new IntPoint(0, size));
return square;
}
// 创建圆形近似
Path CreateCircle(int centerX, int centerY, int radius, int segments = 36)
{
Path circle = new Path(segments);
double angleStep = 2 * Math.PI / segments;
for (int i = 0; i < segments; i++)
{
double angle = i * angleStep;
circle.Add(new IntPoint(
centerX + (int)(radius * Math.Cos(angle)),
centerY + (int)(radius * Math.Sin(angle))
));
}
return circle;
}
// 创建带孔的矩形
Paths CreateRectWithHole()
{
Paths result = new Paths(2); // 预分配:1个外轮廓 + 1个孔
// 外轮廓(逆时针)
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));
result.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));
result.Add(hole);
return result;
}
Clipper 提供了在两种表示之间转换的静态方法:
public static Paths PolyTreeToPaths(PolyTree polytree)
{
Paths result = new Paths();
result.Capacity = polytree.Total;
AddPolyNodeToPaths(polytree, NodeType.ntAny, result);
return result;
}
internal static void AddPolyNodeToPaths(PolyNode polynode, NodeType nt, Paths paths)
{
bool match = true;
switch (nt)
{
case NodeType.ntOpen: return; // 跳过开放路径
case NodeType.ntClosed: match = !polynode.IsOpen; break;
default: break; // ntAny 匹配所有
}
if (polynode.m_polygon.Count > 0 && match)
paths.Add(polynode.m_polygon);
foreach (PolyNode pn in polynode.Childs)
AddPolyNodeToPaths(pn, nt, paths);
}
NodeType 枚举:
internal enum NodeType { ntAny, ntOpen, ntClosed };
提取所有开放路径:
public static Paths OpenPathsFromPolyTree(PolyTree polytree)
{
Paths result = new Paths();
result.Capacity = polytree.ChildCount;
for (int i = 0; i < polytree.ChildCount; i++)
if (polytree.Childs[i].IsOpen)
result.Add(polytree.Childs[i].m_polygon);
return result;
}
提取所有闭合路径:
public static Paths ClosedPathsFromPolyTree(PolyTree polytree)
{
Paths result = new Paths();
result.Capacity = polytree.Total;
AddPolyNodeToPaths(polytree, NodeType.ntClosed, result);
return result;
}
PolyTree 保留了多边形之间的包含关系:
原始多边形:
┌────────────────────┐
│ 外轮廓 A │
│ ┌──────────────┐ │
│ │ 孔洞 A1 │ │
│ │ ┌────────┐ │ │
│ │ │岛屿A1a │ │ │
│ │ └────────┘ │ │
│ └──────────────┘ │
└────────────────────┘
PolyTree 结构:
Root
└── A (IsHole=false)
└── A1 (IsHole=true)
└── A1a (IsHole=false)
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 简单裁剪 | Paths | 足够且高效 |
| 需要孔洞信息 | PolyTree | 保留层次关系 |
| 渲染带孔多边形 | PolyTree | 需要区分外轮廓和孔 |
| 进一步布尔运算 | Paths | 可直接作为输入 |
| SVG 导出 | PolyTree | 便于生成正确的路径结构 |
void PrintPolyTree(PolyTree tree)
{
PrintNode(tree, 0);
}
void PrintNode(PolyNode node, int depth)
{
string indent = new string(' ', depth * 2);
if (node.Contour.Count > 0)
{
string type = node.IsHole ? "孔洞" : "外轮廓";
string open = node.IsOpen ? " (开放)" : "";
Console.WriteLine($"{indent}{type}: {node.Contour.Count}个顶点{open}");
}
foreach (var child in node.Childs)
{
PrintNode(child, depth + 1);
}
}
// 使用
Clipper clipper = new Clipper();
clipper.AddPaths(subjects, PolyType.ptSubject, true);
clipper.AddPaths(clips, PolyType.ptClip, true);
PolyTree result = new PolyTree();
clipper.Execute(ClipType.ctIntersection, result);
PrintPolyTree(result);
Clipper 内部使用两个方法构建结果:
private void BuildResult(Paths polyg)
{
polyg.Clear();
polyg.Capacity = m_PolyOuts.Count;
for (int i = 0; i < m_PolyOuts.Count; i++)
{
OutRec outRec = m_PolyOuts[i];
if (outRec.Pts == null) continue;
OutPt p = outRec.Pts.Prev;
int cnt = PointCount(p);
if (cnt < 2) continue;
Path pg = new Path(cnt);
for (int j = 0; j < cnt; j++)
{
pg.Add(p.Pt);
p = p.Prev;
}
polyg.Add(pg);
}
}
流程:
private void BuildResult2(PolyTree polytree)
{
polytree.Clear();
// 添加每个输出多边形到 polytree
polytree.m_AllPolys.Capacity = m_PolyOuts.Count;
for (int i = 0; i < m_PolyOuts.Count; i++)
{
OutRec outRec = m_PolyOuts[i];
int cnt = PointCount(outRec.Pts);
if ((outRec.IsOpen && cnt < 2) || (!outRec.IsOpen && cnt < 3))
continue;
FixHoleLinkage(outRec);
PolyNode pn = new PolyNode();
polytree.m_AllPolys.Add(pn);
outRec.PolyNode = pn;
pn.m_polygon.Capacity = cnt;
OutPt op = outRec.Pts.Prev;
for (int j = 0; j < cnt; j++)
{
pn.m_polygon.Add(op.Pt);
op = op.Prev;
}
}
// 修复 PolyNode 链接
polytree.m_Childs.Capacity = m_PolyOuts.Count;
for (int i = 0; i < m_PolyOuts.Count; i++)
{
OutRec outRec = m_PolyOuts[i];
if (outRec.PolyNode == null) continue;
else if (outRec.IsOpen)
{
outRec.PolyNode.IsOpen = true;
polytree.AddChild(outRec.PolyNode);
}
else if (outRec.FirstLeft != null &&
outRec.FirstLeft.PolyNode != null)
outRec.FirstLeft.PolyNode.AddChild(outRec.PolyNode);
else
polytree.AddChild(outRec.PolyNode);
}
}
流程:
public static double Area(Path poly)
{
int cnt = (int)poly.Count;
if (cnt < 3) return 0;
double a = 0;
for (int i = 0, j = cnt - 1; i < cnt; ++i)
{
a += ((double)poly[j].X + poly[i].X) *
((double)poly[j].Y - poly[i].Y);
j = i;
}
return -a * 0.5;
}
算法:使用鞋带公式(Shoelace formula)计算有符号面积。
数学公式:
A = (1/2) * |Σ(x[i] * y[i+1] - x[i+1] * y[i])|
简化形式(代码使用):
A = -(1/2) * Σ((x[j] + x[i]) * (y[j] - y[i]))
public static bool Orientation(Path poly)
{
return Area(poly) >= 0;
}
返回值:
true:逆时针方向(正面积)false:顺时针方向(负面积)public static void ReversePaths(Paths polys)
{
foreach (var poly in polys)
{
poly.Reverse();
}
}
反转所有路径的方向。
本章详细分析了 Clipper1 的多边形表示系统:
PolyNode:
PolyTree:
Path 和 Paths:
转换方法:
面积和方向:
理解这些数据结构对于正确使用 Clipper 的结果至关重要,特别是在需要处理复杂多边形(带孔洞或多层嵌套)的场景中。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。