第14章:布尔运算执行流程
14.1 概述
本章将详细分析 Clipper2 执行布尔运算的完整流程,从输入多边形到输出结果的每个步骤。理解这个流程对于调试问题和优化性能至关重要。
14.2 执行流程总览
┌─────────────────────────────────────────────────────┐
│ Execute() │
├─────────────────────────────────────────────────────┤
│ 1. 预处理阶段 │
│ ├── 构建顶点列表 │
│ ├── 识别局部极值 │
│ └── 排序局部极小值 │
├─────────────────────────────────────────────────────┤
│ 2. 扫描线处理阶段 │
│ ├── 插入局部极小值到 AEL │
│ ├── 处理水平边 │
│ ├── 计算并处理交点 │
│ └── 处理扫描带顶部 │
├─────────────────────────────────────────────────────┤
│ 3. 后处理阶段 │
│ ├── 处理连接 │
│ ├── 清理共线点 │
│ └── 构建输出路径 │
└─────────────────────────────────────────────────────┘
14.3 预处理阶段
14.3.1 AddPath 处理
public void AddPaths(Paths64 paths, PathType pathtype, bool isOpen = false)
{
// 标记是否有开放路径
if (isOpen) _hasOpenPaths = true;
// 标记需要重新排序
_isSortedMinimaList = false;
foreach (Path64 path in paths)
{
// 跳过无效路径
if (path.Count < 2) continue;
if (!isOpen && path.Count < 3) continue;
// 构建顶点链表
AddPathToVertexList(path, pathtype, isOpen);
}
}
14.3.2 构建顶点链表
private void AddPathToVertexList(Path64 path, PathType pathtype, bool isOpen)
{
// 创建顶点
Vertex? firstVert = null;
Vertex? prevVert = null;
foreach (Point64 pt in path)
{
// 跳过重复点
if (prevVert != null && pt == prevVert.pt) continue;
Vertex v = new Vertex { pt = pt };
if (firstVert == null)
firstVert = v;
else
{
prevVert!.next = v;
v.prev = prevVert;
}
prevVert = v;
}
// 闭合路径
if (!isOpen && firstVert != null && prevVert != null)
{
prevVert.next = firstVert;
firstVert.prev = prevVert;
}
// 标记并添加局部极值
if (firstVert != null)
FindAndMarkLocalMinMax(firstVert, pathtype, isOpen);
}
14.3.3 识别局部极值
private void FindAndMarkLocalMinMax(Vertex firstVertex,
PathType pathtype, bool isOpen)
{
Vertex v = firstVertex;
do
{
// 跳过水平段
if (v.pt.Y == v.next!.pt.Y)
{
v = v.next;
continue;
}
// 检查局部极小值
if (IsLocalMin(v))
{
v.flags |= VertexFlags.LocalMin;
AddLocMin(v, pathtype, isOpen);
}
// 检查局部最大值
else if (IsLocalMax(v))
{
v.flags |= VertexFlags.LocalMax;
}
v = v.next;
} while (v != firstVertex);
}
private bool IsLocalMin(Vertex v)
{
Vertex prev = v.prev!;
Vertex next = v.next!;
// 向上找第一个非水平点
while (prev.pt.Y == v.pt.Y) prev = prev.prev!;
while (next.pt.Y == v.pt.Y) next = next.next!;
return prev.pt.Y > v.pt.Y && next.pt.Y >= v.pt.Y;
}
14.3.4 排序局部极小值
private void SortMinimaList()
{
if (!_isSortedMinimaList)
{
// 按 Y 坐标降序排序(从大到小)
_minimaList.Sort((a, b) => b.vertex.pt.Y.CompareTo(a.vertex.pt.Y));
_isSortedMinimaList = true;
// 初始化扫描线列表
_scanlineList.Clear();
foreach (var lm in _minimaList)
_scanlineList.Add(lm.vertex.pt.Y);
}
}
14.4 扫描线处理主循环
14.4.1 ExecuteInternal 主循环
private void ExecuteInternal(ClipType ct, FillRule fillrule)
{
if (ct == ClipType.NoClip) return;
_fillrule = fillrule;
_cliptype = ct;
Reset();
if (!PopScanline(out long y)) return;
while (true)
{
// 步骤1:插入当前 Y 的局部极小值
InsertLocalMinimaIntoAEL(y);
// 步骤2:处理所有水平边
while (PopHorz(out Active? horz))
{
DoHorizontal(horz!);
}
// 步骤3:获取下一个扫描线
if (!PopScanline(out long topY)) break;
// 步骤4:处理当前扫描带的交点
DoIntersections(y, topY);
// 步骤5:处理扫描带顶部事件
DoTopOfScanbeam(topY);
// 步骤6:更新水平边
while (PopHorz(out Active? horz))
{
DoHorizontal(horz!);
}
y = topY;
}
}
14.4.2 扫描带概念
Y = 100 ─────────────────── 扫描线 (当前 y)
╲ ╱
╲ 扫描带 ╱
╲ ╱
Y = 80 ─────────────────── 扫描线 (topY)
╲ ╱
╲ ╱
╲ ╱
Y = 60 ─────────────────── 下一个扫描线
14.5 步骤1:插入局部极小值
14.5.1 InsertLocalMinimaIntoAEL
private void InsertLocalMinimaIntoAEL(long botY)
{
while (PopLocalMinima(botY, out LocalMinima locMin))
{
Vertex vert = locMin.vertex;
// 创建左边(向 next 方向)
Active leftBound = new Active
{
bot = vert.pt,
top = vert.next!.pt,
curX = vert.pt.X,
dx = GetDx(vert.pt, vert.next.pt),
windDx = 1,
vertexTop = vert.next,
localMin = locMin,
isLeftBound = true
};
// 创建右边(向 prev 方向)
Active rightBound = new Active
{
bot = vert.pt,
top = vert.prev!.pt,
curX = vert.pt.X,
dx = GetDx(vert.pt, vert.prev.pt),
windDx = -1,
vertexTop = vert.prev,
localMin = locMin,
isLeftBound = false
};
// 插入到 AEL
InsertLeftEdge(leftBound);
// 设置缠绕计数
SetWindCountForClosedPathEdge(leftBound);
// 检查是否贡献输出
if (IsContributingClosed(leftBound))
{
Active ae2 = rightBound;
SetOutRecReference(leftBound, ae2);
}
InsertRightEdge(rightBound, leftBound);
SetWindCountForClosedPathEdge(rightBound);
// 添加局部最小值输出点
if (leftBound.outrec != null)
{
AddLocalMinPoly(leftBound, rightBound, leftBound.bot);
}
// 添加下一个扫描线
if (leftBound.top.Y < botY)
AddScanline(leftBound.top.Y);
if (rightBound.top.Y < botY)
AddScanline(rightBound.top.Y);
}
}
14.5.2 AEL 排序插入
private void InsertLeftEdge(Active ae)
{
if (_actives == null)
{
ae.prevInAEL = null;
ae.nextInAEL = null;
_actives = ae;
}
else if (!IsValidAelOrder(_actives, ae))
{
ae.prevInAEL = null;
ae.nextInAEL = _actives;
_actives.prevInAEL = ae;
_actives = ae;
}
else
{
Active ae2 = _actives;
while (ae2.nextInAEL != null && IsValidAelOrder(ae2.nextInAEL, ae))
ae2 = ae2.nextInAEL;
ae.nextInAEL = ae2.nextInAEL;
if (ae2.nextInAEL != null)
ae2.nextInAEL.prevInAEL = ae;
ae.prevInAEL = ae2;
ae2.nextInAEL = ae;
}
}
14.6 步骤2:处理水平边
14.6.1 DoHorizontal
private void DoHorizontal(Active horz)
{
// 确定水平边方向
bool leftToRight;
long horzLeft, horzRight;
if (horz.bot.X < horz.top.X)
{
leftToRight = true;
horzLeft = horz.bot.X;
horzRight = horz.top.X;
}
else
{
leftToRight = false;
horzLeft = horz.top.X;
horzRight = horz.bot.X;
}
// 处理与其他边的交点
Active? ae = leftToRight ? horz.nextInAEL : horz.prevInAEL;
while (ae != null)
{
// 检查是否在水平边范围内
if ((leftToRight && ae.curX > horzRight) ||
(!leftToRight && ae.curX < horzLeft))
break;
// 计算交点
Point64 pt = new Point64(ae.curX, horz.bot.Y);
// 处理交点
if (ae.curX >= horzLeft && ae.curX <= horzRight)
{
IntersectEdges(horz, ae, pt);
SwapPositionsInAEL(horz, ae);
}
ae = leftToRight ? ae.nextInAEL : ae.prevInAEL;
}
// 更新或删除水平边
UpdateEdgeIntoAEL(ref horz);
}
14.7 步骤3-4:处理交点
14.7.1 BuildIntersectList
private bool BuildIntersectList(long topY)
{
if (_actives == null || _actives.nextInAEL == null)
return false;
// 使用简单冒泡找出需要交换的边对
bool result = false;
Active? ae = _actives;
while (ae.nextInAEL != null)
{
Active ae2 = ae.nextInAEL;
// 计算两边在 topY 的 X 坐标
long x1 = TopX(ae, topY);
long x2 = TopX(ae2, topY);
// 检查是否需要交换(X 顺序改变)
if (x1 > x2)
{
// 计算交点
if (GetIntersection(ae, ae2, out Point64 pt))
{
// 限制在扫描带内
if (pt.Y > _currentBotY) pt.Y = _currentBotY;
if (pt.Y < topY) pt.Y = topY;
AddIntersectNode(ae, ae2, pt);
result = true;
}
}
ae = ae2;
}
// 排序交点(按 Y 降序,Y 相同时按 X 升序)
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 result;
}
14.7.2 ProcessIntersectList
private void ProcessIntersectList()
{
foreach (IntersectNode node in _intersectList)
{
// 处理交点
IntersectEdges(node.edge1, node.edge2, node.pt);
// 交换边在 AEL 中的位置
SwapPositionsInAEL(node.edge1, node.edge2);
}
_intersectList.Clear();
}
14.7.3 IntersectEdges
private void IntersectEdges(Active ae1, Active ae2, Point64 pt)
{
// 保存当前状态
bool ae1Contributing = ae1.outrec != null;
bool ae2Contributing = ae2.outrec != null;
// 更新缠绕计数
if (GetPolyType(ae1) == GetPolyType(ae2))
{
// 同类型
if (_fillrule == FillRule.EvenOdd)
{
int tmp = ae1.windCnt;
ae1.windCnt = ae2.windCnt;
ae2.windCnt = tmp;
}
else
{
ae1.windCnt += ae2.windDx;
ae2.windCnt -= ae1.windDx;
}
}
else
{
// 不同类型
ae1.windCnt2 += ae2.windDx;
ae2.windCnt2 -= ae1.windDx;
}
// 检查新的贡献状态
bool ae1ContributingNew = IsContributing(ae1);
bool ae2ContributingNew = IsContributing(ae2);
// 根据状态变化处理
if (ae1Contributing && ae2Contributing)
{
if (ae1ContributingNew && ae2ContributingNew)
{
// 两边继续贡献
AddOutPt(ae1, pt);
AddOutPt(ae2, pt);
SwapOutrecs(ae1, ae2);
}
else if (ae1ContributingNew)
{
// 只有 ae1 继续
AddOutPt(ae1, pt);
SwapOutrecs(ae1, ae2);
}
else if (ae2ContributingNew)
{
// 只有 ae2 继续
AddOutPt(ae2, pt);
SwapOutrecs(ae1, ae2);
}
else
{
// 两边都停止 -> 局部最大值
AddLocalMaxPoly(ae1, ae2, pt);
}
}
else if (ae1Contributing)
{
AddOutPt(ae1, pt);
SwapOutrecs(ae1, ae2);
}
else if (ae2Contributing)
{
AddOutPt(ae2, pt);
SwapOutrecs(ae1, ae2);
}
else if (!ae1ContributingNew && !ae2ContributingNew)
{
// 开始新的局部最小值
AddLocalMinPoly(ae1, ae2, pt, true);
}
}
14.8 步骤5:处理扫描带顶部
14.8.1 DoTopOfScanbeam
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, y);
}
else
{
// 继续向上
// 检查是否有新的水平边
if (IsHorizontal(ae.vertexTop!))
{
PushHorz(ae);
}
ae = ae.nextInAEL;
}
}
}
14.8.2 DoMaxima
private Active? DoMaxima(Active ae, long y)
{
Active? prevE = ae.prevInAEL;
Active? nextE = ae.nextInAEL;
// 检查是否是局部最大值
if (IsLocalMaxima(ae))
{
// 查找配对边
Active? maxPair = GetMaximaPair(ae);
if (maxPair == null)
{
// 开放路径端点
if (ae.outrec != null)
AddOutPt(ae, ae.top);
DeleteFromAEL(ae);
return nextE;
}
// 添加局部最大值
if (ae.outrec != null || maxPair.outrec != null)
AddLocalMaxPoly(ae, maxPair, ae.top);
DeleteFromAEL(ae);
DeleteFromAEL(maxPair);
return prevE != null ? prevE.nextInAEL : _actives;
}
// 更新到下一段边
UpdateEdgeIntoAEL(ref ae);
return ae.nextInAEL;
}
14.9 后处理阶段
14.9.1 BuildPaths
private void BuildPaths(Paths64 closedPaths, Paths64 openPaths)
{
foreach (OutRec outrec in _outrecList)
{
if (outrec.pts == null) continue;
// 清理共线点
CleanCollinear(outrec);
if (outrec.pts == null) continue;
// 构建路径
Path64? path = BuildPath(outrec.pts,
ReverseSolution != IsPositive(outrec),
outrec.isOpen);
if (path == null || path.Count < 2) continue;
if (outrec.isOpen)
openPaths.Add(path);
else if (path.Count > 2)
closedPaths.Add(path);
}
}
14.9.2 CleanCollinear
private void CleanCollinear(OutRec outrec)
{
if (!PreserveCollinear)
{
OutPt? op = outrec.pts;
if (op == null) return;
OutPt startOp = op;
do
{
// 检查是否共线
if (InternalClipper.IsCollinear(op.prev!.pt, op.pt, op.next!.pt))
{
if (op == outrec.pts)
outrec.pts = op.prev;
DisposeOutPt(op);
op = op.prev!;
if (op == outrec.pts)
startOp = op;
}
else
{
op = op.next;
}
} while (op != startOp && outrec.pts != null);
}
}
14.10 完整流程示例
14.10.1 两个正方形求交
// 输入
Path64 subject = new Path64 {
(0,0), (100,0), (100,100), (0,100)
};
Path64 clip = new Path64 {
(50,50), (150,50), (150,150), (50,150)
};
// 1. 预处理
// Subject: 局部极小值在 Y=100 (顶点 (0,100) 和 (100,100))
// Clip: 局部极小值在 Y=150 (顶点 (50,150) 和 (150,150))
// 2. 扫描开始 Y=150
// 插入 Clip 的两条边
// 3. Y=100
// 插入 Subject 的两条边
// 检测交点
// 4. 交点处理
// (50,100), (100,50) 处产生交点
// 5. 构建输出
// 结果: (50,50), (100,50), (100,100), (50,100)
14.11 本章小结
布尔运算执行流程包含三个主要阶段:
- 预处理阶段:
- 构建顶点链表
- 识别局部极值
- 排序局部极小值
- 扫描线处理:
- 插入局部极小值
- 处理水平边
- 计算交点
- 处理扫描带顶部
- 后处理阶段:
- 清理共线点
- 构建输出路径
理解这个流程有助于调试和优化 Clipper2 的使用。
| 上一章:ClipperD浮点裁剪类 | 返回目录 | 下一章:填充规则详解 |