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

推荐订阅源

博客园_首页
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
Y
Y Combinator Blog
美团技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
S
SegmentFault 最新的问题
IT之家
IT之家
Recent Announcements
Recent Announcements
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
T
The Blog of Author Tim Ferriss
Martin Fowler
Martin Fowler
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
U
Unit 42
WordPress大学
WordPress大学
博客园 - Franky
L
LangChain Blog
人人都是产品经理
人人都是产品经理
小众软件
小众软件
博客园 - 叶小钗
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
雷峰网
雷峰网
腾讯CDC
Google DeepMind News
Google DeepMind News
博客园 - 三生石上(FineUI控件)
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Help Net Security
Help Net Security
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Schneier on Security
Schneier on Security
博客园 - 聂微东
A
Arctic Wolf
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
The Exploit Database - CXSecurity.com
C
Cyber Attacks, Cyber Crime and Cyber Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google DeepMind News
Google DeepMind 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
第9章:Active 活动边结构
我才是银古 · 2026-06-23 · via 博客园 - 我才是银古

第9章:Active 活动边结构

9.1 概述

在 Vatti 扫描线算法中,活动边表(Active Edge List, AEL)是核心数据结构之一。Active 类表示当前扫描线穿过的边,这些边按 X 坐标排序,用于确定多边形区域的边界。

9.2 Active 类定义

9.2.1 完整定义

internal class Active
{
    public Point64 bot;          // 边的底部端点
    public Point64 top;          // 边的顶部端点
    public long curX;            // 当前扫描线上的 X 坐标
    
    public double dx;            // 斜率的倒数 (dx/dy)
    
    public int windDx;           // 缠绕方向 (+1 或 -1)
    public int windCnt;          // 缠绕计数
    public int windCnt2;         // 另一多边形的缠绕计数
    
    public OutRec? outrec;       // 输出记录
    
    // 链表指针
    public Active? prevInAEL;    // AEL 中的前一条边
    public Active? nextInAEL;    // AEL 中的后一条边
    public Active? prevInSEL;    // SEL 中的前一条边
    public Active? nextInSEL;    // SEL 中的后一条边
    
    public Active? mergeJump;    // 合并跳转
    
    public Vertex? vertexTop;    // 顶部顶点
    public LocalMinima localMin; // 所属的局部极小值
    
    public bool isLeftBound;     // 是否为左边界
}

9.2.2 成员变量详解

成员 类型 说明
bot Point64 边的下端点(Y值较小)
top Point64 边的上端点(Y值较大)
curX long 当前扫描线 Y 处的 X 坐标
dx double 斜率倒数,用于X坐标插值
windDx int 边的方向:+1向上,-1向下
windCnt int 当前边的缠绕计数
windCnt2 int 另一类型路径的缠绕计数
outrec OutRec? 关联的输出多边形记录

9.3 斜率倒数 (dx)

9.3.1 计算方法

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double GetDx(Point64 pt1, Point64 pt2)
{
    double dy = pt2.Y - pt1.Y;
    if (dy != 0)
        return (pt2.X - pt1.X) / dy;
    else if (pt2.X > pt1.X)
        return double.MaxValue;  // 向右的水平边
    else
        return double.MinValue;  // 向左的水平边
}

9.3.2 几何意义

           │
    top ●──┼──
          ╲│
           ╲   dx = ΔX/ΔY
            ╲
         ●───╲────
    bot     │╲
            │ ╲
           Y轴
  • dx > 0:边向右倾斜(随Y增加,X增加)
  • dx < 0:边向左倾斜(随Y增加,X减少)
  • dx = 0:垂直边
  • dx = ±MaxValue:水平边

9.3.3 X 坐标插值

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static long TopX(Active ae, long currentY)
{
    // 计算边在 currentY 处的 X 坐标
    if (currentY == ae.top.Y || ae.top.X == ae.bot.X)
        return ae.top.X;
    else if (currentY == ae.bot.Y)
        return ae.bot.X;
    else
        return ae.bot.X + (long)Math.Round(ae.dx * (currentY - ae.bot.Y));
}

9.4 活动边表 (AEL)

9.4.1 AEL 的结构

AEL 是一个双向链表,按 X 坐标排序:

AEL 头 ←→ Active1 ←→ Active2 ←→ Active3 ←→ ... ←→ null
          (X=10)     (X=30)      (X=50)

9.4.2 链表指针

public Active? prevInAEL;    // 前一条边
public Active? nextInAEL;    // 后一条边

9.4.3 插入到 AEL

private void InsertLeftEdge(Active ae)
{
    Active ae2;
    if (_actives == null)
    {
        // AEL 为空,直接作为头
        ae.prevInAEL = null;
        ae.nextInAEL = null;
        _actives = ae;
    }
    else if (!IsValidAelOrder(_actives, ae))
    {
        // 插入到头部
        ae.prevInAEL = null;
        ae.nextInAEL = _actives;
        _actives.prevInAEL = ae;
        _actives = ae;
    }
    else
    {
        // 找到正确位置
        ae2 = _actives;
        while (ae2.nextInAEL != null && IsValidAelOrder(ae2.nextInAEL, ae))
            ae2 = ae2.nextInAEL;
        
        // 在 ae2 后插入
        ae.nextInAEL = ae2.nextInAEL;
        if (ae2.nextInAEL != null)
            ae2.nextInAEL.prevInAEL = ae;
        ae.prevInAEL = ae2;
        ae2.nextInAEL = ae;
    }
}

9.4.4 排序规则

private static bool IsValidAelOrder(Active res, Active cmp)
{
    // 首先比较当前 X 坐标
    if (res.curX != cmp.curX)
        return res.curX < cmp.curX;
    
    // X 相等时,比较斜率(决定未来的相对位置)
    // 使用叉积比较斜率
    Point64 d = new Point64(cmp.top.X - cmp.bot.X, cmp.top.Y - cmp.bot.Y);
    
    // 计算叉积符号
    return InternalClipper.CrossProduct(
        new Point64(0, 0), 
        new Point64(res.top.X - res.bot.X, res.top.Y - res.bot.Y),
        d) < 0;
}

9.5 缠绕计数 (Winding Count)

9.5.1 windDx 方向

public int windDx;  // +1 或 -1
  • windDx = +1:边从下往上(随扫描线前进,边向上)
  • windDx = -1:边从上往下(随扫描线前进,边向下)
   ○─────────○
   │         │
   │  内部   │ ← windDx = +1(左边)
   │         │ → windDx = -1(右边)
   │         │
   ○─────────○

9.5.2 windCnt 累积

当扫描线从左到右穿过边时:

private void SetWindCountForClosedPathEdge(Active ae)
{
    Active? ae2 = ae.prevInAEL;
    
    // 找到同类型的前一条边
    while (ae2 != null && 
           (GetPolyType(ae2) != GetPolyType(ae) || IsOpen(ae2)))
    {
        ae2 = ae2.prevInAEL;
    }
    
    if (ae2 == null)
    {
        // 没有前一条同类型边
        ae.windCnt = ae.windDx;
    }
    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;
    }
}

9.5.3 填充判断

private bool IsContributingClosed(Active ae)
{
    switch (_fillrule)
    {
        case FillRule.EvenOdd:
            // 奇数穿越为内部
            return (ae.windCnt & 1) != 0;
            
        case FillRule.NonZero:
            // 非零为内部
            return ae.windCnt != 0;
            
        case FillRule.Positive:
            // 正数为内部
            return ae.windCnt > 0;
            
        case FillRule.Negative:
            // 负数为内部
            return ae.windCnt < 0;
    }
    return false;
}

9.6 边的生命周期

9.6.1 创建 Active

从 LocalMinima 创建边:

private Active CreateActive(Vertex v, bool goingUp, LocalMinima locMin)
{
    Active ae = new Active();
    ae.bot = v.pt;
    ae.vertexTop = goingUp ? v.next! : v.prev!;
    ae.top = ae.vertexTop.pt;
    ae.curX = ae.bot.X;
    ae.localMin = locMin;
    ae.isLeftBound = goingUp;
    ae.windDx = goingUp ? 1 : -1;
    ae.dx = GetDx(ae.bot, ae.top);
    
    return ae;
}

9.6.2 更新边的顶点

当扫描到边的顶点时,需要更新到下一条边:

private void UpdateEdgeIntoAEL(ref Active ae)
{
    ae.bot = ae.top;
    ae.vertexTop = ae.isLeftBound ? 
        ae.vertexTop!.next : ae.vertexTop!.prev;
    ae.top = ae.vertexTop!.pt;
    ae.curX = ae.bot.X;
    ae.dx = GetDx(ae.bot, ae.top);
    
    // 重新插入到正确位置
    if (!IsHorizontal(ae))
        AddScanline(ae.top.Y);
    
    // 处理水平边...
}

9.6.3 删除边

当边到达局部最大值时删除:

private void DeleteFromAEL(Active ae)
{
    Active? prev = ae.prevInAEL;
    Active? next = ae.nextInAEL;
    
    if (prev != null)
        prev.nextInAEL = next;
    else
        _actives = next;
    
    if (next != null)
        next.prevInAEL = prev;
    
    // 清理引用
    ae.prevInAEL = null;
    ae.nextInAEL = null;
}

9.7 边交换

9.7.1 为什么需要交换

当两条边相交时,它们的 X 顺序会改变:

     扫描线1:  边A ─── 边B
                  ╲   ╱
                   ╲ ╱
                    ╳  ← 交点
                   ╱ ╲
                  ╱   ╲
     扫描线2:  边B ─── 边A

9.7.2 SwapPositionsInAEL

private void SwapPositionsInAEL(Active ae1, Active ae2)
{
    // 确保 ae1 在 ae2 之前
    if (ae1.nextInAEL != ae2)
    {
        // 不相邻,需要特殊处理
        Active? prev1 = ae1.prevInAEL;
        Active? next1 = ae1.nextInAEL;
        Active? prev2 = ae2.prevInAEL;
        Active? next2 = ae2.nextInAEL;
        
        // 更新四个链接...
    }
    else
    {
        // 相邻情况
        Active? prev = ae1.prevInAEL;
        Active? next = ae2.nextInAEL;
        
        if (prev != null) prev.nextInAEL = ae2;
        else _actives = ae2;
        
        ae2.prevInAEL = prev;
        ae2.nextInAEL = ae1;
        ae1.prevInAEL = ae2;
        ae1.nextInAEL = next;
        
        if (next != null) next.prevInAEL = ae1;
    }
}

9.8 排序边列表 (SEL)

9.8.1 SEL 的作用

SEL(Sorted Edge List)用于管理当前扫描线上需要处理的边:

public Active? prevInSEL;
public Active? nextInSEL;

9.8.2 SEL vs AEL

特性 AEL SEL
用途 存储所有活动边 临时排序用
排序依据 X 坐标 处理优先级
生命周期 长期 单次扫描线处理

9.9 水平边处理

9.9.1 水平边的特殊性

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsHorizontal(Active ae)
{
    return ae.top.Y == ae.bot.Y;
}

水平边的 dxMaxValueMinValue,需要特殊处理。

9.9.2 水平边结构

internal class HorzSegment
{
    public OutPt? leftOp;
    public OutPt? rightOp;
    public bool leftToRight;
}

internal class HorzJoin
{
    public OutPt? op1;
    public OutPt? op2;
}

9.10 isLeftBound 标志

9.10.1 定义

public bool isLeftBound;

标识边是局部极小值的左边界还是右边界:

              ╱╲
             ╱  ╲
            ╱    ╲
      左边界      右边界
       ↗          ↖
        ╲        ╱
         ╲      ╱
          ╲    ╱
           ╲  ╱
            ╲╱  ← 局部极小值

9.10.2 作用

  • 确定边的遍历方向
  • 配对左右边界构建输出多边形

9.11 mergeJump 字段

9.11.1 用途

public Active? mergeJump;

用于优化边的合并操作,跳过已处理的边。

9.11.2 使用场景

当多条边在同一点汇合时,使用 mergeJump 快速找到需要处理的边。

9.12 调试辅助

9.12.1 ToString 方法

#if DEBUG
public override string ToString()
{
    return $"Active: ({bot.X},{bot.Y})->({top.X},{top.Y}) " +
           $"curX={curX} dx={dx:F2} windCnt={windCnt}";
}
#endif

9.12.2 遍历 AEL

internal void DumpAEL()
{
    Active? ae = _actives;
    Console.WriteLine("=== AEL ===");
    while (ae != null)
    {
        Console.WriteLine(ae.ToString());
        ae = ae.nextInAEL;
    }
}

9.13 本章小结

Active 类是 Clipper2 扫描线算法的核心结构:

  1. 几何信息bottopcurXdx
  2. 缠绕信息windDxwindCntwindCnt2
  3. 链表指针prevInAELnextInAELprevInSELnextInSEL
  4. 输出关联outrec
  5. 顶点信息vertexToplocalMinisLeftBound

理解 Active 的结构和操作是理解 Clipper2 扫描线算法的关键。


上一章:ClipperBase基类详解 | 返回目录 | 下一章:Vertex与LocalMinima