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

推荐订阅源

M
MIT News - Artificial intelligence
有赞技术团队
有赞技术团队
S
Schneier on Security
aimingoo的专栏
aimingoo的专栏
T
Troy Hunt's Blog
U
Unit 42
Hacker News - Newest:
Hacker News - Newest: "LLM"
V2EX - 技术
V2EX - 技术
T
The Blog of Author Tim Ferriss
V
Visual Studio Blog
H
Heimdal Security Blog
H
Hacker News: Front Page
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
Cloudbric
Cloudbric
Google DeepMind News
Google DeepMind News
C
Cisco Blogs
The Cloudflare Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
MyScale Blog
MyScale Blog
F
Fortinet All Blogs
N
News | PayPal Newsroom
Attack and Defense Labs
Attack and Defense Labs
D
DataBreaches.Net
N
News and Events Feed by Topic
Security Archives - TechRepublic
Security Archives - TechRepublic
Forbes - Security
Forbes - Security
Simon Willison's Weblog
Simon Willison's Weblog
F
Full Disclosure
The Register - Security
The Register - Security
L
LINUX DO - 热门话题
Webroot Blog
Webroot Blog
Google Online Security Blog
Google Online Security Blog
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
S
Security Affairs
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
云风的 BLOG
云风的 BLOG
博客园 - 叶小钗
T
Threatpost
Spread Privacy
Spread Privacy
小众软件
小众软件
AWS News Blog
AWS News Blog
S
Secure Thoughts
S
Security @ Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks

博客园 - 我才是银古

第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
第11章:活动边表 AEL 管理
我才是银古 · 2026-06-24 · via 博客园 - 我才是银古

第11章:活动边表 AEL 管理

11.1 概述

活动边表(Active Edge List, AEL)是 Vatti 裁剪算法的核心数据结构。它存储当前扫描线穿过的所有边,并按 X 坐标排序。本章将深入分析 AEL 的管理机制。

11.2 AEL 的结构

AEL 是一个双向链表,通过 TEdge 的 NextInAEL 和 PrevInAEL 指针连接:

m_ActiveEdges
      │
      ▼
   ┌─────┐    ┌─────┐    ┌─────┐    ┌─────┐
   │ e1  │◄──►│ e2  │◄──►│ e3  │◄──►│ e4  │
   └─────┘    └─────┘    └─────┘    └─────┘
   X=10       X=30       X=50       X=80
   
   按 X 坐标升序排列

11.3 InsertEdgeIntoAEL

将边插入 AEL 并保持 X 坐标排序:

private void InsertEdgeIntoAEL(TEdge edge, TEdge startEdge)
{
    if (m_ActiveEdges == null)
    {
        // AEL 为空,直接设为头
        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;
    }
}

11.3.1 E2InsertsBeforeE1

判断 e2 是否应该插入到 e1 之前:

private bool E2InsertsBeforeE1(TEdge e1, TEdge e2)
{
    if (e2.Curr.X == e1.Curr.X)
    {
        // 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 相同时,比较在某个高度上谁更靠左

11.4 DeleteFromAEL

从 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;  // e 是头节点
    
    // 更新后继节点
    if (AelNext != null)
        AelNext.PrevInAEL = AelPrev;
    
    // 清理 e 的指针
    e.NextInAEL = null;
    e.PrevInAEL = null;
}

11.5 SwapPositionsInAEL

交换 AEL 中两条边的位置:

internal void SwapPositionsInAEL(TEdge edge1, TEdge edge2)
{
    // 检查边是否有效
    if (edge1.NextInAEL == edge1.PrevInAEL ||
        edge2.NextInAEL == edge2.PrevInAEL) return;

    if (edge1.NextInAEL == edge2)
    {
        // edge1 紧跟在 edge2 前面
        //  ... → edge1 → edge2 → ...
        //  变为
        //  ... → edge2 → edge1 → ...
        
        TEdge next = edge2.NextInAEL;
        if (next != null) next.PrevInAEL = edge1;
        
        TEdge prev = edge1.PrevInAEL;
        if (prev != null) prev.NextInAEL = edge2;
        
        edge2.PrevInAEL = prev;
        edge2.NextInAEL = edge1;
        edge1.PrevInAEL = edge2;
        edge1.NextInAEL = next;
    }
    else if (edge2.NextInAEL == edge1)
    {
        // edge2 紧跟在 edge1 前面(对称情况)
        TEdge next = edge1.NextInAEL;
        if (next != null) next.PrevInAEL = edge2;
        
        TEdge prev = edge2.PrevInAEL;
        if (prev != null) prev.NextInAEL = edge1;
        
        edge1.PrevInAEL = prev;
        edge1.NextInAEL = edge2;
        edge2.PrevInAEL = edge1;
        edge2.NextInAEL = next;
    }
    else
    {
        // 两条边不相邻
        TEdge next = edge1.NextInAEL;
        TEdge prev = edge1.PrevInAEL;
        
        edge1.NextInAEL = edge2.NextInAEL;
        if (edge1.NextInAEL != null) 
            edge1.NextInAEL.PrevInAEL = edge1;
        edge1.PrevInAEL = edge2.PrevInAEL;
        if (edge1.PrevInAEL != null) 
            edge1.PrevInAEL.NextInAEL = edge1;
        
        edge2.NextInAEL = next;
        if (edge2.NextInAEL != null) 
            edge2.NextInAEL.PrevInAEL = edge2;
        edge2.PrevInAEL = prev;
        if (edge2.PrevInAEL != null) 
            edge2.PrevInAEL.NextInAEL = edge2;
    }

    // 更新头指针
    if (edge1.PrevInAEL == null) m_ActiveEdges = edge1;
    else if (edge2.PrevInAEL == null) m_ActiveEdges = edge2;
}

11.5.1 交换图解

相邻交换:
Before:  ... ← A → B → ...
After:   ... ← B → A → ...

非相邻交换:
Before:  ... ← A → ... ← B → ...
After:   ... ← B → ... ← A → ...

11.6 边的更新

11.6.1 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);
}

11.6.2 更新图解

Before:
        ●  e.Top
       /
      / e
     /
    ● e.Bot (= e.NextInLML.Bot)
     \
      \ e.NextInLML
       \
        ● e.NextInLML.Top

After (扫描线到达 e.Top):
    e → e.NextInLML
    AEL 中 e 被替换为 e.NextInLML

11.7 ProcessEdgesAtTopOfScanbeam

处理到达扫描线顶部的边:

private void ProcessEdgesAtTopOfScanbeam(cInt topY)
{
    TEdge e = m_ActiveEdges;
    
    while (e != null)
    {
        bool IsMaximaEdge = IsMaxima(e, topY);

        if (IsMaximaEdge)
        {
            TEdge eMaxPair = GetMaximaPairEx(e);
            IsMaximaEdge = (eMaxPair == null || !IsHorizontal(eMaxPair));
        }

        if (IsMaximaEdge)
        {
            // 处理局部极大值
            if (StrictlySimple) InsertMaxima(e.Top.X);
            TEdge ePrev = e.PrevInAEL;
            DoMaxima(e);
            if (ePrev == null) 
                e = m_ActiveEdges;
            else 
                e = ePrev.NextInAEL;
        }
        else
        {
            // 更新边的位置
            if (IsIntermediate(e, topY) && IsHorizontal(e.NextInLML))
            {
                UpdateEdgeIntoAEL(ref e);
                if (e.OutIdx >= 0)
                    AddOutPt(e, e.Bot);
                AddEdgeToSEL(e);
            } 
            else
            {
                e.Curr.X = TopX(e, topY);
                e.Curr.Y = topY;
#if use_xyz
                // 更新 Z 坐标
                if (e.Top.Y == topY) e.Curr.Z = e.Top.Z;
                else if (e.Bot.Y == topY) e.Curr.Z = e.Bot.Z;
                else e.Curr.Z = 0;
#endif
            }

            // 处理 StrictlySimple 模式下的接触点
            if (StrictlySimple)
            {
                TEdge ePrev = e.PrevInAEL;
                if ((e.OutIdx >= 0) && (e.WindDelta != 0) && ePrev != null &&
                    (ePrev.OutIdx >= 0) && (ePrev.Curr.X == e.Curr.X) &&
                    (ePrev.WindDelta != 0))
                {
                    IntPoint ip = new IntPoint(e.Curr);
#if use_xyz
                    SetZ(ref ip, ePrev, e);
#endif
                    OutPt op = AddOutPt(ePrev, ip);
                    OutPt op2 = AddOutPt(e, ip);
                    AddJoin(op, op2, ip);
                }
            }

            e = e.NextInAEL;
        }
    }

    // 处理水平边
    ProcessHorizontals();
    m_Maxima = null;

    // 处理中间顶点
    e = m_ActiveEdges;
    while (e != null)
    {
        if (IsIntermediate(e, topY))
        {
            OutPt op = null;
            if (e.OutIdx >= 0) 
                op = AddOutPt(e, e.Top);
            UpdateEdgeIntoAEL(ref e);

            // 处理共边连接
            TEdge ePrev = e.PrevInAEL;
            TEdge eNext = e.NextInAEL;
            
            if (ePrev != null && ePrev.Curr.X == e.Bot.X &&
                ePrev.Curr.Y == e.Bot.Y && op != null &&
                ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y &&
                SlopesEqual(e.Curr, e.Top, ePrev.Curr, ePrev.Top, m_UseFullRange) &&
                (e.WindDelta != 0) && (ePrev.WindDelta != 0))
            {
                OutPt op2 = AddOutPt(ePrev, e.Bot);
                AddJoin(op, op2, e.Top);
            }
            else if (eNext != null && eNext.Curr.X == e.Bot.X &&
                eNext.Curr.Y == e.Bot.Y && op != null &&
                eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y &&
                SlopesEqual(e.Curr, e.Top, eNext.Curr, eNext.Top, m_UseFullRange) &&
                (e.WindDelta != 0) && (eNext.WindDelta != 0))
            {
                OutPt op2 = AddOutPt(eNext, e.Bot);
                AddJoin(op, op2, e.Top);
            }
        }
        e = e.NextInAEL;
    }
}

11.7.1 IsMaxima

private bool IsMaxima(TEdge e, double Y)
{
    return (e != null && e.Top.Y == Y && e.NextInLML == null);
}

条件:边的顶点在当前扫描线且没有后续段。

11.7.2 IsIntermediate

private bool IsIntermediate(TEdge e, double Y)
{
    return (e.Top.Y == Y && e.NextInLML != null);
}

条件:边的顶点在当前扫描线且有后续段。

11.8 DoMaxima

处理局部极大值:

private void DoMaxima(TEdge e)
{
    TEdge eMaxPair = GetMaximaPairEx(e);
    
    if (eMaxPair == null)
    {
        // 没有配对边(开放路径)
        if (e.OutIdx >= 0)
            AddOutPt(e, e.Top);
        DeleteFromAEL(e);
        return;
    }

    TEdge eNext = e.NextInAEL;
    
    // 处理 e 和 eMaxPair 之间的边
    while (eNext != null && eNext != eMaxPair)
    {
        IntersectEdges(e, eNext, e.Top);
        SwapPositionsInAEL(e, eNext);
        eNext = e.NextInAEL;
    }

    if (e.OutIdx == Unassigned && eMaxPair.OutIdx == Unassigned)
    {
        // 两条边都没有输出
        DeleteFromAEL(e);
        DeleteFromAEL(eMaxPair);
    }
    else if (e.OutIdx >= 0 && eMaxPair.OutIdx >= 0)
    {
        // 两条边都有输出
        if (e.OutIdx >= 0) 
            AddLocalMaxPoly(e, eMaxPair, e.Top);
        DeleteFromAEL(e);
        DeleteFromAEL(eMaxPair);
    }
#if use_lines
    else if (e.WindDelta == 0)
    {
        // 开放路径
        if (e.OutIdx >= 0) 
        {
            AddOutPt(e, e.Top);
            e.OutIdx = Unassigned;
        }
        DeleteFromAEL(e);

        if (eMaxPair.OutIdx >= 0)
        {
            AddOutPt(eMaxPair, e.Top);
            eMaxPair.OutIdx = Unassigned;
        }
        DeleteFromAEL(eMaxPair);
    } 
#endif
    else 
        throw new ClipperException("DoMaxima error");
}

11.9 AEL 状态可视化

扫描过程示例:

Y=0:
  ─────────────────────────────────
  AEL: [e1, e2]
  
          ╱╲
         ╱  ╲
        ╱    ╲
       e1    e2

Y=50:
  ─────────────────────────────────
  AEL: [e1, e3, e4, e2]
  
          ╱╲
         ╱  ╲
      ──╱────╲──
       e1 e3 e4 e2

Y=100:
  ─────────────────────────────────
  AEL: [e1, e2]
  
          ╱╲
      ───╱──╲───
        e1  e2

11.10 本章小结

本章详细分析了 AEL 的管理机制:

  1. 数据结构:双向链表,按 X 坐标排序

  2. 基本操作

    • InsertEdgeIntoAEL:插入边
    • DeleteFromAEL:删除边
    • SwapPositionsInAEL:交换位置
  3. 边更新

    • UpdateEdgeIntoAEL:边转折时更新
    • ProcessEdgesAtTopOfScanbeam:处理顶部事件
  4. 极值处理

    • IsMaxima/IsIntermediate:判断边类型
    • DoMaxima:处理局部极大值

AEL 是 Clipper 算法的核心,正确维护 AEL 是算法正确性的关键。


上一章:布尔运算执行流程 | 返回目录 | 下一章:交点计算与处理