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

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

博客园 - 我才是银古

第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# 源码解读教程 第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
第19章:PolyTree 多边形树结构
我才是银古 · 2026-04-11 · via 博客园 - 我才是银古

第19章:PolyTree 多边形树结构

19.1 概述

PolyTree 是 Clipper2 中用于表示具有层次结构的多边形的数据结构。它能够正确表示外轮廓和孔洞的嵌套关系,这在许多应用场景中非常重要。

19.2 为什么需要 PolyTree

19.2.1 Paths64 的局限

// Paths64 只是路径的列表,没有层次信息
Paths64 paths = new Paths64 {
    outerPolygon,   // 外轮廓
    hole1,          // 孔洞1
    hole2,          // 孔洞2
    island          // 孔洞中的岛屿
};

// 问题:无法知道哪个是哪个的孔洞

19.2.2 PolyTree 的优势

PolyTree 层次结构:

Root (虚拟根节点)
├── OuterPolygon1 (外轮廓)
│   ├── Hole1 (孔洞)
│   │   └── Island1 (孔洞中的岛屿)
│   └── Hole2 (孔洞)
└── OuterPolygon2 (另一个外轮廓)
    └── Hole3 (孔洞)

19.3 PolyPath64 类

19.3.1 类定义

public class PolyPath64 : IEnumerable<PolyPath64>
{
    internal PolyPath64? _parent;
    internal readonly List<PolyPath64> _childs = new List<PolyPath64>();
    public Path64? Polygon { get; set; }
    
    public PolyPath64(PolyPath64? parent = null)
    {
        _parent = parent;
    }
    
    public PolyPath64? Parent => _parent;
    public int Count => _childs.Count;
    public bool IsHole => _parent != null && !_parent.IsHole;
    
    public PolyPath64 this[int index] => _childs[index];
    
    public IEnumerator<PolyPath64> GetEnumerator()
    {
        return _childs.GetEnumerator();
    }
}

19.3.2 成员说明

成员 类型 说明
_parent PolyPath64? 父节点
_childs List<PolyPath64> 子节点列表
Polygon Path64? 多边形路径
IsHole bool 是否为孔洞

19.3.3 IsHole 判断

public bool IsHole
{
    get
    {
        // 根节点不是孔洞
        if (_parent == null) return false;
        
        // 如果父节点是孔洞,则自己不是孔洞
        // 如果父节点不是孔洞,则自己是孔洞
        return !_parent.IsHole;
    }
}

// 层次关系:
// Level 0 (Root):     不是孔洞(虚拟根)
// Level 1 (外轮廓):   不是孔洞
// Level 2 (孔洞):     是孔洞
// Level 3 (岛屿):     不是孔洞
// Level 4 (岛中孔):   是孔洞
// ...

19.4 PolyTree64 类

19.4.1 类定义

public class PolyTree64 : PolyPath64
{
    public PolyTree64() : base(null)
    {
        // PolyTree64 是特殊的根节点
        // 它没有父节点,也没有 Polygon
    }
    
    public void Clear()
    {
        _childs.Clear();
    }
}

19.4.2 与 PolyPath64 的关系

PolyTree64 (根节点,无 Polygon)
    ↓ 继承
PolyPath64 (普通节点,有 Polygon)

19.5 PolyPathD 和 PolyTreeD

19.5.1 浮点版本

public class PolyPathD : IEnumerable<PolyPathD>
{
    internal PolyPathD? _parent;
    internal readonly List<PolyPathD> _childs = new List<PolyPathD>();
    public PathD? Polygon { get; set; }
    public double Scale { get; internal set; }
    
    public bool IsHole => _parent != null && !_parent.IsHole;
    
    // ... 类似 PolyPath64
}

public class PolyTreeD : PolyPathD
{
    public PolyTreeD() : base(null)
    {
        Scale = 1.0;
    }
}

19.5.2 Scale 属性

// 用于 ClipperD 的精度转换
public double Scale { get; internal set; }

// 设置后会传递给所有子节点
internal void SetScale(double scale)
{
    Scale = scale;
    foreach (var child in _childs)
    {
        (child as PolyPathD)?.SetScale(scale);
    }
}

19.6 添加子节点

19.6.1 AddChild 方法

public PolyPath64 AddChild(Path64 path)
{
    PolyPath64 child = new PolyPath64(this);
    child.Polygon = path;
    _childs.Add(child);
    return child;
}

19.6.2 使用示例

PolyTree64 tree = new PolyTree64();

// 添加外轮廓
PolyPath64 outer = tree.AddChild(outerPath);

// 添加孔洞到外轮廓
PolyPath64 hole = outer.AddChild(holePath);

// 添加岛屿到孔洞
PolyPath64 island = hole.AddChild(islandPath);

19.7 遍历 PolyTree

19.7.1 IEnumerable 实现

// PolyPath64 实现了 IEnumerable<PolyPath64>
foreach (PolyPath64 child in polyTree)
{
    ProcessPath(child.Polygon);
    
    // 递归处理子节点
    foreach (PolyPath64 grandChild in child)
    {
        ProcessPath(grandChild.Polygon);
    }
}

19.7.2 递归遍历

void TraversePolyTree(PolyPath64 node, int level = 0)
{
    // 处理当前节点
    if (node.Polygon != null)
    {
        string indent = new string(' ', level * 2);
        string type = node.IsHole ? "Hole" : "Polygon";
        Console.WriteLine($"{indent}{type}: {node.Polygon.Count} points");
    }
    
    // 递归处理子节点
    foreach (PolyPath64 child in node)
    {
        TraversePolyTree(child, level + 1);
    }
}

19.7.3 收集所有路径

void CollectPaths(PolyPath64 node, Paths64 outerPaths, Paths64 holes)
{
    if (node.Polygon != null)
    {
        if (node.IsHole)
            holes.Add(node.Polygon);
        else
            outerPaths.Add(node.Polygon);
    }
    
    foreach (PolyPath64 child in node)
    {
        CollectPaths(child, outerPaths, holes);
    }
}

19.8 在 Clipper64 中使用

19.8.1 Execute 输出到 PolyTree

Clipper64 clipper = new Clipper64();
clipper.AddSubject(subjects);
clipper.AddClip(clips);

PolyTree64 tree = new PolyTree64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, tree);

// 现在 tree 包含了层次化的结果

19.8.2 与 Paths64 对比

// 方式1:输出到 Paths64(丢失层次信息)
Paths64 solution = new Paths64();
clipper.Execute(ClipType.Union, FillRule.NonZero, solution);

// 方式2:输出到 PolyTree64(保留层次信息)
PolyTree64 tree = new PolyTree64();
clipper.Execute(ClipType.Union, FillRule.NonZero, tree);

19.9 PolyTree 转 Paths

19.9.1 PolyTreeToPaths64

public static Paths64 PolyTreeToPaths64(PolyTree64 polytree)
{
    Paths64 result = new Paths64();
    AddPolyPath(polytree, result);
    return result;
}

private static void AddPolyPath(PolyPath64 polypath, Paths64 paths)
{
    if (polypath.Polygon != null)
        paths.Add(polypath.Polygon);
    
    foreach (PolyPath64 child in polypath)
    {
        AddPolyPath(child, paths);
    }
}

19.9.2 静态方法

// Clipper 类中的便捷方法
Paths64 paths = Clipper.PolyTreeToPaths64(tree);
PathsD pathsD = Clipper.PolyTreeToPathsD(treeD);

19.10 实际应用

19.10.1 绘制带孔洞的多边形

void DrawPolyTree(PolyTree64 tree, Graphics g)
{
    foreach (PolyPath64 outer in tree)
    {
        // 创建带孔洞的 GraphicsPath
        GraphicsPath gp = new GraphicsPath();
        
        // 添加外轮廓
        gp.AddPolygon(ToPointArray(outer.Polygon));
        
        // 添加孔洞
        foreach (PolyPath64 hole in outer)
        {
            gp.AddPolygon(ToPointArray(hole.Polygon));
            
            // 递归处理孔洞中的岛屿
            DrawIslands(hole, gp);
        }
        
        // 设置填充规则
        gp.FillMode = FillMode.Alternate;
        
        // 绘制
        g.FillPath(brush, gp);
    }
}

19.10.2 计算嵌套面积

double CalculateNetArea(PolyPath64 node)
{
    double area = 0;
    
    if (node.Polygon != null)
    {
        double pathArea = Math.Abs(Clipper.Area(node.Polygon));
        
        if (node.IsHole)
            area -= pathArea;
        else
            area += pathArea;
    }
    
    // 递归处理子节点
    foreach (PolyPath64 child in node)
    {
        area += CalculateNetArea(child);
    }
    
    return area;
}

19.10.3 SVG 导出

string ExportToSvg(PolyTree64 tree)
{
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("<svg xmlns=\"http://www.w3.org/2000/svg\">");
    
    foreach (PolyPath64 outer in tree)
    {
        sb.Append("<path d=\"");
        
        // 外轮廓
        AppendPathData(sb, outer.Polygon, false);
        
        // 孔洞
        foreach (PolyPath64 hole in outer)
        {
            AppendPathData(sb, hole.Polygon, true);
        }
        
        sb.AppendLine("\" fill=\"blue\" fill-rule=\"evenodd\" />");
    }
    
    sb.AppendLine("</svg>");
    return sb.ToString();
}

19.11 构建 PolyTree

19.11.1 内部构建过程

// 在 ClipperBase 中
private void BuildTree(PolyTree64 polytree, Paths64 openPaths)
{
    polytree.Clear();
    
    // 处理所有 OutRec
    foreach (OutRec outrec in _outrecList)
    {
        if (outrec.pts == null) continue;
        
        // 构建路径
        Path64 path = BuildPath(outrec.pts, false, false);
        if (path == null || path.Count < 3) continue;
        
        // 确定父节点
        PolyPath64 parent = GetParent(outrec, polytree);
        
        // 添加到树中
        outrec.polypath = parent.AddChild(path);
    }
}

private PolyPath64 GetParent(OutRec outrec, PolyTree64 tree)
{
    OutRec? owner = outrec.owner;
    
    while (owner != null && owner.polypath == null)
        owner = owner.owner;
    
    if (owner == null)
        return tree;
    else
        return owner.polypath!;
}

19.11.2 Owner 关系

// OutRec 的 owner 字段决定了层次关系
// owner 链形成了包含关系

OutRec A (owner = null)     → 属于 PolyTree 根
OutRec B (owner = A)        → B 是 A 的孔洞
OutRec C (owner = B)        → C 是 B 中的岛屿

19.12 调试与可视化

19.12.1 打印树结构

void PrintTree(PolyPath64 node, int depth = 0)
{
    string indent = new string(' ', depth * 2);
    
    if (node.Polygon != null)
    {
        double area = Clipper.Area(node.Polygon);
        Console.WriteLine($"{indent}{(node.IsHole ? "Hole" : "Outer")}: " +
                          $"{node.Polygon.Count} pts, area={area:F1}");
    }
    else
    {
        Console.WriteLine($"{indent}Root");
    }
    
    foreach (var child in node)
    {
        PrintTree(child, depth + 1);
    }
}

19.12.2 验证树结构

bool ValidateTree(PolyPath64 node)
{
    foreach (var child in node)
    {
        // 检查父引用
        if (child._parent != node)
        {
            Console.WriteLine("Invalid parent reference!");
            return false;
        }
        
        // 检查孔洞状态交替
        if (child.Polygon != null && node.Polygon != null)
        {
            if (child.IsHole == node.IsHole)
            {
                Console.WriteLine("Invalid hole/polygon alternation!");
                return false;
            }
        }
        
        // 递归检查
        if (!ValidateTree(child))
            return false;
    }
    
    return true;
}

19.13 本章小结

PolyTree 是 Clipper2 中表示层次化多边形的重要结构:

  1. 层次关系:正确表示外轮廓、孔洞、岛屿的嵌套
  2. PolyPath64:树的节点,包含路径和子节点
  3. PolyTree64:根节点,没有路径
  4. IsHole 属性:自动计算是否为孔洞
  5. 遍历方式:IEnumerable 支持 foreach
  6. 应用场景:绘图、SVG 导出、面积计算

当需要保留多边形的层次结构时,使用 PolyTree 而不是 Paths64。


上一章:Minkowski和与差 | 返回目录 | 下一章:实际应用与最佳实践