第15章填充规则详解,具体有哪些应用场景?

摘要:layout: default title: 第15章:填充规则详解 第15章:填充规则详解 15.1 概述 填充规则(Fill Rule)决定了多边形的哪些区域被视为"内部"。这在处理自
第15章:填充规则详解 15.1 概述 填充规则(Fill Rule)决定了多边形的哪些区域被视为"内部"。这在处理自相交多边形、嵌套多边形和复杂形状时尤为重要。Clipper2 支持四种填充规则:EvenOdd、NonZero、Positive 和 Negative。 15.2 FillRule 枚举 15.2.1 定义 public enum FillRule { EvenOdd, // 奇偶规则 NonZero, // 非零规则 Positive, // 正向规则 Negative // 负向规则 } 15.2.2 缠绕数概念 缠绕数(Winding Number)是理解填充规则的关键: 从某点向外画一条射线,统计穿过边界的次数: - 从右向左穿过:+1 - 从左向右穿过:-1 缠绕数 = 所有穿越的代数和 15.3 EvenOdd(奇偶规则) 15.3.1 原理 从任意点向外画射线,统计与多边形边界相交的次数: 奇数次相交:点在内部 偶数次相交:点在外部 private bool IsContributing_EvenOdd(Active ae) { return (ae.windCnt & 1) != 0; // windCnt 为奇数 } 15.3.2 图示 ↗ 1次穿越 → 内部 ┌─────────────────┐ │ │ │ ┌───────┐ │ ↗ 2次穿越 → 外部 │ │ │ │ │ │ ● │ │ ↗ 3次穿越 → 内部 │ │ │ │ │ └───────┘ │ │ │ └─────────────────┘ 15.3.3 特点 路径方向无关:顺时针或逆时针都一样 自相交处理:相交区域会形成"孔洞" 最常用:大多数图形应用的默认选择 15.3.4 自相交示例 EvenOdd 规则下的蝴蝶结: ╲ ╱ ╲ ╱ ╲ ╱ ╲ ╱ ← 交叉点 ╱╲ ╱ ╲ 交叉区域被视为"外部" ╱ ╲ ╱ ╲ 15.4 NonZero(非零规则) 15.4.1 原理 计算缠绕数,非零即为内部: 缠绕数 ≠ 0:点在内部 缠绕数 = 0:点在外部 private bool IsContributing_NonZero(Active ae) { return ae.windCnt != 0; } 15.4.2 缠绕数计算 ↑ 射线 ┌────────────┼────────────┐ │ │ │ │ ←─────── │ ───────→ │ 顺时针:-1 │ │ │ │ ┌────────┼────────┐ │ │ │ │ │ │ │ │ ←──── │ ────→ │ │ 顺时针:-1 │ │ │ │ │ │ │ ● │ │ │ │ │ │ │ │ │ └────────┼────────┘ │ │ │ │ └────────────┼────────────┘ │ 缠绕数 = -1 + (-1) = -2 ≠ 0 → 内部 15.4.3 同向嵌套 同向嵌套(都是逆时针): ┌───────────────┐ ← 外轮廓 │ ← │ │ ┌─────────┐ │ ← 内轮廓 │ │ ← │ │ │ │ ┌───┐ │ │ ← 最内轮廓 │ │ │ ← │ │ │ │ │ └───┘ │ │ │ └─────────┘ │ └───────────────┘ NonZero: 全部为内部 EvenOdd: 内外内(交替) 15.4.4 反向嵌套 反向嵌套(一个逆时针,一个顺时针): ┌───────────────┐ ← 逆时针 │ ← │ │ ┌─────────┐ │ ← 顺时针 │ │ → │ │ │ │ │ │ 缠绕数 = 1 - 1 = 0 │ │ ● │ │ → 外部(孔洞) │ │ │ │ │ └─────────┘ │ └───────────────┘ 15.5 Positive(正向规则) 15.5.1 原理 只有当缠绕数为正时,点才在内部: private bool IsContributing_Positive(Active ae) { return ae.windCnt > 0; } 15.5.2 用途 只保留逆时针方向的区域: 逆时针(正面积)→ 保留 顺时针(负面积)→ 移除 15.5.3 示例 两个重叠多边形: ┌───────────────┐ ← 逆时针 (windCnt = 1) │ │ │ ┌─────┐ │ ← 逆时针 (windCnt = 2) │ │ │ │ │ │ ● │ │ windCnt = 2 > 0 → 内部 │ │ │ │ │ └─────┘ │ │ │ └───────────────┘ 如果内部是顺时针: ┌───────────────┐ ← 逆时针 (windCnt = 1) │ │ │ ┌─────┐ │ ← 顺时针 (windCnt = 0) │ │ │ │ │ │ ● │ │ windCnt = 0 → 外部 │ │ │ │ │ └─────┘ │ └───────────────┘ 15.6 Negative(负向规则) 15.6.1 原理 只有当缠绕数为负时,点才在内部: private bool IsContributing_Negative(Active ae) { return ae.windCnt < 0; } 15.6.2 用途 只保留顺时针方向的区域: 顺时针(负面积)→ 保留 逆时针(正面积)→ 移除 15.7 填充规则实现 15.7.1 IsContributing 方法 private bool IsContributingClosed(Active ae) { switch (_fillrule) { case FillRule.EvenOdd: // 奇数穿越为内部 if (GetPolyType(ae) == PathType.Subject) return (ae.windCnt & 1) != 0; else return (ae.windCnt2 & 1) != 0; case FillRule.NonZero: // 非零为内部 if (GetPolyType(ae) == PathType.Subject) return ae.windCnt != 0; else return ae.windCnt2 != 0; case FillRule.Positive: // 正数为内部 if (GetPolyType(ae) == PathType.Subject) return ae.windCnt > 0; else return ae.windCnt2 > 0; case FillRule.Negative: // 负数为内部 if (GetPolyType(ae) == PathType.Subject) return ae.windCnt < 0; else return ae.windCnt2 < 0; } return false; } 15.7.2 布尔运算中的填充判断 private bool IsContributing(Active ae) { switch (_cliptype) { case ClipType.Intersection: return IsIntersectionContributing(ae); case ClipType.Union: return IsUnionContributing(ae); case ClipType.Difference: return IsDifferenceContributing(ae); case ClipType.Xor: return IsXorContributing(ae); } return false; } private bool IsIntersectionContributing(Active ae) { // 交集:两个多边形都要在内部 bool inSubject = IsInsideSubject(ae); bool inClip = IsInsideClip(ae); return inSubject && inClip; } private bool IsUnionContributing(Active ae) { // 并集:任一多边形在内部 bool inSubject = IsInsideSubject(ae); bool inClip = IsInsideClip(ae); return inSubject || inClip; } 15.8 缠绕计数更新 15.8.1 SetWindCountForClosedPathEdge private void SetWindCountForClosedPathEdge(Active ae) { Active? ae2 = ae.prevInAEL; // 找到同类型的前一条边 PathType pt = GetPolyType(ae); while (ae2 != null && (GetPolyType(ae2) != pt || IsOpen(ae2))) { ae2 = ae2.prevInAEL; } if (ae2 == null) { // 没有前一条同类型边 ae.windCnt = ae.windDx; ae2 = _actives; } else if (_fillrule == FillRule.EvenOdd) { // 奇偶规则 ae.windCnt = ae.windDx; ae.windCnt2 = ae2.windCnt2; } else { // 非零/正向/负向规则 // 累加缠绕数 if (ae2.windCnt * ae2.windDx < 0) { if (Math.Abs(ae2.windCnt) > 1) { if (ae2.windDx * ae.windDx < 0) ae.windCnt = ae2.windCnt; else ae.windCnt = ae2.windCnt + ae.windDx; } else ae.windCnt = IsOpen(ae) ? 1 : ae.windDx; } else { if (ae2.windDx * ae.windDx < 0) ae.windCnt = ae2.windCnt; else ae.windCnt = ae2.windCnt + ae.windDx; } ae.windCnt2 = ae2.windCnt2; } // 计算另一类型的缠绕计数 CalcWindCnt2(ae); } 15.8.2 交点处的缠绕更新 private void UpdateWindingOnIntersection(Active ae1, Active 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; } } } 15.9 填充规则对比 15.9.1 视觉对比 同向嵌套三层正方形: EvenOdd: NonZero: ┌─────────────┐ ┌─────────────┐ │ ░░░░░░░░░░░ │ │ ░░░░░░░░░░░ │ │ ░┌───────┐░ │ │ ░░░░░░░░░░░ │ │ ░│ │░ │ │ ░░░░░░░░░░░ │ │ ░│ ░░░░░ │░ │ │ ░░░░░░░░░░░ │ │ ░│ ░ ░ │░ │ │ ░░░░░░░░░░░ │ │ ░│ ░░░░░ │░ │ │ ░░░░░░░░░░░ │ │ ░│ │░ │ │ ░░░░░░░░░░░ │ │ ░└───────┘░ │ │ ░░░░░░░░░░░ │ │ ░░░░░░░░░░░ │ │ ░░░░░░░░░░░ │ └─────────────┘ └─────────────┘ 15.9.2 自相交多边形 八字形(自相交): EvenOdd: NonZero: ╱╲ ╱╲ ╱░░╲ ╱░░╲ ╱░░░░╲ ╱░░░░╲ ╱░░░░░░╲ ╱░░░░░░╲ ╱────────╲──╱ ╱────────╲──╱ ╲────────╱──╲ ╲░░░░░░░░╱──╲ ╲░░░░░░╱ ╲░░░░░░╱ ╲░░░░╱ ╲░░░░╱ ╲░░╱ ╲░░╱ ╲╱ ╲╱ 15.10 选择填充规则 15.10.1 使用建议 填充规则 适用场景 EvenOdd SVG/PDF 默认,自相交形成孔洞 NonZero 字体渲染,同向路径合并 Positive 只保留正向(逆时针)区域 Negative 只保留负向(顺时针)区域 15.10.2 常见应用 // SVG 路径渲染 clipper.Execute(ClipType.Union, FillRule.EvenOdd, result); // 字体轮廓 clipper.Execute(ClipType.Union, FillRule.NonZero, result); // 提取外轮廓 clipper.Execute(ClipType.Union, FillRule.Positive, result); 15.11 本章小结 填充规则是 Clipper2 中的重要概念: 四种规则: EvenOdd:奇数穿越为内部 NonZero:非零缠绕数为内部 Positive:正缠绕数为内部 Negative:负缠绕数为内部 缠绕数: 从点向外射线穿越边界的代数和 方向决定正负 实现细节: windCnt:同类型路径的缠绕计数 windCnt2:另一类型路径的缠绕计数 交点处更新缠绕计数 选择建议: 一般情况用 EvenOdd 需要方向感知用 NonZero 特殊需求用 Positive/Negative 正确选择填充规则对于获得预期的裁剪结果至关重要。 上一章:布尔运算执行流程 | 返回目录 | 下一章:ClipperOffset偏移类详解