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

推荐订阅源

Project Zero
Project Zero
Security Latest
Security Latest
G
GRAHAM CLULEY
C
CXSECURITY Database RSS Feed - CXSecurity.com
云风的 BLOG
云风的 BLOG
月光博客
月光博客
V
Visual Studio Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
宝玉的分享
宝玉的分享
阮一峰的网络日志
阮一峰的网络日志
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
Attack and Defense Labs
Attack and Defense Labs
罗磊的独立博客
D
DataBreaches.Net
TaoSecurity Blog
TaoSecurity Blog
T
Threatpost
S
Secure Thoughts
T
The Exploit Database - CXSecurity.com
P
Palo Alto Networks Blog
Cisco Talos Blog
Cisco Talos Blog
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
NISL@THU
NISL@THU
Spread Privacy
Spread Privacy
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
PCI Perspectives
PCI Perspectives
P
Proofpoint News Feed
Google DeepMind News
Google DeepMind News
V
V2EX
WordPress大学
WordPress大学
Recorded Future
Recorded Future
Stack Overflow Blog
Stack Overflow Blog
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
D
Docker
Latest news
Latest news
C
CERT Recently Published Vulnerability Notes
D
Darknet – Hacking Tools, Hacker News & Cyber Security
B
Blog RSS Feed
V2EX - 技术
V2EX - 技术
小众软件
小众软件
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
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章:二次开发实战与综合案例 第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
第8章:局部极小值与扫描线机制
我才是银古 · 2026-06-24 · via 博客园 - 我才是银古

第8章:局部极小值与扫描线机制

8.1 概述

Vatti 裁剪算法的核心思想是使用自底向上的扫描线来处理多边形。本章将深入分析 Clipper 如何识别局部极小值、管理扫描线,以及这些机制如何驱动整个裁剪过程。

8.2 局部极小值的定义

8.2.1 几何定义

局部极小值(Local Minimum):多边形轮廓上的一个顶点,其 Y 坐标小于相邻两个顶点的 Y 坐标(在 Y 轴向上的坐标系中,这是局部最低点)。

        ●         ●
       / \       / \
      /   \     /   \
     /     \   /     \
    /       ●─●       \
   /    极小值点       \
  ●                     ●
 极小值点             极小值点

8.2.2 LocalMinima 结构

internal class LocalMinima
{
    internal cInt Y;              // 极小值点的 Y 坐标
    internal TEdge LeftBound;     // 左边界(向左上方延伸的边)
    internal TEdge RightBound;    // 右边界(向右上方延伸的边)
    internal LocalMinima Next;    // 下一个局部极小值(链表)
}

8.2.3 左右边界的确定

// 在 AddPath 中确定左右边界
if (E.Dx < E.Prev.Dx) 
{
    locMin.LeftBound = E.Prev;
    locMin.RightBound = E;
    leftBoundIsForward = false;
} 
else
{
    locMin.LeftBound = E;
    locMin.RightBound = E.Prev;
    leftBoundIsForward = true;
}

判断规则:比较两条边的斜率(Dx),斜率较小的边作为右边界。

情况1: E.Dx < E.Prev.Dx
       Prev (左边界)
           ╲
            ╲   E (右边界,斜率更小)
             ╲ ╱
              ●  极小值点

情况2: E.Dx >= E.Prev.Dx
       E (左边界)
           ╱
          ╱   Prev (右边界)
         ╱ ╲
        ●    极小值点

8.3 FindNextLocMin 方法

查找下一个局部极小值:

private TEdge FindNextLocMin(TEdge E)
{
    TEdge E2;
    for (;;)
    {
        // 找到一个下降后上升的转折点
        while (E.Bot != E.Prev.Bot || E.Curr == E.Top) 
            E = E.Next;
        
        // 处理水平边
        if (E.Dx != horizontal && E.Prev.Dx != horizontal) 
            break;
        
        while (E.Prev.Dx == horizontal) 
            E = E.Prev;
        E2 = E;
        while (E.Dx == horizontal) 
            E = E.Next;
        
        // 检查是否是中间水平段
        if (E.Top.Y == E.Prev.Bot.Y) 
            continue;
        
        // 选择正确的起始点
        if (E2.Prev.Bot.X < E.Bot.X) 
            E = E2;
        break;
    }
    return E;
}

8.3.1 算法图解

步骤1: 跳过上升的边
    ●
   ╱
  ╱   ← 跳过
 ╱
●

步骤2: 识别下降转上升的转折
      ●
     ╱ ╲
    ╱   ╲
   ●     ● ← 找到转折点
    ╲   ╱
     ●─●

步骤3: 处理水平边的特殊情况
   ●         ●
    ╲       ╱
     ●─────●  ← 水平段
      极小值区域

8.4 ProcessBound 方法

处理从局部极小值延伸的边界:

private TEdge ProcessBound(TEdge E, bool LeftBoundIsForward)
{
    TEdge EStart, Result = E;
    TEdge Horz;

    // 处理标记为 Skip 的边
    if (Result.OutIdx == Skip)
    {
        // ... 创建额外的局部极小值 ...
    }

    // 处理水平边
    if (E.Dx == horizontal)
    {
        // 确定水平边的方向
        if (LeftBoundIsForward) 
            EStart = E.Prev;
        else 
            EStart = E.Next;
        
        if (EStart.Dx == horizontal)
        {
            // 可能需要反转水平边
            if (EStart.Bot.X != E.Bot.X && EStart.Top.X != E.Bot.X)
                ReverseHorizontal(E);
        }
        else if (EStart.Bot.X != E.Bot.X)
            ReverseHorizontal(E);
    }

    // 向上遍历边界
    EStart = E;
    if (LeftBoundIsForward)
    {
        // 向上沿 Next 方向
        while (Result.Top.Y == Result.Next.Bot.Y && Result.Next.OutIdx != Skip)
            Result = Result.Next;
        
        // 处理水平连接
        if (Result.Dx == horizontal && Result.Next.OutIdx != Skip)
        {
            // ... 处理顶部水平边 ...
        }
        
        // 设置 NextInLML 链接
        while (E != Result)
        {
            E.NextInLML = E.Next;
            if (E.Dx == horizontal && E != EStart && E.Bot.X != E.Prev.Top.X) 
                ReverseHorizontal(E);
            E = E.Next;
        }
        Result = Result.Next;
    }
    else
    {
        // 向上沿 Prev 方向(逻辑类似)
        // ...
    }
    return Result;
}

8.4.1 NextInLML 的构建

从局部极小值向上遍历,构建 NextInLML 链:

局部极小值 LM
    ├── LeftBound: e1 → e2 → e3 (NextInLML 链)
    └── RightBound: e4 → e5 → e6 (NextInLML 链)

        e3      e6
        │       │
        e2      e5
         ╲     ╱
          e1-e4
            LM

8.5 InsertLocalMinima 方法

将局部极小值插入到有序链表:

private void InsertLocalMinima(LocalMinima newLm)
{
    if (m_MinimaList == null)
    {
        m_MinimaList = newLm;
    }
    else if (newLm.Y >= m_MinimaList.Y)
    {
        // 插入到头部
        newLm.Next = m_MinimaList;
        m_MinimaList = newLm;
    } 
    else
    {
        // 找到正确位置插入
        LocalMinima tmpLm = m_MinimaList;
        while (tmpLm.Next != null && (newLm.Y < tmpLm.Next.Y))
            tmpLm = tmpLm.Next;
        newLm.Next = tmpLm.Next;
        tmpLm.Next = newLm;
    }
}

排序规则:按 Y 坐标降序(Y 最大的在前)。

链表结构(按 Y 降序):
m_MinimaList → LM(Y=100) → LM(Y=50) → LM(Y=0) → null

这样扫描从 Y=0 开始向上时,
可以按顺序遇到 LM(Y=0) → LM(Y=50) → LM(Y=100)

8.6 扫描线管理

8.6.1 Scanbeam 结构

internal class Scanbeam
{
    internal cInt Y;          // 扫描线 Y 坐标
    internal Scanbeam Next;   // 下一个扫描线
}

8.6.2 InsertScanbeam

internal void InsertScanbeam(cInt Y)
{
    if (m_Scanbeam == null)
    {
        m_Scanbeam = new Scanbeam();
        m_Scanbeam.Next = null;
        m_Scanbeam.Y = Y;
    }
    else if (Y > m_Scanbeam.Y)
    {
        // 插入到头部
        Scanbeam newSb = new Scanbeam();
        newSb.Y = Y;
        newSb.Next = m_Scanbeam;
        m_Scanbeam = newSb;
    }
    else
    {
        // 找位置插入,忽略重复
        Scanbeam sb2 = m_Scanbeam;
        while (sb2.Next != null && (Y <= sb2.Next.Y)) 
            sb2 = sb2.Next;
        if (Y == sb2.Y) return;  // 忽略重复
        Scanbeam newSb = new Scanbeam();
        newSb.Y = Y;
        newSb.Next = sb2.Next;
        sb2.Next = newSb;
    }
}

8.6.3 PopScanbeam

internal Boolean PopScanbeam(out cInt Y)
{
    if (m_Scanbeam == null)
    {
        Y = 0;
        return false;
    }
    Y = m_Scanbeam.Y;
    m_Scanbeam = m_Scanbeam.Next;
    return true;
}

8.6.4 扫描线事件

扫描线 Y 坐标来自以下事件:

  1. 局部极小值:边开始参与
  2. 局部极大值:边结束
  3. 边的转折点:边的 Top 到达
  4. 交点:(在处理过程中动态添加)
Y
↑
80 ─────── 局部极大值
70 ─────── 交点
60 ─────── 边转折点
40 ─────── 交点
20 ─────── 局部极小值
0  ─────── 局部极小值

8.7 扫描线处理循环

主处理循环在 ExecuteInternal 中:

private bool ExecuteInternal()
{
    Reset();
    m_SortedEdges = null;
    m_Maxima = null;

    cInt botY, topY;
    if (!PopScanbeam(out botY)) return false;
    
    // 插入初始局部极小值
    InsertLocalMinimaIntoAEL(botY);
    
    // 主循环
    while (PopScanbeam(out topY) || LocalMinimaPending())
    {
        // 处理水平边
        ProcessHorizontals();
        m_GhostJoins.Clear();
        
        // 处理交点
        if (!ProcessIntersections(topY)) 
            return false;
        
        // 处理扫描线顶部的边
        ProcessEdgesAtTopOfScanbeam(topY);
        
        botY = topY;
        // 插入新的局部极小值
        InsertLocalMinimaIntoAEL(botY);
    }
    
    // 后处理:方向修正、连接处理等
    // ...
    
    return true;
}

8.7.1 处理流程图

开始
  │
  ▼
PopScanbeam(botY)
  │
  ▼
InsertLocalMinimaIntoAEL(botY)
  │
  ▼
┌─────────────────────────────────┐
│ while (PopScanbeam || Pending) │
│  │                              │
│  ├─► ProcessHorizontals()      │
│  │                              │
│  ├─► ProcessIntersections()    │
│  │                              │
│  ├─► ProcessEdgesAtTopOfScanbeam│
│  │                              │
│  └─► InsertLocalMinimaIntoAEL  │
│                                 │
└─────────────────────────────────┘
  │
  ▼
后处理
  │
  ▼
完成

8.8 InsertLocalMinimaIntoAEL

将当前扫描线上的局部极小值的边插入活动边表:

private void InsertLocalMinimaIntoAEL(cInt botY)
{
    LocalMinima lm;
    while (PopLocalMinima(botY, out lm))
    {
        TEdge lb = lm.LeftBound;
        TEdge rb = lm.RightBound;

        OutPt Op1 = null;
        if (lb == null)
        {
            // 只有右边界
            InsertEdgeIntoAEL(rb, null);
            SetWindingCount(rb);
            if (IsContributing(rb))
                Op1 = AddOutPt(rb, rb.Bot);
        }
        else if (rb == null)
        {
            // 只有左边界
            InsertEdgeIntoAEL(lb, null);
            SetWindingCount(lb);
            if (IsContributing(lb))
                Op1 = AddOutPt(lb, lb.Bot);
            InsertScanbeam(lb.Top.Y);
        }
        else
        {
            // 两个边界都存在
            InsertEdgeIntoAEL(lb, null);
            InsertEdgeIntoAEL(rb, lb);
            SetWindingCount(lb);
            rb.WindCnt = lb.WindCnt;
            rb.WindCnt2 = lb.WindCnt2;
            if (IsContributing(lb))
                Op1 = AddLocalMinPoly(lb, rb, lb.Bot);
            InsertScanbeam(lb.Top.Y);
        }

        // 处理右边界
        if (rb != null)
        {
            if (IsHorizontal(rb))
            {
                if (rb.NextInLML != null)
                    InsertScanbeam(rb.NextInLML.Top.Y);
                AddEdgeToSEL(rb);
            }
            else
                InsertScanbeam(rb.Top.Y);
        }

        // 处理边之间的交叉
        if (lb == null || rb == null) continue;
        
        // ... 处理连接点 ...
    }
}

8.9 局部极小值的类型

8.9.1 V 形极小值

    ╱╲
   ╱  ╲
  ╱    ╲
 ●      ●
  LeftBound  RightBound

两个边界都存在,形成 V 形。

8.9.2 单边极小值(开放路径)

    │
    │
    │
    ●
  单边界(开放路径的端点)

只有一个边界,另一个为 null。

8.9.3 水平极小值

  ╲      ╱
   ╲    ╱
    ●──●
   水平段

极小值点连接一个或多个水平边。

8.10 LocalMinimaPending

internal Boolean LocalMinimaPending()
{
    return (m_CurrentLM != null);
}

检查是否还有未处理的局部极小值。

8.11 本章小结

本章深入分析了 Clipper 的局部极小值和扫描线机制:

  1. 局部极小值

    • 多边形轮廓上的最低点
    • 作为边进入活动边表的入口
    • LocalMinima 结构包含左右边界
  2. 边界处理

    • ProcessBound 构建 NextInLML 链
    • 正确处理水平边
    • 确定左右边界
  3. 扫描线管理

    • InsertScanbeam 维护有序扫描线列表
    • PopScanbeam 获取下一个扫描位置
    • 扫描线来自多种事件
  4. 主循环

    • 从底部向上扫描
    • 在每个扫描线处理边的插入、交点、移除
    • InsertLocalMinimaIntoAEL 将边加入活动边表

理解这些机制是理解 Clipper 裁剪算法的关键基础。


上一章:TEdge边缘结构 | 返回目录 | 下一章:Clipper类结构