第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偏移类详解 |