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

推荐订阅源

月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
IT之家
IT之家
Cyberwarzone
Cyberwarzone
T
Troy Hunt's Blog
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
T
Threat Research - Cisco Blogs
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
G
GRAHAM CLULEY
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 叶小钗
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes
The Hacker News
The Hacker News
Jina AI
Jina AI
T
Tor Project blog
V
Vulnerabilities – Threatpost
酷 壳 – CoolShell
酷 壳 – CoolShell
Spread Privacy
Spread Privacy
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 司徒正美
V2EX - 技术
V2EX - 技术
I
Intezer
The Cloudflare Blog
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
博客园 - 【当耐特】
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
Lohrmann on Cybersecurity
Scott Helme
Scott Helme
Google Online Security Blog
Google Online Security Blog
量子位
The Last Watchdog
The Last Watchdog
AI
AI
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security Affairs
P
Palo Alto Networks Blog
S
Secure Thoughts
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Attack and Defense Labs
Attack and Defense Labs

博客园 - 我才是银古

第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
第7章:边缘数据结构 TEdge 深入分析
我才是银古 · 2026-06-24 · via 博客园 - 我才是银古

第7章:边缘数据结构 TEdge 深入分析

7.1 概述

TEdge 是 Clipper1 中最核心的内部数据结构,代表多边形的一条边。Vatti 裁剪算法的所有操作都围绕边展开:边的插入、删除、交点计算、输出构建等。深入理解 TEdge 的设计对于理解 Clipper 算法至关重要。

7.2 TEdge 类定义

internal class TEdge 
{
    internal IntPoint Bot;        // 底部端点
    internal IntPoint Curr;       // 当前点(随扫描线更新)
    internal IntPoint Top;        // 顶部端点
    internal IntPoint Delta;      // 增量 (Top - Bot)
    internal double Dx;           // 斜率 (Delta.X / Delta.Y)
    internal PolyType PolyTyp;    // 多边形类型
    internal EdgeSide Side;       // 当前在输出多边形的哪一侧
    internal int WindDelta;       // 缠绕数变化量
    internal int WindCnt;         // 缠绕计数
    internal int WindCnt2;        // 对方多边形类型的缠绕计数
    internal int OutIdx;          // 输出多边形索引
    internal TEdge Next;          // 下一条边(多边形顺序)
    internal TEdge Prev;          // 上一条边(多边形顺序)
    internal TEdge NextInLML;     // LML 中的下一条边
    internal TEdge NextInAEL;     // AEL 中的下一条边
    internal TEdge PrevInAEL;     // AEL 中的上一条边
    internal TEdge NextInSEL;     // SEL 中的下一条边
    internal TEdge PrevInSEL;     // SEL 中的上一条边
}

7.3 坐标相关字段

7.3.1 Bot、Top、Curr

      Top (X_top, Y_top)
         ●
        /
       /
      /   ← Curr (当前扫描线位置)
     /
    /
   ●
Bot (X_bot, Y_bot)

约定:Bot.Y >= Top.Y(Clipper Y轴向上)

字段说明

字段 说明
Bot 边的底部端点(Y 值较大)
Top 边的顶部端点(Y 值较小)
Curr 当前扫描线与边的交点,随扫描进行更新

7.3.2 Delta 和 Dx

Delta.X = Top.X - Bot.X;
Delta.Y = Top.Y - Bot.Y;  // 总是 <= 0(因为 Bot.Y >= Top.Y)
Dx = Delta.X / Delta.Y;    // 水平边时设为 horizontal 常量

Dx 的物理意义

  • 表示 Y 坐标每减少 1 时,X 坐标变化量
  • 用于计算扫描线与边的交点

7.3.3 Curr 的更新

在处理每个扫描线时,需要更新边的当前 X 坐标:

// 在 TopX 方法中计算
private static cInt TopX(TEdge edge, cInt currentY)
{
    if (currentY == edge.Top.Y)
        return edge.Top.X;
    return edge.Bot.X + Round(edge.Dx * (currentY - edge.Bot.Y));
}

// 在 ProcessEdgesAtTopOfScanbeam 中更新
e.Curr.X = TopX(e, topY);
e.Curr.Y = topY;

7.4 多边形类型与方向

7.4.1 PolyTyp

internal PolyType PolyTyp;  // ptSubject 或 ptClip

标识边属于主体多边形还是裁剪多边形。

7.4.2 Side

internal EdgeSide Side;  // esLeft 或 esRight

标识边当前位于输出多边形的左侧还是右侧。

输出多边形示意:
        Top
        /\
       /  \
 Left /    \ Right
     /      \
    /________\
       Bot
       
esLeft: 边在输出多边形的左侧
esRight: 边在输出多边形的右侧

7.4.3 WindDelta

internal int WindDelta;  // 1, -1, 或 0

含义:从下往上穿过这条边时,缠绕数的变化量。

说明
1 向上穿过时缠绕数 +1
-1 向上穿过时缠绕数 -1
0 开放路径的边

7.4.4 WindCnt 和 WindCnt2

internal int WindCnt;   // 同类型多边形的缠绕计数
internal int WindCnt2;  // 不同类型多边形的缠绕计数

缠绕计数的作用

  • 确定点是否在多边形内部
  • 实现不同的填充规则

7.5 链表指针

TEdge 参与多个链表,每个链表有不同的用途:

7.5.1 多边形链表(Next/Prev)

internal TEdge Next;  // 多边形中的下一条边
internal TEdge Prev;  // 多边形中的上一条边

形成闭合的双向循环链表,表示原始多边形的边:

   e0 ←→ e1 ←→ e2 ←→ e3 ←→ e0
   (循环链表)

7.5.2 LML 链表(NextInLML)

internal TEdge NextInLML;  // Local Minima List 中的后续边

LML(Local Minima List):从局部极小值点延伸的边序列。

                 ●──────●
                / NextInLML
               /
              ●  ← 局部极小值
             /
NextInLML   /
           /
          ●

7.5.3 AEL 链表(NextInAEL/PrevInAEL)

internal TEdge NextInAEL;  // 活动边表中的下一条边
internal TEdge PrevInAEL;  // 活动边表中的上一条边

AEL(Active Edge List):当前扫描线穿过的所有边,按 X 坐标排序。

扫描线: ─────────────────────────────────
           │    │       │      │
          e1   e2      e3     e4
           
AEL: e1 ←→ e2 ←→ e3 ←→ e4

7.5.4 SEL 链表(NextInSEL/PrevInSEL)

internal TEdge NextInSEL;  // 排序边表中的下一条边
internal TEdge PrevInSEL;  // 排序边表中的上一条边

SEL(Sorted Edge List):用于水平边处理和交点排序。

7.5.5 OutIdx

internal int OutIdx;  // 输出多边形记录的索引
含义
Unassigned (-1) 边尚未与输出多边形关联
Skip (-2) 边应被跳过(开放路径的终点边)
>= 0 关联的 OutRec 索引

7.6 边的生命周期

7.6.1 创建阶段(AddPath)

// 1. 创建边对象
List<TEdge> edges = new List<TEdge>(highI + 1);
for (int i = 0; i <= highI; i++) edges.Add(new TEdge());

// 2. 初始化多边形链表
InitEdge(edges[0], edges[1], edges[highI], pg[0]);

// 3. 设置方向和斜率
InitEdge2(E, polyType);

7.6.2 进入 AEL

当扫描线到达边的底部(局部极小值)时,边被插入 AEL:

private void InsertEdgeIntoAEL(TEdge edge, TEdge startEdge)
{
    if (m_ActiveEdges == null)
    {
        edge.PrevInAEL = null;
        edge.NextInAEL = null;
        m_ActiveEdges = edge;
    }
    else if (startEdge == null && E2InsertsBeforeE1(m_ActiveEdges, edge))
    {
        // 插入到 AEL 头部
        edge.PrevInAEL = null;
        edge.NextInAEL = m_ActiveEdges;
        m_ActiveEdges.PrevInAEL = edge;
        m_ActiveEdges = edge;
    }
    else
    {
        // 找到正确位置插入
        if (startEdge == null) startEdge = m_ActiveEdges;
        while (startEdge.NextInAEL != null &&
               !E2InsertsBeforeE1(startEdge.NextInAEL, edge))
            startEdge = startEdge.NextInAEL;
        edge.NextInAEL = startEdge.NextInAEL;
        if (startEdge.NextInAEL != null) 
            startEdge.NextInAEL.PrevInAEL = edge;
        edge.PrevInAEL = startEdge;
        startEdge.NextInAEL = edge;
    }
}

7.6.3 在 AEL 中更新

// 交换两条边的位置
internal void SwapPositionsInAEL(TEdge edge1, TEdge edge2)
{
    // 复杂的链表指针操作...
}

// 从 AEL 删除
internal void DeleteFromAEL(TEdge e)
{
    TEdge AelPrev = e.PrevInAEL;
    TEdge AelNext = e.NextInAEL;
    if (AelPrev == null && AelNext == null && (e != m_ActiveEdges))
        return; // 已删除
    if (AelPrev != null)
        AelPrev.NextInAEL = AelNext;
    else 
        m_ActiveEdges = AelNext;
    if (AelNext != null)
        AelNext.PrevInAEL = AelPrev;
    e.NextInAEL = null;
    e.PrevInAEL = null;
}

7.6.4 边的延续(UpdateEdgeIntoAEL)

当边到达中间转折点时,更新为下一段:

internal void UpdateEdgeIntoAEL(ref TEdge e)
{
    if (e.NextInLML == null)
        throw new ClipperException("UpdateEdgeIntoAEL: invalid call");
    
    TEdge AelPrev = e.PrevInAEL;
    TEdge AelNext = e.NextInAEL;
    e.NextInLML.OutIdx = e.OutIdx;
    
    // 更新 AEL 指针
    if (AelPrev != null)
        AelPrev.NextInAEL = e.NextInLML;
    else 
        m_ActiveEdges = e.NextInLML;
    if (AelNext != null)
        AelNext.PrevInAEL = e.NextInLML;
    
    e.NextInLML.Side = e.Side;
    e.NextInLML.WindDelta = e.WindDelta;
    e.NextInLML.WindCnt = e.WindCnt;
    e.NextInLML.WindCnt2 = e.WindCnt2;
    e = e.NextInLML;
    e.Curr = e.Bot;
    e.PrevInAEL = AelPrev;
    e.NextInAEL = AelNext;
    
    if (!IsHorizontal(e)) InsertScanbeam(e.Top.Y);
}

7.6.5 离开 AEL

当扫描线到达边的顶部(局部极大值)时:

// 在 DoMaxima 中
DeleteFromAEL(e);
DeleteFromAEL(eMaxPair);

7.7 边的排序

7.7.1 E2InsertsBeforeE1

private bool E2InsertsBeforeE1(TEdge e1, TEdge e2)
{
    if (e2.Curr.X == e1.Curr.X)
    {
        if (e2.Top.Y > e1.Top.Y)
            return e2.Top.X < TopX(e1, e2.Top.Y);
        else 
            return e1.Top.X > TopX(e2, e1.Top.Y);
    }
    else 
        return e2.Curr.X < e1.Curr.X;
}

排序规则

  1. 首先按当前 X 坐标排序
  2. X 相同时,按斜率方向排序(确保正确的左右关系)

7.7.2 IsHorizontal

internal static bool IsHorizontal(TEdge e)
{
    return e.Delta.Y == 0;
}

水平边需要特殊处理,因为它们与扫描线平行。

7.8 边对的查找

7.8.1 GetMaximaPair

找到在同一个局部极大值点的配对边:

internal TEdge GetMaximaPair(TEdge e)
{
    if ((e.Next.Top == e.Top) && e.Next.NextInLML == null)
        return e.Next;
    else if ((e.Prev.Top == e.Top) && e.Prev.NextInLML == null)
        return e.Prev;
    else 
        return null;
}

7.8.2 GetMaximaPairEx

internal TEdge GetMaximaPairEx(TEdge e)
{
    TEdge result = GetMaximaPair(e);
    if (result == null || result.OutIdx == Skip ||
        ((result.NextInAEL == result.PrevInAEL) && !IsHorizontal(result))) 
        return null;
    return result;
}

额外检查确保配对边仍在 AEL 中。

7.9 边的可视化

多边形:
        ●(100, 200)──────●(300, 200)
       /                  \
      /                    \
     /                      \
    ●(0, 100)              ●(400, 100)
     \                      /
      \                    /
       \                  /
        ●(100, 0)────────●(300, 0)

边分解:
e0: (100,0) → (0,100)   Bot=(100,0), Top=(0,100)
e1: (0,100) → (100,200) Bot=(0,100), Top=(100,200)
e2: (100,200) → (300,200) 水平边
e3: (300,200) → (400,100) Bot=(400,100), Top=(300,200)
e4: (400,100) → (300,0)  Bot=(300,0), Top=(400,100)
e5: (300,0) → (100,0)    水平边

扫描过程:
Y=0:   AEL = [e0, e5, e4]
Y=100: AEL = [e1, e3]
Y=200: 处理完成

7.10 本章小结

TEdge 是 Clipper 算法的核心数据结构:

  1. 几何信息:Bot、Top、Curr、Delta、Dx 描述边的位置和方向

  2. 多边形属性:PolyTyp、Side、WindDelta、WindCnt 用于布尔运算

  3. 链表关系

    • Next/Prev:原始多边形结构
    • NextInLML:局部极小值延伸
    • NextInAEL/PrevInAEL:活动边表
    • NextInSEL/PrevInSEL:排序边表
  4. 生命周期:从创建、插入 AEL、更新、到删除的完整过程

理解 TEdge 的设计是理解 Clipper 算法的关键。


上一章:ClipperBase基类详解 | 返回目录 | 下一章:局部极小值与扫描线