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

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
T
The Exploit Database - CXSecurity.com
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
F
Fortinet All Blogs
博客园_首页
P
Proofpoint News Feed
V
Vulnerabilities – Threatpost
The Cloudflare Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Heimdal Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
Security Affairs
The Register - Security
The Register - Security
S
Security @ Cisco Blogs
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
Schneier on Security
Schneier on Security
WordPress大学
WordPress大学
Google DeepMind News
Google DeepMind News
GbyAI
GbyAI
T
Tailwind CSS Blog
Hacker News: Ask HN
Hacker News: Ask HN
W
WeLiveSecurity
D
Docker
L
LangChain Blog
B
Blog RSS Feed
The Last Watchdog
The Last Watchdog
Cloudbric
Cloudbric
TaoSecurity Blog
TaoSecurity Blog
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
I
InfoQ
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
宝玉的分享
宝玉的分享
I
Intezer
云风的 BLOG
云风的 BLOG
V2EX - 技术
V2EX - 技术
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章:二次开发实战与综合案例 第17章:与 Qt VTK Python pythonOCC 生态集成 第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 第4章:矩形边界 - Rect64、RectD
第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD
我才是银古 · 2026-04-11 · via 博客园 - 我才是银古

第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD

3.1 概述

在 Clipper2 中,路径(Path)是点的有序集合,用于表示多边形轮廓或开放线段。与 Clipper1 使用类型别名不同,Clipper2 定义了独立的类,继承自 List<T>

3.2 Path64 类

3.2.1 类定义

public class Path64 : List<Point64> 
{
    public Path64() : base() { }
    public Path64(int capacity = 0) : base(capacity) { }
    public Path64(IEnumerable<Point64> path) : base(path) { }
    
    public override string ToString()
    {
        return string.Join(", ", this);
    }
}

3.2.2 设计特点

  1. 继承 List:直接获得所有列表操作方法
  2. 多个构造函数:支持空构造、预分配容量、从集合初始化
  3. 自定义 ToString:方便调试和日志输出

3.2.3 使用示例

// 创建空路径
Path64 path1 = new Path64();

// 预分配容量
Path64 path2 = new Path64(100);

// 从数组创建
Path64 path3 = new Path64(new Point64[] {
    new Point64(0, 0),
    new Point64(100, 0),
    new Point64(100, 100),
    new Point64(0, 100)
});

// 使用集合初始化器
Path64 path4 = new Path64 {
    new Point64(0, 0),
    new Point64(100, 0),
    new Point64(100, 100)
};

// 输出: "0,0 , 100,0 , 100,100 , 0,100 "
Console.WriteLine(path3);

3.2.4 与 Clipper1 的对比

// Clipper1 方式(类型别名)
using Path = List<IntPoint>;
Path path = new Path();

// Clipper2 方式(独立类)
Path64 path = new Path64();

独立类的优势:

  • 可以添加自定义方法
  • 更清晰的类型语义
  • 方便扩展功能

3.3 PathD 类

3.3.1 类定义

public class PathD : List<PointD>
{
    public PathD() : base() { }
    public PathD(int capacity = 0) : base(capacity) { }
    public PathD(IEnumerable<PointD> path) : base(path) { }
    
    public string ToString(int precision = 2)
    {
        return string.Join(", ", ConvertAll(x => x.ToString(precision)));
    }
}

3.3.2 带精度的 ToString

PathD path = new PathD {
    new PointD(0.123, 0.456),
    new PointD(1.234, 5.678)
};

// 默认2位小数: "0.12,0.46, 1.23,5.68"
Console.WriteLine(path.ToString());

// 4位小数: "0.1230,0.4560, 1.2340,5.6780"
Console.WriteLine(path.ToString(4));

3.4 Paths64 和 PathsD 类

3.4.1 Paths64 定义

public class Paths64 : List<Path64>
{
    public Paths64() : base() { }
    public Paths64(int capacity = 0) : base(capacity) { }
    public Paths64(IEnumerable<Path64> paths) : base(paths) { }
    
    public override string ToString()
    {
        return string.Join(Environment.NewLine, this);
    }
}

3.4.2 PathsD 定义

public class PathsD : List<PathD>
{
    public PathsD() : base() { }
    public PathsD(int capacity = 0) : base(capacity) { }
    public PathsD(IEnumerable<PathD> paths) : base(paths) { }
    
    public string ToString(int precision = 2)
    {
        return string.Join(Environment.NewLine, 
            ConvertAll(x => x.ToString(precision)));
    }
}

3.4.3 使用示例

// 创建多边形集合
Paths64 paths = new Paths64 {
    new Path64 {
        new Point64(0, 0),
        new Point64(100, 0),
        new Point64(100, 100),
        new Point64(0, 100)
    },
    new Path64 {
        new Point64(25, 25),
        new Point64(75, 25),
        new Point64(75, 75),
        new Point64(25, 75)
    }
};

// 每个路径一行
Console.WriteLine(paths);

3.5 路径创建工具方法

3.5.1 MakePath 方法

Clipper2 提供了便捷的路径创建方法:

// 从整数数组创建 Path64
public static Path64 MakePath(int[] arr)
{
    int len = arr.Length / 2;
    Path64 p = new Path64(len);
    for (int i = 0; i < len; i++)
        p.Add(new Point64(arr[i * 2], arr[i * 2 + 1]));
    return p;
}

// 从 long 数组创建 Path64
public static Path64 MakePath(long[] arr)
{
    int len = arr.Length / 2;
    Path64 p = new Path64(len);
    for (int i = 0; i < len; i++)
        p.Add(new Point64(arr[i * 2], arr[i * 2 + 1]));
    return p;
}

// 从 double 数组创建 PathD
public static PathD MakePath(double[] arr)
{
    int len = arr.Length / 2;
    PathD p = new PathD(len);
    for (int i = 0; i < len; i++)
        p.Add(new PointD(arr[i * 2], arr[i * 2 + 1]));
    return p;
}

3.5.2 使用示例

// 简洁的路径创建
Path64 square = Clipper.MakePath(new long[] {
    0, 0, 100, 0, 100, 100, 0, 100
});

PathD triangle = Clipper.MakePath(new double[] {
    0.0, 0.0, 50.0, 100.0, 100.0, 0.0
});

3.5.3 带 Z 坐标的 MakePath

#if USINGZ
public static Path64 MakePathZ(long[] arr)
{
    int len = arr.Length / 3;
    Path64 p = new Path64(len);
    for (int i = 0; i < len; i++)
        p.Add(new Point64(arr[i * 3], arr[i * 3 + 1], arr[i * 3 + 2]));
    return p;
}

public static PathD MakePathZ(double[] arr)
{
    int len = arr.Length / 3;
    PathD p = new PathD(len);
    for (int i = 0; i < len; i++)
        p.Add(new PointD(arr[i * 3], arr[i * 3 + 1], (long)arr[i * 3 + 2]));
    return p;
}
#endif

3.6 路径类型转换

3.6.1 Path64 和 PathD 互转

// PathD 转 Path64(无缩放)
public static Path64 Path64(PathD path)
{
    Path64 result = new Path64(path.Count);
    foreach (PointD pt in path)
        result.Add(new Point64(pt));
    return result;
}

// Path64 转 PathD(无缩放)
public static PathD PathD(Path64 path)
{
    PathD result = new PathD(path.Count);
    foreach (Point64 pt in path)
        result.Add(new PointD(pt));
    return result;
}

// Paths64 转 PathsD
public static PathsD PathsD(Paths64 paths)
{
    PathsD result = new PathsD(paths.Count);
    foreach (Path64 path in paths)
        result.Add(PathD(path));
    return result;
}

// PathsD 转 Paths64
public static Paths64 Paths64(PathsD paths)
{
    Paths64 result = new Paths64(paths.Count);
    foreach (PathD path in paths)
        result.Add(Path64(path));
    return result;
}

3.6.2 带缩放的转换

// PathD 转 Path64 并缩放
public static Path64 ScalePath64(PathD path, double scale)
{
    int cnt = path.Count;
    Path64 res = new Path64(cnt);
    foreach (PointD pt in path)
        res.Add(new Point64(pt, scale));
    return res;
}

// Paths64 缩放
public static Paths64 ScalePaths64(PathsD paths, double scale)
{
    int cnt = paths.Count;
    Paths64 res = new Paths64(cnt);
    foreach (PathD path in paths)
        res.Add(ScalePath64(path, scale));
    return res;
}

// Path64 转 PathD 并缩放
public static PathD ScalePathD(Path64 path, double scale)
{
    int cnt = path.Count;
    PathD res = new PathD(cnt);
    foreach (Point64 pt in path)
        res.Add(new PointD(pt, scale));
    return res;
}

// Paths64 转 PathsD 并缩放
public static PathsD ScalePathsD(Paths64 paths, double scale)
{
    int cnt = paths.Count;
    PathsD res = new PathsD(cnt);
    foreach (Path64 path in paths)
        res.Add(ScalePathD(path, scale));
    return res;
}

3.6.3 同类型缩放

// Path64 缩放(返回新路径)
public static Path64 ScalePath(Path64 path, double scale)
{
    if (InternalClipper.IsAlmostZero(scale - 1)) return path;
    Path64 result = new Path64(path.Count);
#if USINGZ
    foreach (Point64 pt in path)
        result.Add(new Point64(pt.X * scale, pt.Y * scale, pt.Z));
#else
    foreach (Point64 pt in path)
        result.Add(new Point64(pt.X * scale, pt.Y * scale));
#endif
    return result;
}

// PathD 缩放
public static PathD ScalePath(PathD path, double scale)
{
    if (InternalClipper.IsAlmostZero(scale - 1)) return path;
    PathD result = new PathD(path.Count);
    foreach (PointD pt in path)
        result.Add(new PointD(pt, scale));
    return result;
}

优化:当缩放因子接近 1 时,直接返回原路径避免不必要的复制。

3.7 路径操作方法

3.7.1 路径平移

// Path64 平移
public static Path64 TranslatePath(Path64 path, long dx, long dy)
{
    Path64 result = new Path64(path.Count);
    foreach (Point64 pt in path)
        result.Add(new Point64(pt.X + dx, pt.Y + dy));
    return result;
}

// 多路径平移
public static Paths64 TranslatePaths(Paths64 paths, long dx, long dy)
{
    Paths64 result = new Paths64(paths.Count);
    foreach (Path64 path in paths)
        result.Add(OffsetPath(path, dx, dy));
    return result;
}

// PathD 平移
public static PathD TranslatePath(PathD path, double dx, double dy)
{
    PathD result = new PathD(path.Count);
    foreach (PointD pt in path)
        result.Add(new PointD(pt.x + dx, pt.y + dy));
    return result;
}

3.7.2 路径反转

// 反转 Path64
public static Path64 ReversePath(Path64 path)
{
    Path64 result = new Path64(path);
    result.Reverse();
    return result;
}

// 反转 PathD
public static PathD ReversePath(PathD path)
{
    PathD result = new PathD(path);
    result.Reverse();
    return result;
}

// 反转多个路径
public static Paths64 ReversePaths(Paths64 paths)
{
    Paths64 result = new Paths64(paths.Count);
    foreach (Path64 t in paths)
        result.Add(ReversePath(t));
    return result;
}

3.7.3 去除重复点

// 去除整数路径中的重复点
public static Path64 StripDuplicates(Path64 path, bool isClosedPath)
{
    int cnt = path.Count;
    Path64 result = new Path64(cnt);
    if (cnt == 0) return result;
    
    Point64 lastPt = path[0];
    result.Add(lastPt);
    for (int i = 1; i < cnt; i++)
        if (lastPt != path[i])
        {
            lastPt = path[i];
            result.Add(lastPt);
        }
    
    // 闭合路径:检查首尾是否重复
    if (isClosedPath && lastPt == result[0])
        result.RemoveAt(result.Count - 1);
    
    return result;
}

// 去除近距离重复点(浮点版本)
public static PathD StripNearDuplicates(PathD path,
    double minEdgeLenSqrd, bool isClosedPath)
{
    int cnt = path.Count;
    PathD result = new PathD(cnt);
    if (cnt == 0) return result;
    
    PointD lastPt = path[0];
    result.Add(lastPt);
    for (int i = 1; i < cnt; i++)
        if (!PointsNearEqual(lastPt, path[i], minEdgeLenSqrd))
        {
            lastPt = path[i];
            result.Add(lastPt);
        }

    if (isClosedPath && PointsNearEqual(lastPt, result[0], minEdgeLenSqrd))
    {
        result.RemoveAt(result.Count - 1);
    }

    return result;
}

3.7.4 去除共线点

public static Path64 TrimCollinear(Path64 path, bool isOpen = false)
{
    int len = path.Count;
    int i = 0;
    
    if (!isOpen)
    {
        // 从路径起点开始查找非共线点
        while (i < len - 1 && 
               InternalClipper.IsCollinear(path[len - 1], path[i], path[i + 1])) 
            i++;
        // 从路径终点开始查找非共线点
        while (i < len - 1 && 
               InternalClipper.IsCollinear(path[len - 2], path[len - 1], path[i])) 
            len--;
    }

    if (len - i < 3)
    {
        if (!isOpen || len < 2 || path[0] == path[1])
            return new Path64();
        return path;
    }

    Path64 result = new Path64(len - i);
    Point64 last = path[i];
    result.Add(last);
    
    for (i++; i < len - 1; i++)
    {
        if (InternalClipper.IsCollinear(last, path[i], path[i + 1])) 
            continue;
        last = path[i];
        result.Add(last);
    }

    if (isOpen)
        result.Add(path[len - 1]);
    else if (!InternalClipper.IsCollinear(last, path[len - 1], result[0]))
        result.Add(path[len - 1]);
    else
    {
        // 继续检查结果路径中的共线点
        while (result.Count > 2 && InternalClipper.IsCollinear(
                 result[result.Count - 1], result[result.Count - 2], result[0]))
        {
            result.RemoveAt(result.Count - 1);
        }
        if (result.Count < 3)
            result.Clear();
    }
    return result;
}

3.8 路径简化算法

3.8.1 Ramer-Douglas-Peucker 算法

这是经典的线段简化算法:

internal static void RDP(Path64 path, int begin, int end, 
    double epsSqrd, List<bool> flags)
{
    while (true)
    {
        int idx = 0;
        double max_d = 0;
        
        // 跳过首尾相同的点
        while (end > begin && path[begin] == path[end]) 
            flags[end--] = false;
        
        // 找到距离首尾连线最远的点
        for (int i = begin + 1; i < end; ++i)
        {
            double d = PerpendicDistFromLineSqrd(path[i], path[begin], path[end]);
            if (d <= max_d) continue;
            max_d = d;
            idx = i;
        }

        // 如果最大距离小于阈值,返回
        if (max_d <= epsSqrd) return;
        
        // 保留该点,递归处理两侧
        flags[idx] = true;
        if (idx > begin + 1) RDP(path, begin, idx, epsSqrd, flags);
        if (idx < end - 1)
        {
            begin = idx;
            continue;  // 尾递归优化
        }
        break;
    }
}

public static Path64 RamerDouglasPeucker(Path64 path, double epsilon)
{
    int len = path.Count;
    if (len < 5) return path;
    
    List<bool> flags = new List<bool>(new bool[len]) 
    { 
        [0] = true, 
        [len - 1] = true 
    };
    
    RDP(path, 0, len - 1, Sqr(epsilon), flags);
    
    Path64 result = new Path64(len);
    for (int i = 0; i < len; ++i)
        if (flags[i]) result.Add(path[i]);
    return result;
}

3.8.2 SimplifyPath 方法

另一种简化算法,基于删除对整体形状影响最小的点:

public static Path64 SimplifyPath(Path64 path,
    double epsilon, bool isClosedPath = true)
{
    int len = path.Count, high = len - 1;
    double epsSqr = Sqr(epsilon);
    if (len < 4) return path;

    bool[] flags = new bool[len];
    double[] dsq = new double[len];  // 存储每个点到相邻点连线的距离
    int curr = 0;

    // 计算每个点的重要性(到相邻点连线的距离)
    if (isClosedPath)
    {
        dsq[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]);
        dsq[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]);
    }
    else
    {
        dsq[0] = double.MaxValue;
        dsq[high] = double.MaxValue;
    }

    for (int i = 1; i < high; ++i)
        dsq[i] = PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]);

    // 迭代删除重要性最低的点
    for (; ; )
    {
        // 找到下一个需要删除的点
        if (dsq[curr] > epsSqr)
        {
            int start = curr;
            do {
                curr = GetNext(curr, high, ref flags);
            } while (curr != start && dsq[curr] > epsSqr);
            if (curr == start) break;
        }

        int prev = GetPrior(curr, high, ref flags);
        int next = GetNext(curr, high, ref flags);
        if (next == prev) break;

        // 删除当前点并更新相邻点的距离
        // ... 省略详细逻辑
    }
    
    // 构建结果
    Path64 result = new Path64(len);
    for (int i = 0; i < len; i++)
        if (!flags[i]) result.Add(path[i]);
    return result;
}

3.9 路径面积计算

3.9.1 Shoelace 公式

public static double Area(Path64 path)
{
    // https://en.wikipedia.org/wiki/Shoelace_formula
    double a = 0.0;
    int cnt = path.Count;
    if (cnt < 3) return 0.0;
    
    Point64 prevPt = path[cnt - 1];
    foreach (Point64 pt in path)
    {
        a += (double) (prevPt.Y + pt.Y) * (prevPt.X - pt.X);
        prevPt = pt;
    }
    return a * 0.5;
}

Shoelace 公式(鞋带公式):

Area = 0.5 * |Σ(x[i]*y[i+1] - x[i+1]*y[i])|

3.9.2 判断路径方向

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPositive(Path64 poly)
{
    return Area(poly) >= 0;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPositive(PathD poly)
{
    return Area(poly) >= 0;
}
  • 正面积:逆时针方向(在 Y 轴向下的坐标系中)
  • 负面积:顺时针方向

3.10 本章小结

本章详细介绍了 Clipper2 的路径和多边形数据结构:

  1. 四种路径类型:Path64、PathD、Paths64、PathsD
  2. 继承设计:直接继承 List,获得所有列表功能
  3. 便捷创建:MakePath 等工厂方法
  4. 类型转换:Path64 ⇔ PathD,带/不带缩放
  5. 路径操作:平移、反转、去重、简化
  6. 面积计算:基于 Shoelace 公式

这些路径结构是后续所有裁剪、偏移等操作的基础。


上一章:核心数据结构 | 返回目录 | 下一章:矩形边界