znlgis 博客

GIS开发与技术分享

第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 中的重要概念:

  1. 四种规则
    • EvenOdd:奇数穿越为内部
    • NonZero:非零缠绕数为内部
    • Positive:正缠绕数为内部
    • Negative:负缠绕数为内部
  2. 缠绕数
    • 从点向外射线穿越边界的代数和
    • 方向决定正负
  3. 实现细节
    • windCnt:同类型路径的缠绕计数
    • windCnt2:另一类型路径的缠绕计数
    • 交点处更新缠绕计数
  4. 选择建议
    • 一般情况用 EvenOdd
    • 需要方向感知用 NonZero
    • 特殊需求用 Positive/Negative

正确选择填充规则对于获得预期的裁剪结果至关重要。


上一章:布尔运算执行流程 返回目录 下一章:ClipperOffset偏移类详解