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

推荐订阅源

V
V2EX
PCI Perspectives
PCI Perspectives
Webroot Blog
Webroot Blog
Help Net Security
Help Net Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
S
Securelist
V2EX - 技术
V2EX - 技术
Schneier on Security
Schneier on Security
P
Proofpoint News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Forbes - Security
Forbes - Security
N
News | PayPal Newsroom
Cyberwarzone
Cyberwarzone
C
Cisco Blogs
Cloudbric
Cloudbric
GbyAI
GbyAI
A
About on SuperTechFans
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Vercel News
Vercel News
P
Proofpoint News Feed
SecWiki News
SecWiki News
T
Tailwind CSS Blog
腾讯CDC
C
Cybersecurity and Infrastructure Security Agency CISA
The Hacker News
The Hacker News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Microsoft Azure Blog
Microsoft Azure Blog
T
Troy Hunt's Blog
G
Google Developers Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cisco Talos Blog
Cisco Talos Blog
D
DataBreaches.Net
V
Vulnerabilities – Threatpost
博客园 - 叶小钗
C
Check Point Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Know Your Adversary
Know Your Adversary
T
Tor Project blog
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
Y
Y Combinator Blog
H
Help Net Security
Latest news
Latest news

博客园 - 我才是银古

第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章:二次开发实战与综合案例 第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 偏移类详解 第15章:填充规则详解 第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
第16章:填充规则详解
我才是银古 · 2026-06-24 · via 博客园 - 我才是银古

第16章:填充规则详解

16.1 概述

填充规则决定了多边形的哪些区域被视为"内部"。Clipper 支持四种填充规则,不同的规则在处理自相交多边形时会产生不同的结果。

16.2 PolyFillType 枚举

public enum PolyFillType { 
    pftEvenOdd,   // 奇偶规则
    pftNonZero,   // 非零规则
    pftPositive,  // 正向规则
    pftNegative   // 负向规则
}

16.3 EvenOdd(奇偶规则)

16.3.1 原理

从任意点发出射线,计算与多边形边界的交点数:

  • 奇数交点:点在多边形内部
  • 偶数交点:点在多边形外部

16.3.2 图解

    ┌─────────────────────┐
    │        1            │  1个交点 → 内部
    │    ┌───────┐        │
    │    │   2   │        │  2个交点 → 外部
    │    │       │        │
    │    └───────┘        │
    │        1            │  1个交点 → 内部
    └─────────────────────┘
    0                        0个交点 → 外部

自相交情况:
        ╲
         ╲
    ┌─────╲────┐
    │      ╲   │
    │       ╲──┤
    │      ╱   │
    │     ╱    │
    └────╱─────┘
        ╱
交叉区域有 2 个交点 → 外部(空洞)

16.3.3 代码实现

private bool IsEvenOddFillType(TEdge edge)
{
    if (edge.PolyTyp == PolyType.ptSubject)
        return m_SubjFillType == PolyFillType.pftEvenOdd;
    else
        return m_ClipFillType == PolyFillType.pftEvenOdd;
}

// 在 IsContributing 中
case PolyFillType.pftEvenOdd:
    if (edge.WindDelta == 0 && edge.WindCnt != 1) 
        return false;
    break;

16.4 NonZero(非零规则)

16.4.1 原理

从任意点发出射线,计算缠绕数(Winding Number):

  • 向上穿过边界时 +1
  • 向下穿过边界时 -1
  • 缠绕数 ≠ 0:点在多边形内部
  • 缠绕数 = 0:点在多边形外部

16.4.2 图解

方向向量 →

    ↓ -1      ↑ +1
    │         │
    │   W=0   │
    │  (外部) │
    ├─────────┤
    │         │
    │   W=+1  │
    │  (内部) │
    └─────────┘

自相交(同向):
    ↓         ↓
    │         │
    │   W=+2  │  ← 重叠区域仍为内部
    │         │
    ├─────────┤
    │   W=+1  │
    └─────────┘

自相交(反向):
    ↓         ↑
    │         │
    │   W=0   │  ← 重叠区域为外部
    │         │
    ├─────────┤
    │   W=+1  │
    └─────────┘

16.4.3 代码实现

case PolyFillType.pftNonZero:
    if (Math.Abs(edge.WindCnt) != 1) 
        return false;
    break;

16.5 Positive(正向规则)

16.5.1 原理

只有缠绕数 > 0 的区域被视为内部。

16.5.2 图解

    ↓ -1      ↑ +1      ↓ -1
    │         │         │
    │  W=-1   │  W=0    │  W=-1
    │  (外部) │ (外部)  │  (外部)
    ├─────────┼─────────┤
    │  W=0    │  W=+1   │  W=0
    │ (外部)  │ (内部)  │  (外部)
    └─────────┴─────────┘

16.5.3 代码实现

case PolyFillType.pftPositive:
    if (edge.WindCnt != 1) 
        return false;
    break;

16.6 Negative(负向规则)

16.6.1 原理

只有缠绕数 < 0 的区域被视为内部。

16.6.2 图解

    ↓ -1      ↑ +1      ↓ -1
    │         │         │
    │  W=-1   │  W=0    │  W=-1
    │  (内部) │ (外部)  │  (内部)
    ├─────────┼─────────┤
    │  W=0    │  W=+1   │  W=0
    │ (外部)  │ (外部)  │  (外部)
    └─────────┴─────────┘

16.6.3 代码实现

default: // pftNegative
    if (edge.WindCnt != -1) 
        return false; 
    break;

16.7 缠绕数计算

16.7.1 WindDelta

// 边的 WindDelta 取决于边的方向和多边形的方向
// 逆时针多边形:从下到上的边 WindDelta = +1
// 顺时针多边形:从下到上的边 WindDelta = -1
// 开放路径:WindDelta = 0

16.7.2 SetWindingCount

private void SetWindingCount(TEdge edge)
{
    TEdge e = edge.PrevInAEL;
    
    // 找到同类型的前一条边
    while (e != null && ((e.PolyTyp != edge.PolyTyp) || (e.WindDelta == 0))) 
        e = e.PrevInAEL;
    
    if (e == null)
    {
        // 第一条边
        if (edge.WindDelta == 0) 
            edge.WindCnt = (pft == PolyFillType.pftNegative ? -1 : 1);
        else 
            edge.WindCnt = edge.WindDelta;
        edge.WindCnt2 = 0;
    }
    else if (IsEvenOddFillType(edge))
    {
        // 奇偶规则:简单切换
        edge.WindCnt = edge.WindDelta;
        edge.WindCnt2 = e.WindCnt2;
    }
    else
    {
        // 非零规则:累加
        if (e.WindCnt * e.WindDelta < 0)
        {
            // 缠绕数正在减少
            if (Math.Abs(e.WindCnt) > 1)
            {
                if (e.WindDelta * edge.WindDelta < 0) 
                    edge.WindCnt = e.WindCnt;
                else 
                    edge.WindCnt = e.WindCnt + edge.WindDelta;
            }
            else
                edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta);
        }
        else
        {
            // 缠绕数正在增加
            if (edge.WindDelta == 0)
                edge.WindCnt = (e.WindCnt < 0 ? e.WindCnt - 1 : e.WindCnt + 1);
            else if (e.WindDelta * edge.WindDelta < 0)
                edge.WindCnt = e.WindCnt;
            else 
                edge.WindCnt = e.WindCnt + edge.WindDelta;
        }
        edge.WindCnt2 = e.WindCnt2;
    }
    
    // 计算另一类多边形的缠绕数
    // ...
}

16.8 IsContributing 中的填充规则

private bool IsContributing(TEdge edge)
{
    PolyFillType pft, pft2;
    if (edge.PolyTyp == PolyType.ptSubject)
    {
        pft = m_SubjFillType;
        pft2 = m_ClipFillType;
    }
    else
    {
        pft = m_ClipFillType;
        pft2 = m_SubjFillType;
    }

    // 检查自身类型的缠绕数
    switch (pft)
    {
        case PolyFillType.pftEvenOdd:
            if (edge.WindDelta == 0 && edge.WindCnt != 1) 
                return false;
            break;
        case PolyFillType.pftNonZero:
            if (Math.Abs(edge.WindCnt) != 1) 
                return false;
            break;
        case PolyFillType.pftPositive:
            if (edge.WindCnt != 1) 
                return false;
            break;
        default: // pftNegative
            if (edge.WindCnt != -1) 
                return false; 
            break;
    }

    // 检查与另一类多边形的关系(根据 ClipType)
    // ...
}

16.9 填充规则与布尔运算的结合

16.9.1 交集(Intersection)

case ClipType.ctIntersection:
    switch (pft2)
    {
        case PolyFillType.pftEvenOdd:
        case PolyFillType.pftNonZero:
            return (edge.WindCnt2 != 0);  // 在另一多边形内部
        case PolyFillType.pftPositive:
            return (edge.WindCnt2 > 0);
        default:
            return (edge.WindCnt2 < 0);
    }

16.9.2 并集(Union)

case ClipType.ctUnion:
    switch (pft2)
    {
        case PolyFillType.pftEvenOdd:
        case PolyFillType.pftNonZero:
            return (edge.WindCnt2 == 0);  // 不在另一多边形内部
        case PolyFillType.pftPositive:
            return (edge.WindCnt2 <= 0);
        default:
            return (edge.WindCnt2 >= 0);
    }

16.9.3 差集(Difference)

case ClipType.ctDifference:
    if (edge.PolyTyp == PolyType.ptSubject)
        // Subject 边:不在 Clip 内部
        switch (pft2)
        {
            case PolyFillType.pftEvenOdd:
            case PolyFillType.pftNonZero:
                return (edge.WindCnt2 == 0);
            // ...
        }
    else
        // Clip 边:在 Subject 内部
        switch (pft2)
        {
            case PolyFillType.pftEvenOdd:
            case PolyFillType.pftNonZero:
                return (edge.WindCnt2 != 0);
            // ...
        }

16.10 使用示例

16.10.1 自相交多边形

// 创建 8 字形多边形
Path figure8 = new Path();
figure8.Add(new IntPoint(0, 0));
figure8.Add(new IntPoint(100, 100));
figure8.Add(new IntPoint(100, 0));
figure8.Add(new IntPoint(0, 100));

Clipper clipper = new Clipper();
clipper.AddPath(figure8, PolyType.ptSubject, true);

// 使用奇偶规则
Paths resultEvenOdd = new Paths();
clipper.Execute(ClipType.ctUnion, resultEvenOdd, 
    PolyFillType.pftEvenOdd, PolyFillType.pftEvenOdd);
// 结果:交叉区域为空洞

// 使用非零规则
clipper.Clear();
clipper.AddPath(figure8, PolyType.ptSubject, true);
Paths resultNonZero = new Paths();
clipper.Execute(ClipType.ctUnion, resultNonZero, 
    PolyFillType.pftNonZero, PolyFillType.pftNonZero);
// 结果:取决于边的方向

16.10.2 重叠多边形

// 两个重叠的正方形
Path square1 = CreateSquare(0, 0, 100);
Path square2 = CreateSquare(50, 50, 100);

Clipper clipper = new Clipper();
clipper.AddPath(square1, PolyType.ptSubject, true);
clipper.AddPath(square2, PolyType.ptSubject, true);

// 奇偶规则:重叠区域变成空洞
Paths resultEO = new Paths();
clipper.Execute(ClipType.ctUnion, resultEO, 
    PolyFillType.pftEvenOdd);

// 非零规则:重叠区域保持填充
clipper.Clear();
clipper.AddPath(square1, PolyType.ptSubject, true);
clipper.AddPath(square2, PolyType.ptSubject, true);
Paths resultNZ = new Paths();
clipper.Execute(ClipType.ctUnion, resultNZ, 
    PolyFillType.pftNonZero);

16.11 选择填充规则的建议

场景 推荐规则 原因
简单多边形 EvenOdd 直观,自动处理方向
自相交路径 EvenOdd 自动创建空洞
路径描边 NonZero 重叠保持填充
SVG 渲染 取决于 SVG 属性 保持兼容性
字体轮廓 NonZero TrueType 标准

16.12 本章小结

本章详细分析了 Clipper 的四种填充规则:

  1. EvenOdd:基于交点奇偶性
  2. NonZero:基于缠绕数符号
  3. Positive:只填充正缠绕区域
  4. Negative:只填充负缠绕区域

不同的填充规则会影响:

  • 自相交多边形的处理
  • 重叠区域的处理
  • 边的贡献判断

选择合适的填充规则取决于具体应用场景。


上一章:孔洞检测与处理 | 返回目录 | 下一章:ClipperOffset详解