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

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
小众软件
小众软件
博客园_首页
博客园 - 聂微东
V
V2EX
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
SegmentFault 最新的问题
J
Java Code Geeks
Last Week in AI
Last Week in AI
The Cloudflare Blog
月光博客
月光博客
雷峰网
雷峰网
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
博客园 - Franky
腾讯CDC
Jina AI
Jina AI
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
量子位
爱范儿
爱范儿
美团技术团队
T
Tailwind CSS Blog
博客园 - 【当耐特】
D
Docker
IT之家
IT之家
V
Visual Studio Blog
P
Proofpoint News Feed
L
LangChain Blog
Engineering at Meta
Engineering at Meta
C
Check Point Blog
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Recorded Future
Recorded Future

博客园 - 我才是银古

第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
第13章:水平边处理算法
我才是银古 · 2026-06-24 · via 博客园 - 我才是银古

第13章:水平边处理算法

13.1 概述

水平边是 Clipper 算法中需要特殊处理的情况。由于水平边与扫描线平行,不能使用常规的扫描线方法处理。本章将深入分析 Clipper 如何处理水平边。

13.2 水平边的特殊性

13.2.1 定义

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

13.2.2 斜率标记

private void SetDx(TEdge e)
{
    e.Delta.X = (e.Top.X - e.Bot.X);
    e.Delta.Y = (e.Top.Y - e.Bot.Y);
    if (e.Delta.Y == 0) 
        e.Dx = horizontal;  // 特殊标记
    else 
        e.Dx = (double)(e.Delta.X) / (e.Delta.Y);
}

13.2.3 挑战

  1. 扫描线与水平边重合
  2. 无法计算常规的扫描线交点
  3. 需要考虑边的方向性

13.3 SEL(Sorted Edge List)

水平边使用 SEL 进行管理:

13.3.1 AddEdgeToSEL

private void AddEdgeToSEL(TEdge edge)
{
    if (m_SortedEdges == null)
    {
        m_SortedEdges = edge;
        edge.PrevInSEL = null;
        edge.NextInSEL = null;
    }
    else
    {
        edge.NextInSEL = m_SortedEdges;
        edge.PrevInSEL = null;
        m_SortedEdges.PrevInSEL = edge;
        m_SortedEdges = edge;
    }
}

13.3.2 PopEdgeFromSEL

private bool PopEdgeFromSEL(out TEdge e)
{
    e = m_SortedEdges;
    if (e == null) return false;
    TEdge oldE = e;
    m_SortedEdges = e.NextInSEL;
    if (m_SortedEdges != null) 
        m_SortedEdges.PrevInSEL = null;
    oldE.NextInSEL = null;
    oldE.PrevInSEL = null;
    return true;
}

13.3.3 DeleteFromSEL

private void DeleteFromSEL(TEdge e)
{
    TEdge SelPrev = e.PrevInSEL;
    TEdge SelNext = e.NextInSEL;
    if (SelPrev == null && SelNext == null && (e != m_SortedEdges))
        return;
    if (SelPrev != null)
        SelPrev.NextInSEL = SelNext;
    else 
        m_SortedEdges = SelNext;
    if (SelNext != null)
        SelNext.PrevInSEL = SelPrev;
    e.NextInSEL = null;
    e.PrevInSEL = null;
}

13.4 ProcessHorizontals

处理所有待处理的水平边:

private void ProcessHorizontals()
{
    TEdge horzEdge;
    while (PopEdgeFromSEL(out horzEdge))
        ProcessHorizontal(horzEdge);
}

13.5 ProcessHorizontal 详解

private void ProcessHorizontal(TEdge horzEdge)
{
    Direction dir;
    cInt horzLeft, horzRight;
    bool IsOpen = horzEdge.WindDelta == 0;

    GetHorzDirection(horzEdge, out dir, out horzLeft, out horzRight);

    TEdge eLastHorz = horzEdge, eMaxPair = null;
    
    // 找到连续水平边的最后一条
    while (eLastHorz.NextInLML != null && IsHorizontal(eLastHorz.NextInLML)) 
        eLastHorz = eLastHorz.NextInLML;
    
    if (eLastHorz.NextInLML == null)
        eMaxPair = GetMaximaPair(eLastHorz);

    // 处理 Maxima 列表
    Maxima currMax = m_Maxima;
    if (currMax != null)
    {
        // 找到范围内的第一个 maxima
        if (dir == Direction.dLeftToRight)
        {
            while (currMax != null && currMax.X <= horzEdge.Bot.X)
                currMax = currMax.Next;
            if (currMax != null && currMax.X >= eLastHorz.Top.X) 
                currMax = null;
        }
        else
        {
            while (currMax.Next != null && currMax.Next.X < horzEdge.Bot.X) 
                currMax = currMax.Next;
            if (currMax.X <= eLastHorz.Top.X) currMax = null;
        }
    }

    OutPt op1 = null;
    
    // 主处理循环
    for (;;)
    {
        bool IsLastHorz = (horzEdge == eLastHorz);
        TEdge e = GetNextInAEL(horzEdge, dir);
        
        while (e != null)
        {
            // 在水平边上插入 maxima 点
            if (currMax != null)
            {
                if (dir == Direction.dLeftToRight)
                {
                    while (currMax != null && currMax.X < e.Curr.X) 
                    {
                        if (horzEdge.OutIdx >= 0 && !IsOpen) 
                            AddOutPt(horzEdge, new IntPoint(currMax.X, horzEdge.Bot.Y));
                        currMax = currMax.Next;                  
                    }
                }
                else
                {
                    while (currMax != null && currMax.X > e.Curr.X)
                    {
                        if (horzEdge.OutIdx >= 0 && !IsOpen)
                            AddOutPt(horzEdge, new IntPoint(currMax.X, horzEdge.Bot.Y));
                        currMax = currMax.Prev;
                    }
                }
            }

            // 检查是否超出水平边范围
            if ((dir == Direction.dLeftToRight && e.Curr.X > horzRight) ||
                (dir == Direction.dRightToLeft && e.Curr.X < horzLeft)) 
                break;
                            
            // 检查是否到达中间水平边的末端
            if (e.Curr.X == horzEdge.Top.X && horzEdge.NextInLML != null && 
                e.Dx < horzEdge.NextInLML.Dx) 
                break;

            // 处理输出
            if (horzEdge.OutIdx >= 0 && !IsOpen)
            {
#if use_xyz
                if (dir == Direction.dLeftToRight) 
                    SetZ(ref e.Curr, horzEdge, e);
                else 
                    SetZ(ref e.Curr, e, horzEdge);
#endif
                op1 = AddOutPt(horzEdge, e.Curr);
                
                // 处理与其他水平边的连接
                TEdge eNextHorz = m_SortedEdges;
                while (eNextHorz != null)
                {
                    if (eNextHorz.OutIdx >= 0 &&
                        HorzSegmentsOverlap(horzEdge.Bot.X, horzEdge.Top.X, 
                                           eNextHorz.Bot.X, eNextHorz.Top.X))
                    {
                        OutPt op2 = GetLastOutPt(eNextHorz);
                        AddJoin(op2, op1, eNextHorz.Top);
                    }
                    eNextHorz = eNextHorz.NextInSEL;
                }
                AddGhostJoin(op1, horzEdge.Bot);
            }
          
            // 处理最大配对边
            if (e == eMaxPair && IsLastHorz)
            {
                if (horzEdge.OutIdx >= 0)
                    AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge.Top);
                DeleteFromAEL(horzEdge);
                DeleteFromAEL(eMaxPair);
                return;
            }
              
            // 处理交点
            if (dir == Direction.dLeftToRight)
            {
                IntPoint Pt = new IntPoint(e.Curr.X, horzEdge.Curr.Y);
                IntersectEdges(horzEdge, e, Pt);
            }
            else
            {
                IntPoint Pt = new IntPoint(e.Curr.X, horzEdge.Curr.Y);
                IntersectEdges(e, horzEdge, Pt);
            }
            
            TEdge eNext = GetNextInAEL(e, dir);
            SwapPositionsInAEL(horzEdge, e);
            e = eNext;
        }

        // 检查是否继续到下一个水平段
        if (horzEdge.NextInLML == null || !IsHorizontal(horzEdge.NextInLML)) 
            break;

        UpdateEdgeIntoAEL(ref horzEdge);
        if (horzEdge.OutIdx >= 0) 
            AddOutPt(horzEdge, horzEdge.Bot);
        GetHorzDirection(horzEdge, out dir, out horzLeft, out horzRight);
    }

    // 后处理
    if (horzEdge.OutIdx >= 0 && op1 == null)
    {
        op1 = GetLastOutPt(horzEdge);
        TEdge eNextHorz = m_SortedEdges;
        while (eNextHorz != null)
        {
            if (eNextHorz.OutIdx >= 0 &&
                HorzSegmentsOverlap(horzEdge.Bot.X, horzEdge.Top.X, 
                                   eNextHorz.Bot.X, eNextHorz.Top.X))
            {
                OutPt op2 = GetLastOutPt(eNextHorz);
                AddJoin(op2, op1, eNextHorz.Top);
            }
            eNextHorz = eNextHorz.NextInSEL;
        }
        AddGhostJoin(op1, horzEdge.Top);
    }

    // 更新或删除水平边
    if (horzEdge.NextInLML != null)
    {
        if (horzEdge.OutIdx >= 0)
        {
            op1 = AddOutPt(horzEdge, horzEdge.Top);
            UpdateEdgeIntoAEL(ref horzEdge);
            if (horzEdge.WindDelta == 0) return;
            
            // 处理连接
            TEdge ePrev = horzEdge.PrevInAEL;
            TEdge eNext = horzEdge.NextInAEL;
            if (ePrev != null && ePrev.Curr.X == horzEdge.Bot.X &&
                ePrev.Curr.Y == horzEdge.Bot.Y && ePrev.WindDelta != 0 &&
                (ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y &&
                SlopesEqual(horzEdge, ePrev, m_UseFullRange)))
            {
                OutPt op2 = AddOutPt(ePrev, horzEdge.Bot);
                AddJoin(op1, op2, horzEdge.Top);
            }
            else if (eNext != null && eNext.Curr.X == horzEdge.Bot.X &&
                eNext.Curr.Y == horzEdge.Bot.Y && eNext.WindDelta != 0 &&
                eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y &&
                SlopesEqual(horzEdge, eNext, m_UseFullRange))
            {
                OutPt op2 = AddOutPt(eNext, horzEdge.Bot);
                AddJoin(op1, op2, horzEdge.Top);
            }
        }
        else
            UpdateEdgeIntoAEL(ref horzEdge); 
    }
    else
    {
        if (horzEdge.OutIdx >= 0) 
            AddOutPt(horzEdge, horzEdge.Top);
        DeleteFromAEL(horzEdge);
    }
}

13.6 GetHorzDirection

获取水平边的方向:

void GetHorzDirection(TEdge HorzEdge, out Direction Dir, out cInt Left, out cInt Right)
{
    if (HorzEdge.Bot.X < HorzEdge.Top.X)
    {
        Left = HorzEdge.Bot.X;
        Right = HorzEdge.Top.X;
        Dir = Direction.dLeftToRight;
    } 
    else
    {
        Left = HorzEdge.Top.X;
        Right = HorzEdge.Bot.X;
        Dir = Direction.dRightToLeft;
    }
}

13.7 GetNextInAEL

获取 AEL 中指定方向的下一条边:

private TEdge GetNextInAEL(TEdge e, Direction Direction)
{
    return Direction == Direction.dLeftToRight ? e.NextInAEL : e.PrevInAEL;
}

13.8 水平边处理图解

水平边处理示例:

初始状态:
        │      │      │      │
        e1     e2     e3     e4
        │      │      │      │
────────┼──────┼──────┼──────┼──── horzEdge
        │      │      │      │
      Left                 Right

处理过程(从左到右):
1. 与 e1 交叉 → IntersectEdges(horzEdge, e1)
2. 交换位置 → e1 horzEdge e2 e3 e4
3. 与 e2 交叉 → IntersectEdges(horzEdge, e2)
4. 交换位置 → e1 e2 horzEdge e3 e4
5. 继续直到 Right

最终状态:
        │      │      │      │
        e1     e2     e3     e4
        │      │      │      │
────────┴──────┴──────┴──────┴──── 
        horzEdge 被删除或更新

13.9 HorzSegmentsOverlap

判断两个水平段是否重叠:

private bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b)
{
    if (seg1a > seg1b) Swap(ref seg1a, ref seg1b);
    if (seg2a > seg2b) Swap(ref seg2a, ref seg2b);
    return (seg1a < seg2b) && (seg2a < seg1b);
}

13.10 GhostJoin

幽灵连接用于标记水平边上可能需要连接的点:

private void AddGhostJoin(OutPt Op, IntPoint OffPt)
{
    Join j = new Join();
    j.OutPt1 = Op;
    j.OffPt = OffPt;
    m_GhostJoins.Add(j);
}

用途

  • 记录水平边上的输出点
  • 在后续处理中用于连接重叠的水平边

13.11 水平边的方向约定

13.11.1 ReverseHorizontal

private void ReverseHorizontal(TEdge e)
{
    // 交换 Bot.X 和 Top.X
    Swap(ref e.Bot.X, ref e.Top.X);
#if use_xyz
    Swap(ref e.Bot.Z, ref e.Top.Z);
#endif
}

13.11.2 何时需要反转

在 ProcessBound 中处理水平边时,需要确保水平边的方向与边界方向一致:

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

13.12 连续水平边处理

// 找到连续水平边的最后一条
TEdge eLastHorz = horzEdge;
while (eLastHorz.NextInLML != null && IsHorizontal(eLastHorz.NextInLML)) 
    eLastHorz = eLastHorz.NextInLML;

// 处理连续水平边
for (;;)
{
    // ... 处理当前水平边 ...
    
    // 检查是否有更多水平边
    if (horzEdge.NextInLML == null || !IsHorizontal(horzEdge.NextInLML)) 
        break;
    
    // 更新到下一段
    UpdateEdgeIntoAEL(ref horzEdge);
    // ... 继续处理 ...
}

13.13 本章小结

本章详细分析了 Clipper 的水平边处理:

  1. 水平边特殊性

    • 与扫描线平行
    • 使用特殊斜率标记
    • 需要单独处理
  2. SEL 管理

    • 存储待处理的水平边
    • 与 AEL 配合使用
  3. ProcessHorizontal

    • 确定处理方向
    • 遍历与水平边相交的边
    • 处理交点和输出
  4. 特殊情况

    • 连续水平边
    • 水平边重叠
    • 幽灵连接

水平边处理是 Clipper 算法中最复杂的部分之一。


上一章:交点计算与处理 | 返回目录 | 下一章:输出多边形构建