第20章:实际应用与最佳实践
20.1 概述
本章总结 Clipper2 在实际项目中的应用技巧、性能优化方法和常见问题的解决方案。
20.2 选择正确的类
20.2.1 类选择指南
| 需求 | 推荐类 |
|---|---|
| 整数坐标布尔运算 | Clipper64 |
| 浮点坐标布尔运算 | ClipperD |
| 快速矩形裁剪 | RectClip64 |
| 路径偏移 | ClipperOffset |
| 简单操作 | 静态方法 Clipper.* |
20.2.2 静态方法 vs 类实例
// 简单操作用静态方法
Paths64 result = Clipper.Intersect(subjects, clips, FillRule.NonZero);
// 复杂操作用类实例
Clipper64 clipper = new Clipper64();
clipper.PreserveCollinear = true;
clipper.AddSubject(subjects);
clipper.AddClip(clips);
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
20.3 性能优化
20.3.1 重用 Clipper 实例
// 不好:每次创建新实例
foreach (var item in items)
{
Clipper64 clipper = new Clipper64(); // 开销
clipper.AddSubject(item.Subject);
clipper.AddClip(item.Clip);
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
// 好:重用实例
Clipper64 clipper = new Clipper64();
foreach (var item in items)
{
clipper.Clear(); // 清除数据,保留配置
clipper.AddSubject(item.Subject);
clipper.AddClip(item.Clip);
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
20.3.2 预分配容量
// 预估结果大小
int estimatedCount = subjects.Sum(p => p.Count);
Paths64 result = new Paths64(estimatedCount);
20.3.3 使用边界框过滤
// 快速跳过不相交的多边形
Rect64 clipBounds = Clipper.GetBounds(clips);
foreach (Path64 subject in subjects)
{
Rect64 subjectBounds = Clipper.GetBounds(subject);
if (!subjectBounds.Intersects(clipBounds))
{
// 完全不相交,跳过
continue;
}
// 执行裁剪
clipper.AddSubject(subject);
}
20.3.4 并行处理
// 对于大量独立的裁剪操作
var results = items.AsParallel().Select(item =>
{
// 每个线程有自己的 Clipper 实例
Clipper64 clipper = new Clipper64();
clipper.AddSubject(item.Subject);
clipper.AddClip(item.Clip);
Paths64 result = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
return result;
}).ToList();
20.4 精度处理
20.4.1 选择合适的精度
// 屏幕坐标(像素)
ClipperD clipper = new ClipperD(0); // 整数精度
// CAD 坐标(毫米,保留 2 位小数)
ClipperD clipper = new ClipperD(2); // 0.01 精度
// 地理坐标(经纬度,保留 6 位小数)
ClipperD clipper = new ClipperD(6); // 0.000001 精度
20.4.2 坐标缩放
// 手动缩放可以获得更好的控制
double scale = 1000.0; // 保留 3 位小数
// 输入时缩放
Path64 scaledSubject = ScalePath(subject, scale);
Path64 scaledClip = ScalePath(clip, scale);
// 处理
Clipper64 clipper = new Clipper64();
clipper.AddSubject(scaledSubject);
clipper.AddClip(scaledClip);
Paths64 scaledResult = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, scaledResult);
// 输出时还原
PathsD result = ScalePaths(scaledResult, 1.0 / scale);
20.4.3 避免坐标溢出
// 检查坐标范围
const long MaxCoord = InternalClipper.MaxCoord;
bool IsValidPath(Path64 path)
{
foreach (var pt in path)
{
if (Math.Abs(pt.X) > MaxCoord || Math.Abs(pt.Y) > MaxCoord)
return false;
}
return true;
}
// 必要时缩小坐标
PathD NormalizePath(PathD path, double maxRange)
{
Rect64 bounds = Clipper.GetBounds(Clipper.ScalePath64(path, 1.0));
double scale = maxRange / Math.Max(
Math.Max(bounds.Width, bounds.Height), 1);
return ScalePath(path, scale);
}
20.5 错误处理
20.5.1 检查输入有效性
bool IsValidPolygon(Path64 path)
{
// 至少 3 个点
if (path.Count < 3) return false;
// 检查面积
if (Math.Abs(Clipper.Area(path)) < 1.0) return false;
// 检查退化边
for (int i = 0; i < path.Count; i++)
{
int j = (i + 1) % path.Count;
if (path[i] == path[j]) return false;
}
return true;
}
20.5.2 处理空结果
Paths64 result = new Paths64();
bool success = clipper.Execute(ClipType.Intersection,
FillRule.NonZero, result);
if (!success)
{
Console.WriteLine("裁剪操作失败");
return null;
}
if (result.Count == 0)
{
Console.WriteLine("结果为空(可能完全不相交)");
return result;
}
20.5.3 异常处理
try
{
clipper.AddSubject(subject);
clipper.AddClip(clip);
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
catch (Exception ex)
{
// 记录错误
Console.WriteLine($"裁剪错误: {ex.Message}");
// 尝试简化后重试
subject = Clipper.SimplifyPath(subject, 1.0);
clip = Clipper.SimplifyPath(clip, 1.0);
clipper.Clear();
clipper.AddSubject(subject);
clipper.AddClip(clip);
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
20.6 常见应用场景
20.6.1 地图瓦片切割
public Paths64 CutToTile(Paths64 geometry, int tileX, int tileY, int zoom)
{
// 计算瓦片边界
double tileSize = 360.0 / Math.Pow(2, zoom);
double left = -180.0 + tileX * tileSize;
double bottom = -90.0 + tileY * tileSize;
// 使用 RectClip 快速裁剪
Rect64 tileBounds = new Rect64(
(long)(left * 1000000),
(long)(bottom * 1000000),
(long)((left + tileSize) * 1000000),
(long)((bottom + tileSize) * 1000000)
);
RectClip64 rc = new RectClip64(tileBounds);
return rc.Execute(geometry);
}
20.6.2 缓冲区分析
public Paths64 CreateBuffer(Path64 path, double distance,
bool isPolygon)
{
ClipperOffset co = new ClipperOffset();
// 配置参数
co.ArcTolerance = distance * 0.01; // 1% 精度
EndType endType = isPolygon ? EndType.Polygon : EndType.Round;
co.AddPath(path, JoinType.Round, endType);
Paths64 result = new Paths64();
co.Execute(distance, result);
return result;
}
20.6.3 多边形简化
public Path64 SimplifyPolygon(Path64 path, double tolerance)
{
// Douglas-Peucker 简化
Path64 simplified = Clipper.SimplifyPath(path, tolerance);
// 清理自相交
Paths64 clean = Clipper.SimplifyPaths(new Paths64 { simplified },
tolerance);
if (clean.Count > 0)
return clean[0];
return path;
}
20.6.4 布尔运算组合
public Paths64 ComplexOperation(Paths64 baseShape,
Paths64 additions, Paths64 subtractions)
{
Clipper64 clipper = new Clipper64();
// 先合并基础形状和添加部分
clipper.AddSubject(baseShape);
clipper.AddClip(additions);
Paths64 unionResult = new Paths64();
clipper.Execute(ClipType.Union, FillRule.NonZero, unionResult);
// 再减去要删除的部分
clipper.Clear();
clipper.AddSubject(unionResult);
clipper.AddClip(subtractions);
Paths64 finalResult = new Paths64();
clipper.Execute(ClipType.Difference, FillRule.NonZero, finalResult);
return finalResult;
}
20.6.5 路径膨胀/收缩
public Paths64 CreateOutline(Path64 shape, double outlineWidth)
{
ClipperOffset co = new ClipperOffset();
co.AddPath(shape, JoinType.Miter, EndType.Polygon);
// 外边界
Paths64 outer = new Paths64();
co.Execute(outlineWidth / 2, outer);
// 内边界
co.Clear();
co.AddPath(shape, JoinType.Miter, EndType.Polygon);
Paths64 inner = new Paths64();
co.Execute(-outlineWidth / 2, inner);
// 相减得到轮廓
return Clipper.Difference(outer, inner, FillRule.NonZero);
}
20.7 调试技巧
20.7.1 可视化输出
public void SaveToSvg(Paths64 paths, string filename)
{
Rect64 bounds = Clipper.GetBounds(paths);
using (StreamWriter sw = new StreamWriter(filename))
{
sw.WriteLine($"<svg viewBox=\"{bounds.left} {bounds.top} " +
$"{bounds.Width} {bounds.Height}\" " +
$"xmlns=\"http://www.w3.org/2000/svg\">");
foreach (Path64 path in paths)
{
sw.Write("<polygon points=\"");
foreach (Point64 pt in path)
{
sw.Write($"{pt.X},{pt.Y} ");
}
sw.WriteLine("\" fill=\"blue\" stroke=\"black\" />");
}
sw.WriteLine("</svg>");
}
}
20.7.2 日志记录
public void LogOperation(string operation, Paths64 subject,
Paths64 clip, Paths64 result)
{
Console.WriteLine($"=== {operation} ===");
Console.WriteLine($"Subject: {subject.Count} paths, " +
$"{subject.Sum(p => p.Count)} points");
Console.WriteLine($"Clip: {clip.Count} paths, " +
$"{clip.Sum(p => p.Count)} points");
Console.WriteLine($"Result: {result.Count} paths, " +
$"{result.Sum(p => p.Count)} points");
Console.WriteLine($"Area: {result.Sum(p => Math.Abs(Clipper.Area(p)))}");
}
20.7.3 边界情况测试
[Test]
public void TestEdgeCases()
{
// 空输入
Assert.DoesNotThrow(() =>
Clipper.Intersect(new Paths64(), new Paths64(), FillRule.NonZero));
// 单点
var singlePoint = new Paths64 { new Path64 { new Point64(0, 0) } };
Assert.DoesNotThrow(() =>
Clipper.Union(singlePoint, FillRule.NonZero));
// 共线点
var collinear = new Path64 {
new Point64(0, 0),
new Point64(50, 0),
new Point64(100, 0)
};
Assert.DoesNotThrow(() =>
Clipper.SimplifyPath(collinear, 0.0));
}
20.8 内存管理
20.8.1 及时清理
// 大批量处理时,定期调用 GC
int batchSize = 1000;
int processed = 0;
foreach (var item in items)
{
ProcessItem(item);
processed++;
if (processed % batchSize == 0)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
20.8.2 避免内存泄漏
// 使用 using 或 try-finally 确保清理
void ProcessWithCleanup()
{
Clipper64 clipper = new Clipper64();
Paths64 result = new Paths64();
try
{
clipper.AddSubject(subjects);
clipper.AddClip(clips);
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
finally
{
clipper.Clear();
// 如果不再需要 result,也清理
}
}
20.9 最佳实践清单
20.9.1 输入验证
- 检查路径点数(闭合路径 ≥ 3,开放路径 ≥ 2)
- 检查坐标范围不超过 MaxCoord
- 移除重复点
- 确保路径方向一致
20.9.2 性能优化
- 重用 Clipper 实例
- 使用边界框预过滤
- 对矩形裁剪使用 RectClip64
- 考虑并行处理
20.9.3 精度控制
- 选择合适的精度级别
- 理解整数与浮点的转换
- 避免精度丢失
20.9.4 错误处理
- 检查 Execute 返回值
- 处理空结果
- 捕获并记录异常
20.10 本章小结
使用 Clipper2 的最佳实践:
- 选择正确的类:根据需求选择 Clipper64、ClipperD 或 RectClip64
- 性能优化:重用实例、预过滤、并行处理
- 精度处理:选择合适精度、避免溢出
- 错误处理:验证输入、处理异常、检查结果
- 内存管理:及时清理、避免泄漏
遵循这些最佳实践可以帮助你在项目中高效、正确地使用 Clipper2。
| 上一章:PolyTree多边形树结构 | 返回目录 |
教程总结
本教程全面介绍了 Clipper2 C# 源代码的各个方面:
- 基础篇(第1-5章):核心数据结构、路径表示、枚举类型
- 核心架构篇(第6-10章):内部工具类、高精度运算、扫描线算法核心结构
- 输出结构篇(第11-13章):输出多边形构建、Clipper64 和 ClipperD 类
- 布尔运算篇(第14-15章):执行流程、填充规则
- 高级功能篇(第16-18章):偏移、矩形裁剪、Minkowski 运算
- 进阶篇(第19-20章):PolyTree 结构、实际应用
希望本教程能帮助你深入理解 Clipper2 的实现原理,在实际项目中更好地应用这个优秀的几何库。