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

推荐订阅源

Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
aimingoo的专栏
aimingoo的专栏
Recent Announcements
Recent Announcements
A
About on SuperTechFans
U
Unit 42
MyScale Blog
MyScale Blog
J
Java Code Geeks
博客园_首页
Blog — PlanetScale
Blog — PlanetScale
D
Docker
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 司徒正美
量子位
月光博客
月光博客
G
Google Developers Blog
V
V2EX
博客园 - 聂微东
宝玉的分享
宝玉的分享
IT之家
IT之家
Vercel News
Vercel 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
第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类结构