第12章:Clipper64 裁剪类详解
12.1 概述
Clipper64 是 Clipper2 的核心裁剪类,用于对整数坐标的多边形执行布尔运算。它继承自 ClipperBase,实现了完整的 Vatti 扫描线裁剪算法。
12.2 类定义
12.2.1 类声明
public class Clipper64 : ClipperBase
{
#if USINGZ
internal ZCallback64? _zCallback;
public ZCallback64? ZCallback
{
get => _zCallback;
set => _zCallback = value;
}
#endif
}
12.2.2 继承关系
ClipperBase (抽象基类)
↓
Clipper64 (整数坐标裁剪)
12.3 Execute 方法
12.3.1 主要重载
// 输出到 Paths64
public bool Execute(ClipType clipType, FillRule fillRule, Paths64 closedSolution)
{
return Execute(clipType, fillRule, closedSolution, new Paths64());
}
// 输出到 Paths64,分离闭合和开放路径
public bool Execute(ClipType clipType, FillRule fillRule,
Paths64 closedSolution, Paths64 openSolution)
{
closedSolution.Clear();
openSolution.Clear();
try
{
ExecuteInternal(clipType, fillRule);
BuildPaths(closedSolution, openSolution);
}
catch
{
return false;
}
ClearSolution();
return true;
}
// 输出到 PolyTree64
public bool Execute(ClipType clipType, FillRule fillRule, PolyTree64 polytree)
{
return Execute(clipType, fillRule, polytree, new Paths64());
}
// 输出到 PolyTree64,分离开放路径
public bool Execute(ClipType clipType, FillRule fillRule,
PolyTree64 polytree, Paths64 openSolution)
{
polytree.Clear();
openSolution.Clear();
try
{
ExecuteInternal(clipType, fillRule);
BuildTree(polytree, openSolution);
}
catch
{
return false;
}
ClearSolution();
return true;
}
12.3.2 返回值说明
true:裁剪成功false:裁剪过程中发生异常
12.4 ExecuteInternal 方法
12.4.1 核心执行流程
private void ExecuteInternal(ClipType clipType, FillRule fillRule)
{
if (clipType == ClipType.NoClip) return;
_fillrule = fillRule;
_cliptype = clipType;
Reset();
// 排序局部极小值
if (!PopScanline(out long y)) return;
while (true)
{
// 插入当前 Y 的局部极小值
InsertLocalMinimaIntoAEL(y);
// 处理水平边
while (PopHorz(out Active? ae))
{
DoHorizontal(ae!);
}
// 处理顶部事件
if (!PopScanline(out long nextY)) break;
// 处理边交点
DoIntersections(y, nextY);
// 处理顶部边
DoTopOfScanbeam(nextY);
// 继续下一个扫描线
y = nextY;
}
// 处理开放路径
ProcessOpenPaths();
}
12.4.2 Reset 方法
private void Reset()
{
if (!_isSortedMinimaList)
{
_minimaList.Sort((a, b) => b.vertex.pt.Y.CompareTo(a.vertex.pt.Y));
_isSortedMinimaList = true;
}
// 重置扫描线
_scanlineList.Clear();
foreach (var lm in _minimaList)
AddScanline(lm.vertex.pt.Y);
_currentLocMinIdx = 0;
_actives = null;
_sel = null;
_currentBotY = 0;
}
12.5 InsertLocalMinimaIntoAEL
12.5.1 方法实现
private void InsertLocalMinimaIntoAEL(long botY)
{
// 处理所有当前 Y 坐标的局部极小值
while (PopLocalMinima(botY, out LocalMinima locMin))
{
Vertex vert = locMin.vertex;
// 创建左边和右边
Active leftBound = CreateLeftEdge(vert, locMin);
Active rightBound = CreateRightEdge(vert, locMin);
// 检查是否是有效边
if (leftBound.bot.Y != leftBound.top.Y)
{
// 插入左边到 AEL
InsertLeftEdge(leftBound);
// 设置缠绕计数
SetWindCountForClosedPathEdge(leftBound);
// 检查是否贡献输出
if (IsContributingClosed(leftBound))
SetOutRecReference(leftBound, rightBound);
}
if (rightBound.bot.Y != rightBound.top.Y)
{
// 插入右边到 AEL
InsertRightEdge(rightBound, leftBound);
// 设置缠绕计数
SetWindCountForClosedPathEdge(rightBound);
// 检查交点
CheckAndAddIntersection(leftBound, rightBound, botY);
}
// 添加局部最小值多边形
if (leftBound.outrec != null || rightBound.outrec != null)
AddLocalMinPoly(leftBound, rightBound, leftBound.bot);
}
}
12.5.2 CreateLeftEdge
private Active CreateLeftEdge(Vertex v, LocalMinima locMin)
{
Active ae = new Active();
ae.bot = v.pt;
ae.curX = v.pt.X;
ae.localMin = locMin;
ae.isLeftBound = true;
// 设置顶点和方向
ae.vertexTop = v.next!;
ae.top = ae.vertexTop.pt;
ae.windDx = 1;
ae.dx = GetDx(ae.bot, ae.top);
return ae;
}
12.6 DoIntersections
12.6.1 交点处理主逻辑
private void DoIntersections(long topY, long nextY)
{
if (BuildIntersectList(topY, nextY))
{
ProcessIntersectList();
_intersectList.Clear();
}
}
12.6.2 BuildIntersectList
private bool BuildIntersectList(long topY, long nextY)
{
if (_actives == null || _actives.nextInAEL == null) return false;
// 计算所有相邻边对的交点
Active? ae = _actives;
while (ae.nextInAEL != null)
{
Active ae2 = ae.nextInAEL;
if (GetIntersection(ae, ae2, out Point64 pt) &&
pt.Y >= nextY && pt.Y <= topY)
{
// 添加到交点列表
AddIntersect(ae, ae2, pt);
}
ae = ae2;
}
// 按 Y 坐标排序交点
if (_intersectList.Count > 1)
{
_intersectList.Sort((a, b) => {
if (a.pt.Y != b.pt.Y)
return b.pt.Y.CompareTo(a.pt.Y);
return a.pt.X.CompareTo(b.pt.X);
});
}
return _intersectList.Count > 0;
}
12.6.3 ProcessIntersectList
private void ProcessIntersectList()
{
foreach (IntersectNode node in _intersectList)
{
// 处理交点
IntersectEdges(node.ae1, node.ae2, node.pt);
// 交换边在 AEL 中的位置
SwapPositionsInAEL(node.ae1, node.ae2);
}
}
12.7 IntersectEdges
12.7.1 核心交点处理
private void IntersectEdges(Active ae1, Active ae2, Point64 pt)
{
// 根据填充规则和路径类型决定如何处理交点
bool ae1Contributing = ae1.outrec != null;
bool ae2Contributing = ae2.outrec != null;
// 更新缠绕计数
UpdateWindCount(ae1, ae2);
bool ae1ContributingNew = IsContributing(ae1);
bool ae2ContributingNew = IsContributing(ae2);
if (ae1Contributing != ae1ContributingNew ||
ae2Contributing != ae2ContributingNew)
{
// 贡献状态改变,需要添加输出点
if (ae1Contributing && ae2Contributing)
{
// 两边都贡献 -> 添加局部最大值
AddLocalMaxPoly(ae1, ae2, pt);
}
else if (ae1Contributing)
{
// 只有 ae1 贡献
AddOutPt(ae1, pt);
SwapOutrecs(ae1, ae2);
}
else if (ae2Contributing)
{
// 只有 ae2 贡献
AddOutPt(ae2, pt);
SwapOutrecs(ae1, ae2);
}
else
{
// 两边都不贡献 -> 开始新的局部最小值
AddLocalMinPoly(ae1, ae2, pt, true);
}
}
else if (ae1Contributing)
{
// 状态未改变,添加输出点
AddOutPt(ae1, pt);
AddOutPt(ae2, pt);
}
}
12.7.2 UpdateWindCount
private void UpdateWindCount(Active ae1, Active ae2)
{
// 更新 ae1 和 ae2 的缠绕计数
if (GetPolyType(ae1) == GetPolyType(ae2))
{
// 同类型路径
if (_fillrule == FillRule.EvenOdd)
{
int tmp = ae1.windCnt;
ae1.windCnt = ae2.windCnt;
ae2.windCnt = tmp;
}
else
{
if (ae1.windCnt + ae2.windDx == 0)
ae1.windCnt = -ae1.windCnt;
else
ae1.windCnt += ae2.windDx;
if (ae2.windCnt - ae1.windDx == 0)
ae2.windCnt = -ae2.windCnt;
else
ae2.windCnt -= ae1.windDx;
}
}
else
{
// 不同类型路径
if (_fillrule != FillRule.EvenOdd)
{
ae1.windCnt2 += ae2.windDx;
ae2.windCnt2 -= ae1.windDx;
}
else
{
ae1.windCnt2 = ae1.windCnt2 == 0 ? 1 : 0;
ae2.windCnt2 = ae2.windCnt2 == 0 ? 1 : 0;
}
}
}
12.8 DoTopOfScanbeam
12.8.1 处理扫描带顶部
private void DoTopOfScanbeam(long y)
{
_sel = null;
Active? ae = _actives;
while (ae != null)
{
// 更新边的当前 X 坐标
ae.curX = TopX(ae, y);
if (ae.top.Y == y)
{
// 边到达顶点
ae = DoMaxima(ae);
}
else
{
// 边继续向上
if (ae.vertexTop != null)
{
// 检查是否有水平边
if (ae.vertexTop.pt.Y == y &&
ae.vertexTop.pt.X != ae.top.X)
{
// 处理水平边
PushHorz(ae);
}
}
ae = ae.nextInAEL;
}
}
}
12.8.2 DoMaxima
private Active? DoMaxima(Active ae)
{
Active? nextAe = ae.nextInAEL;
// 查找配对边
Active? ae2 = GetMaximaPair(ae);
if (ae2 == null)
{
// 没有配对边(开放路径)
if (ae.outrec != null)
AddOutPt(ae, ae.top);
DeleteFromAEL(ae);
return nextAe;
}
// 处理局部最大值
if (ae.outrec != null || ae2.outrec != null)
{
AddLocalMaxPoly(ae, ae2, ae.top);
}
// 更新边到下一段
if (IsLocalMaxima(ae))
{
DeleteFromAEL(ae);
DeleteFromAEL(ae2);
}
else
{
UpdateEdgeIntoAEL(ref ae);
UpdateEdgeIntoAEL(ref ae2);
}
return nextAe;
}
12.9 水平边处理
12.9.1 DoHorizontal
private void DoHorizontal(Active horz)
{
Point64 pt;
bool horzIsOpen = IsOpen(horz);
// 确定水平边方向
Point64 startPt, endPt;
if (horz.bot.X < horz.top.X)
{
startPt = horz.bot;
endPt = horz.top;
}
else
{
startPt = horz.top;
endPt = horz.bot;
}
// 获取左右边界
Active? leftBound = GetLeftBound(horz);
Active? rightBound = GetRightBound(horz);
// 处理与其他边的交点
Active? ae = leftBound;
while (ae != null && ae != rightBound)
{
pt.X = ae.curX;
pt.Y = horz.bot.Y;
if (pt.X >= startPt.X && pt.X <= endPt.X)
{
// 处理交点
IntersectEdges(horz, ae, pt);
// 交换位置
SwapPositionsInAEL(horz, ae);
}
ae = ae.nextInAEL;
}
// 更新水平边
UpdateEdgeIntoAEL(ref horz);
}
12.10 BuildPaths
12.10.1 构建输出路径
private void BuildPaths(Paths64 closedPaths, Paths64 openPaths)
{
closedPaths.Capacity = _outrecList.Count;
openPaths.Capacity = _outrecList.Count;
foreach (OutRec outrec in _outrecList)
{
if (outrec.pts == null) continue;
Path64? path = BuildPath(outrec.pts,
ReverseSolution != IsPositive(outrec),
outrec.isOpen);
if (path == null) continue;
if (outrec.isOpen)
openPaths.Add(path);
else
closedPaths.Add(path);
}
}
12.10.2 IsPositive 判断
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPositive(OutRec outrec)
{
// 计算多边形面积的符号
return Area(outrec) >= 0;
}
12.11 Z 坐标处理
12.11.1 ZCallback 委托
#if USINGZ
public delegate void ZCallback64(Point64 bot1, Point64 top1,
Point64 bot2, Point64 top2, ref Point64 intersectPt);
#endif
12.11.2 使用示例
#if USINGZ
Clipper64 clipper = new Clipper64();
clipper.ZCallback = (bot1, top1, bot2, top2, ref Point64 pt) =>
{
// 线性插值计算 Z 值
if (top1.Y - bot1.Y != 0)
{
double t = (pt.Y - bot1.Y) / (double)(top1.Y - bot1.Y);
pt.Z = (long)(bot1.Z + t * (top1.Z - bot1.Z));
}
else
{
pt.Z = bot1.Z;
}
};
#endif
12.12 错误处理
12.12.1 异常捕获
public bool Execute(ClipType clipType, FillRule fillRule, Paths64 solution)
{
solution.Clear();
try
{
ExecuteInternal(clipType, fillRule);
BuildPaths(solution, new Paths64());
return true;
}
catch (Exception)
{
// 捕获所有异常,返回 false
return false;
}
finally
{
ClearSolution();
}
}
12.12.2 常见错误
- 空路径:输入路径少于 3 个点
- 坐标溢出:坐标超出 MaxCoord 范围
- 自相交:复杂的自相交情况
12.13 使用示例
12.13.1 基本使用
Clipper64 clipper = new Clipper64();
// 添加主体
Path64 subject = new Path64 {
new Point64(0, 0),
new Point64(100, 0),
new Point64(100, 100),
new Point64(0, 100)
};
clipper.AddSubject(subject);
// 添加裁剪
Path64 clip = new Path64 {
new Point64(50, 50),
new Point64(150, 50),
new Point64(150, 150),
new Point64(50, 150)
};
clipper.AddClip(clip);
// 执行交集
Paths64 result = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
12.13.2 多次执行
Clipper64 clipper = new Clipper64();
clipper.AddSubject(subject);
clipper.AddClip(clip);
// 第一次:交集
Paths64 intersection = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, intersection);
// 不需要 Clear(),可以继续使用
// 第二次:并集
Paths64 union = new Paths64();
clipper.Execute(ClipType.Union, FillRule.NonZero, union);
12.14 本章小结
Clipper64 是 Clipper2 的核心类:
- Execute 方法:执行布尔运算的入口
- ExecuteInternal:扫描线算法主循环
- 关键步骤:
- 插入局部极小值
- 处理交点
- 处理水平边
- 处理扫描带顶部
- 输出构建:将内部结构转换为 Paths64 或 PolyTree64
- Z 坐标支持:通过 ZCallback 处理 Z 值
掌握 Clipper64 的使用和实现原理是使用 Clipper2 的关键。
| 上一章:OutRec与OutPt输出结构 | 返回目录 | 下一章:ClipperD浮点裁剪类 |