惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
About on SuperTechFans
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
G
Google Developers Blog
J
Java Code Geeks
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
H
Heimdal Security Blog
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
S
Security @ Cisco Blogs
Latest news
Latest news
I
Intezer
L
Lohrmann on Cybersecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
月光博客
月光博客
T
Threatpost
博客园 - 【当耐特】
S
Schneier on Security
P
Privacy International News Feed
G
GRAHAM CLULEY
T
Tenable Blog
AWS News Blog
AWS News Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
博客园 - Franky
Engineering at Meta
Engineering at Meta
美团技术团队
S
Secure Thoughts
T
Troy Hunt's Blog
Microsoft Security Blog
Microsoft Security Blog
SecWiki News
SecWiki News
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第17章:与 Qt VTK Python pythonOCC 生态集成 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第15章:填充规则详解
我才是银古 · 2026-04-11 · via 博客园 - 我才是银古

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