第1章_Clipper1概述与安装
第一章 Clipper1概述与安装(C#版)
1.1 引言
在计算机图形学、地理信息系统(GIS)、计算机辅助设计(CAD)以及游戏开发等领域,多边形的几何运算是一个基础且至关重要的功能。无论是地图数据的处理、建筑设计中的布尔运算、游戏中的碰撞检测,还是工业制造中的刀具路径规划,都需要对多边形进行精确的裁剪、合并、求交、偏移等操作。这些看似简单的几何运算,在实际实现中却面临着诸多挑战:浮点数精度问题、边缘情况处理、算法性能优化等。
Clipper1(也称为 ClipperLib)正是为解决这些复杂的多边形几何运算问题而诞生的一个成熟、稳定的开源库。它由澳大利亚程序员 Angus Johnson 开发,采用 Boost Software License 许可,可以免费用于商业和非商业项目。Clipper1 以其卓越的鲁棒性、高精度和良好的性能,成为了众多开发者在处理多边形运算时的首选工具。
本教程专注于 Clipper1 的 C# 版本(ClipperLib),将深入讲解其核心概念、数据结构、API 使用方法以及实际应用场景。通过系统学习本教程,您将能够熟练掌握 Clipper1 在 C# 项目中的应用,解决各种复杂的多边形几何运算问题。
1.2 什么是Clipper1
1.2.1 Clipper1的历史与发展
Clipper1(正式名称为 Clipper Library)首次发布于 2010 年,是一个专门用于多边形裁剪和偏移的开源库。它的开发初衷是为了解决现有几何库在处理复杂多边形时存在的各种问题,特别是精度和鲁棒性方面的不足。
在 Clipper1 问世之前,很多几何运算库都基于浮点数运算,这导致了一系列问题:
- 精度损失:浮点数的舍入误差会在复杂运算中累积,导致结果不准确
- 数值不稳定:某些边缘情况下可能产生不可预测的结果
- 一致性问题:相同的输入在不同平台或编译器下可能产生略有差异的结果
Clipper1 通过采用整数坐标系统从根本上解决了这些问题。它基于 Vatti 多边形裁剪算法的改进版本,并针对实际应用场景进行了大量优化。经过多年的发展和完善,Clipper1 已经成为一个非常成熟和可靠的库,被广泛应用于各种商业和开源项目中。
需要注意的是,虽然后来推出了 Clipper2(全新重写的版本),但 Clipper1 仍然在许多项目中被广泛使用,主要原因包括:
- API 稳定,不需要迁移成本
- 文档完善,社区支持良好
- 性能已经足够满足大多数应用场景
- 代码成熟度高,经过长时间的实践验证
1.2.2 核心功能概述
Clipper1 提供了丰富的多边形几何运算功能,主要包括以下几个方面:
多边形布尔运算(Boolean Operations)
布尔运算是 Clipper1 最核心的功能,它支持对多边形执行以下操作:
交集(Intersection)
计算两个或多个多边形重叠的区域。例如,在 GIS 应用中,计算两个行政区域的交叉部分;在 CAD 中,找出两个零件的重叠区域。
并集(Union)
合并多个多边形,得到它们的总覆盖区域。常用于合并相邻或重叠的区域,消除内部边界。例如,将多个地块合并为一个整体区域。
差集(Difference)
从一个多边形中减去另一个多边形,得到剩余的部分。这在制造业中特别有用,比如在一个板材上挖孔。
异或(XOR,Exclusive OR)
得到两个多边形不重叠的部分,即属于 A 但不属于 B,或属于 B 但不属于 A 的区域。
这些布尔运算不仅支持简单的凸多边形,更重要的是能够正确处理:
- 复杂的凹多边形
- 带有孔洞的多边形
- 自相交的多边形
- 多个多边形同时参与运算
多边形偏移(Polygon Offsetting)
多边形偏移也称为膨胀(Inflate)或收缩(Deflate),是指将多边形的边界向外或向内移动指定的距离。这个功能在实际应用中非常广泛:
工业制造
- CNC 铣削中的刀具半径补偿
- 激光切割路径的生成
- 3D 打印中的轮廓偏移
地理信息系统
- 缓冲区分析(Buffer Analysis)
- 影响范围计算
- 安全距离规划
建筑设计
- 建筑退界线计算
- 围墙与建筑物的距离规划
- 绿化带规划
游戏开发
- 角色行走区域计算
- 安全区域生成
- 可见性分析
Clipper1 的偏移功能支持多种连接方式和端点处理方式,可以生成圆角、斜接或方形的偏移效果,满足不同应用场景的需求。
多边形简化(Polygon Simplification)
Clipper1 提供了 SimplifyPolygon 和 SimplifyPolygons 方法,可以:
- 移除自相交部分
- 简化复杂的多边形结构
- 标准化多边形表示
这对于清理来自用户输入或其他系统的不规范多边形数据非常有用。
方向性处理
Clipper1 提供了判断和修正多边形方向的功能:
Orientation()方法判断路径是顺时针还是逆时针ReversePath()和ReversePaths()反转路径方向- 自动处理外轮廓和内孔洞的方向关系
面积和边界计算
Clipper1 还提供了一些实用的几何计算功能:
Area()计算多边形面积(考虑方向,带符号)- 支持带孔洞多边形的面积计算
- 高精度的整数运算确保结果准确
1.2.3 Clipper1的技术特点
基于整数的精确运算
Clipper1 最显著的特点是使用 64 位有符号整数(long 类型,范围约为 ±9.2×10^18)来表示坐标。这种设计带来了几个重要优势:
绝对精确性
整数运算没有舍入误差,所有计算结果都是精确的。这意味着:
- 两次运行相同的输入总是产生完全相同的输出
- 不会出现浮点数计算中的累积误差
- 边界情况处理更加可靠
跨平台一致性
整数运算在所有平台和编译器上的行为是完全一致的,不像浮点数可能因为不同的实现而产生微小差异。
使用方法
开发者需要将浮点数坐标缩放到整数范围。例如:
// 如果原始坐标是以米为单位,精度要求到毫米
// 可以将坐标乘以 1000
double originalX = 123.456; // 米
long scaledX = (long)(originalX * 1000); // 转换为毫米,整数表示
// 使用 Clipper1 进行运算后,再缩放回去
double resultX = scaledX / 1000.0;
缩放系数的选择
- 选择合适的缩放系数很重要
- 太小会损失精度
- 太大可能导致整数溢出
- 一般建议根据实际精度需求选择 10^6 到 10^9 之间的缩放系数
卓越的鲁棒性
Clipper1 能够正确处理各种复杂和边缘情况,这是它被广泛采用的主要原因之一:
自相交多边形
许多几何库在处理自相交多边形时会出现错误或崩溃,但 Clipper1 可以正确处理这类情况,并根据填充规则正确计算结果区域。
退化情况
Clipper1 能够处理各种退化情况:
- 零面积的细长多边形
- 共线的顶点
- 重复的顶点
- 零长度的边
复杂的拓扑关系
- 多层嵌套的孔洞
- 多个多边形的复杂交叉
- 边界重叠的情况
数值极值
由于使用整数运算,只要坐标值在 long 类型的范围内,都可以正确处理,不会出现浮点数的溢出或下溢问题。
良好的性能
虽然 Clipper1 使用的是相对复杂的算法,但通过精心优化,它在实际应用中表现出良好的性能:
时间复杂度
- 对于大多数实际情况,复杂度接近 O(n log n),其中 n 是总顶点数
- 对于某些特殊情况可能达到 O(n²),但这种情况在实践中很少见
内存使用
- 合理的内存占用
- 支持处理包含数十万顶点的多边形
优化特性
- 自动处理简单情况的快速路径
- 智能的预分配和内存管理
- 高效的数据结构设计
灵活的API设计
Clipper1 提供了多层次的 API,适应不同的使用场景:
简单API
对于基本的布尔运算,只需要几行代码:
Clipper clipper = new Clipper();
clipper.AddPaths(subjects, PolyType.ptSubject, true);
clipper.AddPaths(clips, PolyType.ptClip, true);
List<List<IntPoint>> solution = new List<List<IntPoint>>();
clipper.Execute(ClipType.ctIntersection, solution);
高级控制
对于需要更多控制的场景,提供了丰富的选项:
- 多种填充规则(EvenOdd, NonZero, Positive, Negative)
- PolyTree 结构用于表示层次关系
- 可选的 Z 轴处理(通过回调)
- StrictlySimple 选项确保输出的简单性
偏移操作
专门的 ClipperOffset 类提供了强大的偏移功能:
- 多种连接类型(Round, Square, Miter)
- 多种端点类型
- 斜接限制控制
- 圆弧近似精度控制
1.2.4 Clipper1的应用领域
Clipper1 在众多领域都有广泛应用:
地理信息系统(GIS)
- 地图要素的裁剪和合并
- 缓冲区分析
- 叠加分析
- 空间查询和分析
计算机辅助设计(CAD)
- 图形的布尔运算
- 刀具路径生成
- 零件设计中的形状操作
游戏开发
- 2D 碰撞检测
- 可见性判断
- 地形和关卡设计
- 导航网格生成
工业制造
- CNC 加工路径规划
- 激光切割路径生成
- 3D 打印切片处理
计算机图形学
- 矢量图形处理
- SVG 操作
- 图形渲染优化
建筑设计
- 建筑平面图处理
- 空间规划
- 日照分析
1.3 开发环境准备
1.3.1 系统要求
Clipper1 的 C# 版本对系统环境的要求非常宽松:
操作系统
- Windows(XP 及以上,推荐 Windows 10/11)
- Linux(各主流发行版)
- macOS(10.9 及以上)
开发框架
- .NET Framework 2.0 及以上(推荐 .NET Framework 4.x)
- .NET Core 1.0 及以上
- .NET 5/6/7/8
- Mono
- Unity(需要兼容的 .NET 版本)
- Xamarin
Clipper1 的代码非常兼容,不依赖任何特定平台的功能,因此可以在几乎所有支持 C# 的环境中运行。
开发工具
- Visual Studio 2010 及以上版本(推荐 Visual Studio 2022)
- Visual Studio Code + C# 扩展
- JetBrains Rider
- MonoDevelop
- 或任何支持 C# 的 IDE
1.3.2 安装Clipper1
Clipper1 提供了多种安装方式,可以根据项目需求选择最合适的方法。
方法一:通过 NuGet 包管理器安装(推荐)
这是最简单、最推荐的安装方式。NuGet 是 .NET 生态系统的标准包管理器,可以自动处理依赖关系和版本管理。
使用 Visual Studio 的图形界面:
- 在 Visual Studio 中打开您的项目
- 右键点击项目名称,选择"管理 NuGet 程序包"
- 在"浏览"标签页中搜索"Clipper"
- 选择"Clipper"包(作者为 Angus Johnson)
- 点击"安装"按钮
- 接受许可协议
使用 Package Manager Console:
在 Visual Studio 中打开 Package Manager Console(工具 → NuGet 包管理器 → 程序包管理器控制台),然后执行:
Install-Package Clipper
使用 .NET CLI:
在项目目录下打开命令行,执行:
dotnet add package Clipper
指定版本:
如果需要安装特定版本(最新稳定版为 6.4.2),可以使用:
dotnet add package Clipper --version 6.4.2
安装完成后,NuGet 会自动:
- 下载 Clipper 库的 DLL 文件
- 添加到项目引用
- 在需要时复制到输出目录
方法二:手动下载源代码
如果您希望查看或修改源代码,可以从官方网站下载:
- 访问 Clipper 官方网站:https://sourceforge.net/projects/polyclipping/
- 下载最新版本的源代码压缩包
- 解压缩到本地目录
源代码包中包含:
- C# 源文件(通常在 cs 目录下)
- 示例代码
- 文档
- 其他语言版本(C++、Delphi 等)
将源代码集成到项目:
方式 1:直接添加源文件
- 在您的 C# 项目中创建一个文件夹(如 ClipperLib)
- 将 C# 源文件复制到该文件夹
- 在 Visual Studio 中将这些文件添加到项目
- 确保命名空间正确设置为
ClipperLib
方式 2:创建类库项目
- 创建一个新的 .NET 类库项目
- 将 Clipper 源文件添加到该项目
- 编译生成 DLL
- 在您的主项目中引用这个 DLL
方法三:从 GitHub 获取
虽然官方主要维护在 SourceForge 上,但也有一些社区维护的 GitHub 镜像:
git clone https://github.com/AngusJohnson/Clipper1.git
1.3.3 验证安装
安装完成后,我们来创建一个简单的测试程序来验证 Clipper1 是否正确安装。
创建测试项目
- 创建一个新的控制台应用程序项目
- 通过上述任一方法安装 Clipper1
- 创建一个简单的测试代码
测试代码
using System;
using System.Collections.Generic;
using ClipperLib;
namespace ClipperTest
{
class Program
{
static void Main(string[] args)
{
// 使用 ClipperLib 的命名空间别名,方便使用
using Path = List<IntPoint>;
using Paths = List<List<IntPoint>>;
// 创建两个正方形
// 第一个正方形:(0,0) 到 (100,100)
Path square1 = new Path
{
new IntPoint(0, 0),
new IntPoint(100, 0),
new IntPoint(100, 100),
new IntPoint(0, 100)
};
// 第二个正方形:(50,50) 到 (150,150)
Path square2 = new Path
{
new IntPoint(50, 50),
new IntPoint(150, 50),
new IntPoint(150, 150),
new IntPoint(50, 150)
};
// 创建 Clipper 对象
Clipper clipper = new Clipper();
// 添加路径
clipper.AddPath(square1, PolyType.ptSubject, true);
clipper.AddPath(square2, PolyType.ptClip, true);
// 执行交集运算
Paths solution = new Paths();
bool success = clipper.Execute(ClipType.ctIntersection, solution);
// 输出结果
if (success && solution.Count > 0)
{
Console.WriteLine("Clipper1 安装成功!");
Console.WriteLine($"交集运算产生了 {solution.Count} 个多边形");
Console.WriteLine("第一个多边形的顶点:");
foreach (var point in solution[0])
{
Console.WriteLine($" ({point.X}, {point.Y})");
}
// 计算面积
double area = Clipper.Area(solution[0]);
Console.WriteLine($"交集面积:{area}");
}
else
{
Console.WriteLine("运算失败!");
}
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
}
}
预期输出
运行这个程序,您应该看到类似以下的输出:
Clipper1 安装成功!
交集运算产生了 1 个多边形
第一个多边形的顶点:
(50, 50)
(100, 50)
(100, 100)
(50, 100)
交集面积:2500
按任意键退出...
这个结果是正确的:两个正方形的交集是一个 50×50 的正方形,面积为 2500。
常见问题排查
如果程序无法编译或运行,请检查:
-
命名空间引用
确保添加了using ClipperLib; -
版本兼容性
确认项目的目标框架与 Clipper 版本兼容 -
DLL 引用
检查项目引用中是否正确添加了 Clipper.dll -
using 别名冲突
如果项目中有其他使用Path或Paths的类型,可能需要使用完整的类型名或调整别名
1.3.4 项目配置建议
命名空间组织
在实际项目中,建议按以下方式组织代码:
using System;
using System.Collections.Generic;
using ClipperLib;
// 定义类型别名以提高代码可读性
using Path = System.Collections.Generic.List<ClipperLib.IntPoint>;
using Paths = System.Collections.Generic.List<System.Collections.Generic.List<ClipperLib.IntPoint>>;
坐标缩放管理
为了方便坐标转换,建议创建辅助类:
public static class CoordinateConverter
{
// 缩放系数,根据精度需求调整
public const double ScaleFactor = 1000000.0;
// 将浮点数坐标转换为整数
public static IntPoint ToIntPoint(double x, double y)
{
return new IntPoint(
(long)(x * ScaleFactor),
(long)(y * ScaleFactor)
);
}
// 将整数坐标转换回浮点数
public static (double x, double y) ToDouble(IntPoint point)
{
return (
point.X / ScaleFactor,
point.Y / ScaleFactor
);
}
// 转换路径
public static Path ToIntPath(IEnumerable<(double x, double y)> points)
{
Path result = new Path();
foreach (var point in points)
{
result.Add(ToIntPoint(point.x, point.y));
}
return result;
}
// 转换回浮点数路径
public static List<(double x, double y)> ToDoublePath(Path path)
{
var result = new List<(double x, double y)>();
foreach (var point in path)
{
result.Add(ToDouble(point));
}
return result;
}
}
使用示例:
// 使用浮点数定义多边形
var floatPoints = new List<(double x, double y)>
{
(0.0, 0.0),
(10.5, 0.0),
(10.5, 10.5),
(0.0, 10.5)
};
// 转换为整数路径
Path intPath = CoordinateConverter.ToIntPath(floatPoints);
// 使用 Clipper 进行运算...
// 转换回浮点数
var resultPoints = CoordinateConverter.ToDoublePath(intPath);
性能考虑
对于性能敏感的应用:
-
复用 Clipper 对象
// 不推荐:每次都创建新对象 for (int i = 0; i < 1000; i++) { var clipper = new Clipper(); // ... 使用 clipper } // 推荐:复用对象 var clipper = new Clipper(); for (int i = 0; i < 1000; i++) { clipper.Clear(); // ... 使用 clipper } -
预分配内存
// 如果知道大概的顶点数量,可以预分配 Path path = new Path(expectedVertexCount); -
批量操作
尽可能将多个多边形一次性添加,而不是多次调用 Execute
1.4 第一个完整示例
让我们通过一个更完整的示例来展示 Clipper1 的基本使用流程。
1.4.1 需求描述
假设我们需要开发一个地块合并功能:有两块相邻的土地,需要计算它们的总面积、交叉区域以及合并后的边界。
1.4.2 实现代码
using System;
using System.Collections.Generic;
using ClipperLib;
using Path = System.Collections.Generic.List<ClipperLib.IntPoint>;
using Paths = System.Collections.Generic.List<System.Collections.Generic.List<ClipperLib.IntPoint>>;
namespace ClipperDemo
{
class LandMergeExample
{
static void Main(string[] args)
{
Console.WriteLine("=== 地块合并示例 ===\n");
// 定义第一块土地(单位:米,缩放到毫米)
// 假设是一个不规则四边形
Path land1 = new Path
{
new IntPoint(0, 0),
new IntPoint(30000, 0), // 30米
new IntPoint(30000, 40000), // 40米
new IntPoint(0, 40000)
};
// 定义第二块土地(部分重叠)
Path land2 = new Path
{
new IntPoint(20000, 10000),
new IntPoint(50000, 10000),
new IntPoint(50000, 35000),
new IntPoint(20000, 35000)
};
// 1. 计算各自的面积
double area1 = Math.Abs(Clipper.Area(land1)) / 1000000.0; // 转换为平方米
double area2 = Math.Abs(Clipper.Area(land2)) / 1000000.0;
Console.WriteLine($"土地1面积:{area1} 平方米");
Console.WriteLine($"土地2面积:{area2} 平方米");
// 2. 计算交集(重叠区域)
Clipper clipper = new Clipper();
clipper.AddPath(land1, PolyType.ptSubject, true);
clipper.AddPath(land2, PolyType.ptClip, true);
Paths intersection = new Paths();
clipper.Execute(ClipType.ctIntersection, intersection);
if (intersection.Count > 0)
{
double intersectionArea = Math.Abs(Clipper.Area(intersection[0])) / 1000000.0;
Console.WriteLine($"\n重叠区域面积:{intersectionArea} 平方米");
}
// 3. 计算并集(合并后的总区域)
clipper.Clear();
clipper.AddPath(land1, PolyType.ptSubject, true);
clipper.AddPath(land2, PolyType.ptClip, true);
Paths union = new Paths();
clipper.Execute(ClipType.ctUnion, union);
if (union.Count > 0)
{
double unionArea = Math.Abs(Clipper.Area(union[0])) / 1000000.0;
Console.WriteLine($"合并后总面积:{unionArea} 平方米");
Console.WriteLine("\n合并后边界顶点坐标(米):");
foreach (var point in union[0])
{
Console.WriteLine($" ({point.X / 1000.0}, {point.Y / 1000.0})");
}
}
// 4. 计算差集(土地1减去重叠部分)
clipper.Clear();
clipper.AddPath(land1, PolyType.ptSubject, true);
clipper.AddPath(land2, PolyType.ptClip, true);
Paths difference = new Paths();
clipper.Execute(ClipType.ctDifference, difference);
if (difference.Count > 0)
{
double diffArea = Math.Abs(Clipper.Area(difference[0])) / 1000000.0;
Console.WriteLine($"\n土地1独有部分面积:{diffArea} 平方米");
}
// 5. 创建缓冲区(向外扩展5米)
Console.WriteLine("\n=== 创建5米缓冲区 ===");
ClipperOffset offsetter = new ClipperOffset();
offsetter.AddPath(land1, JoinType.jtRound, EndType.etClosedPolygon);
Paths buffered = new Paths();
offsetter.Execute(ref buffered, 5000.0); // 5米 = 5000毫米
if (buffered.Count > 0)
{
double bufferedArea = Math.Abs(Clipper.Area(buffered[0])) / 1000000.0;
Console.WriteLine($"扩展后面积:{bufferedArea} 平方米");
Console.WriteLine($"面积增加:{bufferedArea - area1} 平方米");
}
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
}
}
1.4.3 运行结果分析
运行这个程序会输出类似以下结果:
=== 地块合并示例 ===
土地1面积:1200 平方米
土地2面积:750 平方米
重叠区域面积:250 平方米
合并后总面积:1700 平方米
合并后边界顶点坐标(米):
(0.0, 0.0)
(30.0, 0.0)
(30.0, 10.0)
(50.0, 10.0)
(50.0, 35.0)
(30.0, 35.0)
(30.0, 40.0)
(0.0, 40.0)
土地1独有部分面积:950 平方米
=== 创建5米缓冲区 ===
扩展后面积:1629.5 平方米
面积增加:429.5 平方米
从结果可以看出:
- 两块土地面积之和是 1950 平方米
- 重叠部分是 250 平方米
- 合并后总面积是 1700 平方米(符合:1200 + 750 - 250 = 1700)
- 缓冲区计算正确地增加了周边区域
1.5 文档和学习资源
1.5.1 官方文档
Clipper1 拥有完善的官方文档:
在线文档
- 官方网站:http://www.angusj.com/delphi/clipper.php
- API 参考:http://www.angusj.com/delphi/clipper/documentation/Docs/Overview/_Body.htm
- 包含详细的类、方法、枚举说明
下载版文档
- 源代码包中包含完整的离线文档
- CHM 格式的帮助文件(Windows)
- HTML 格式文档(跨平台)
1.5.2 示例代码
官方示例
- 源代码包中的 Samples 目录
- 展示了各种常见用法
- 包含完整的可运行项目
社区资源
- GitHub 上的各种示例项目
- Stack Overflow 上的问答
- 各技术博客的教程文章
1.5.3 社区支持
论坛和讨论
- SourceForge 项目页面的讨论区
- Stack Overflow 上的 clipperlib 标签
- 相关技术论坛的专题讨论
问题报告
- SourceForge 的问题跟踪系统
- 邮件列表
1.5.4 相关工具
可视化工具
- 官方提供的 Clipper 演示程序
- 可以直观地看到各种操作的效果
- 帮助理解算法行为
集成工具
- Unity 插件
- CAD 软件插件
- GIS 系统集成
1.6 本章小结
在本章中,我们全面介绍了 Clipper1 的基础知识:
- Clipper1 概述:了解了它的历史、设计理念和核心特点
- 核心功能:掌握了布尔运算、多边形偏移等主要功能
- 技术特点:理解了基于整数的精确运算、鲁棒性和性能优势
- 环境搭建:学会了通过 NuGet 或源代码安装 Clipper1
- 实践验证:通过实际代码验证了安装和基本使用
通过本章的学习,您应该已经:
- 理解 Clipper1 适用的应用场景
- 成功安装和配置了开发环境
- 运行了第一个 Clipper1 程序
- 了解了后续学习的资源和方向
在接下来的章节中,我们将深入学习 Clipper1 的各个方面,包括数据结构、核心算法、高级特性和实际应用案例。每一章都会通过大量的代码示例和实践项目,帮助您逐步掌握 Clipper1 的使用技巧,成为多边形几何运算的专家。
1.7 练习题
为了巩固本章所学内容,请完成以下练习:
-
安装验证:在您的开发环境中安装 Clipper1,并运行本章提供的测试代码
-
坐标转换:编写一个转换器类,支持在浮点数和整数坐标之间相互转换,缩放系数可配置
-
简单应用:创建两个三角形,计算它们的交集、并集和差集,并输出顶点坐标
-
面积计算:计算一个正六边形的面积,验证结果是否正确
-
实际问题:思考在您的工作或学习领域中,哪些场景可以应用 Clipper1?列出至少3个具体的应用案例
在下一章中,我们将深入学习 Clipper1 的核心数据结构,包括 IntPoint、Path、Paths、PolyTree 等,这些是理解和使用 Clipper1 的基础。

浙公网安备 33010602011771号