第六章 高级应用与性能优化(C#版)
6.1 引言
在前面的章节中,我们学习了Clipper2的核心功能:布尔运算、多边形偏移、矩形裁剪和闵可夫斯基运算。本章将深入探讨Clipper2 C#版本的高级应用技巧、性能优化方法以及与其他.NET工具和系统的集成方式。通过本章的学习,您将能够在实际.NET项目中更加高效地使用Clipper2。
6.2 Z轴值支持(USINGZ)
6.2.1 启用Z轴支持
Clipper2支持在每个顶点上附加一个Z轴值。这个功能需要通过条件编译启用(在项目中定义 USINGZ 编译常量):
在项目配置中启用
<!-- .csproj:定义 USINGZ 编译常量 -->
<PropertyGroup>
<DefineConstants>USINGZ</DefineConstants>
</PropertyGroup>
启用
USINGZ后,C# 版本库的命名空间会切换为Clipper2ZLib,此时Point64才会带有Z字段。
C#
在C#版本中,需要引用USINGZ程序集(定义 USINGZ 常量后编译 Clipper2 库,得到使用 Clipper2ZLib 命名空间的版本):
using Clipper2ZLib; // 启用Z值支持的命名空间
6.2.2 Z值的用途
Z值可以用于多种目的:
存储顶点标识
// 为每个顶点分配唯一ID
Path64 path = new Path64();
for (int i = 0; i < points.Count; i++) {
path.Add(new Point64(points[i].X, points[i].Y, i)); // Z = 顶点索引
}
存储高程数据
// 在GIS应用中存储高程
Path64 contour = new Path64();
foreach (var pt in terrainPoints) {
contour.Add(new Point64(
(long)(pt.x * 1000),
(long)(pt.y * 1000),
(long)(pt.elevation * 1000)
));
}
存储自定义属性
// 存储颜色索引或材质ID
Path64 polygon = new Path64();
foreach (var vertex in vertices) {
polygon.Add(new Point64(
vertex.x, vertex.y,
vertex.materialId // 材质ID作为Z值
));
}
6.2.3 Z值回调函数
当Clipper2执行布尔运算时,可能会产生新的顶点(在两边相交处)。通过设置回调函数,可以控制这些新顶点的Z值如何计算:
using Clipper2ZLib; // USINGZ 编译版本
// Z值回调函数
static void ZCallback(Point64 e1bot, Point64 e1top,
Point64 e2bot, Point64 e2top,
ref Point64 pt) {
// 使用线性插值计算新顶点的Z值
// 计算交点在edge1上的位置比例
double t1 = 0.5; // 简化处理,实际应该根据交点位置计算
// 插值Z值
long z1 = e1bot.Z + (long)((e1top.Z - e1bot.Z) * t1);
long z2 = e2bot.Z + (long)((e2top.Z - e2bot.Z) * 0.5);
// 使用平均值
pt.Z = (z1 + z2) / 2;
}
static void Main() {
Clipper64 clipper = new Clipper64();
// 设置Z值回调
clipper.ZCallback = ZCallback;
// 添加多边形(带Z值)
Paths64 subject = new Paths64();
Path64 path = new Path64();
path.Add(new Point64(0, 0, 100));
path.Add(new Point64(100, 0, 200));
path.Add(new Point64(100, 100, 300));
path.Add(new Point64(0, 100, 400));
subject.Add(path);
clipper.AddSubject(subject);
clipper.AddClip(clipPaths);
Paths64 result = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
// result中的顶点包含计算后的Z值
}
6.2.4 C#中的Z值回调
using Clipper2ZLib; // USINGZ 编译版本
class Program
{
// Z值回调委托
static void MyZCallback(Point64 e1bot, Point64 e1top,
Point64 e2bot, Point64 e2top,
ref Point64 pt)
{
// 计算新顶点的Z值
pt.Z = (e1bot.Z + e1top.Z + e2bot.Z + e2top.Z) / 4;
}
static void Main()
{
Clipper64 clipper = new Clipper64();
clipper.ZCallback = MyZCallback;
// ... 执行运算
}
}
6.3 输出格式控制
6.3.1 PolyTree与Paths的选择
Clipper2支持两种输出格式:
Paths输出
Clipper64 clipper = new Clipper64();
clipper.AddSubject(subject);
clipper.AddClip(clip);
Paths64 result = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
// result是一个扁平的路径列表,不包含层次信息
PolyTree输出
Clipper64 clipper = new Clipper64();
clipper.AddSubject(subject);
clipper.AddClip(clip);
PolyTree64 tree = new PolyTree64();
Paths64 openPaths = new Paths64(); // 开放路径输出
clipper.Execute(ClipType.Intersection, FillRule.NonZero, tree, openPaths);
// tree包含完整的层次信息
选择建议
| 场景 | 推荐格式 |
|---|---|
| 简单的多边形处理 | Paths |
| 需要区分外边界和孔洞 | PolyTree |
| 后续需要进行嵌套分析 | PolyTree |
| 性能敏感的场景 | Paths(略快) |
| 需要遍历层次结构 | PolyTree |
6.3.2 保留共线点
默认情况下,Clipper2会移除共线的点(位于同一直线上的中间点)。可以通过设置选项保留这些点:
Clipper64 clipper = new Clipper64();
clipper.PreserveCollinear = true; // 保留共线点
clipper.AddSubject(subject);
Paths64 result = new Paths64();
clipper.Execute(ClipType.Union, FillRule.NonZero, result);
应用场景
- 需要保持原始顶点数量
- 后续处理依赖于特定的顶点位置
- 与其他系统交换数据时需要保持一致性
6.3.3 反转输出方向
可以设置输出多边形的方向反转:
Clipper64 clipper = new Clipper64();
clipper.ReverseSolution = true; // 反转输出方向
// 原本逆时针的外边界会变成顺时针
// 原本顺时针的孔洞会变成逆时针
应用场景
- 与使用不同顶点顺序约定的系统集成
- 图形渲染中的背面剔除
6.4 错误处理与验证
6.4.1 输入验证
// 验证路径是否有效
static bool ValidatePath(Path64 path) {
// 至少需要3个顶点
if (path.Count < 3) {
return false;
}
// 检查是否有重复的相邻顶点
for (int i = 0; i < path.Count; i++) {
if (path[i] == path[(i + 1) % path.Count]) {
return false;
}
}
// 检查面积是否为零
double area = Clipper.Area(path);
if (Math.Abs(area) < 1.0) {
return false;
}
return true;
}
// 验证所有路径
static bool ValidatePaths(Paths64 paths) {
foreach (var path in paths) {
if (!ValidatePath(path)) {
return false;
}
}
return true;
}
6.4.2 结果验证
// 验证布尔运算结果
static bool ValidateResult(Paths64 result, ClipType clipType,
Paths64 subject, Paths64 clip) {
if (result.Count == 0) {
// 对于某些情况,空结果可能是正确的
if (clipType == ClipType.Intersection) {
// 如果没有相交,结果可以为空
return true;
}
}
// 检查结果是否有效
foreach (var path in result) {
if (path.Count < 3) {
return false;
}
}
// 可以添加更多验证逻辑...
return true;
}
6.4.3 异常处理
try {
Clipper64 clipper = new Clipper64();
clipper.AddSubject(subject);
clipper.AddClip(clip);
Paths64 result = new Paths64();
bool success = clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
if (!success) {
// 处理执行失败
Console.Error.WriteLine("Clipper执行失败");
}
} catch (Exception e) {
Console.Error.WriteLine("异常: " + e.Message);
}
6.5 性能优化技巧
6.5.1 减少顶点数量
顶点数量是影响性能的主要因素。可以通过路径简化来减少顶点:
// 使用Douglas-Peucker算法简化路径
PathsD simplified = Clipper.SimplifyPaths(paths, tolerance);
// tolerance值越大,简化越多,但形状变形也越大
// 或者使用Ramer-Douglas-Peucker变体
PathsD rdpSimplified = Clipper.RamerDouglasPeucker(paths, tolerance);
选择合适的容差
// 根据应用场景选择容差
double tolerance;
if (isScreenRendering) {
// 屏幕渲染:1像素以下的细节不可见
tolerance = 1.0;
} else if (isCNC) {
// CNC加工:保持0.01mm精度
tolerance = 10; // 假设单位是0.001mm
} else if (isGIS) {
// GIS应用:根据地图比例尺选择
tolerance = mapScale / 1000.0;
}
6.5.2 使用边界框预筛选
在执行布尔运算之前,使用边界框快速排除不可能相交的情况:
static bool MayIntersect(Paths64 paths1, Paths64 paths2) {
Rect64 bounds1 = Clipper.GetBounds(paths1);
Rect64 bounds2 = Clipper.GetBounds(paths2);
return bounds1.Intersects(bounds2);
}
// 使用预筛选
if (MayIntersect(subject, clip)) {
Paths64 result = Clipper.Intersect(subject, clip, FillRule.NonZero);
// 处理结果
} else {
// 不相交,跳过计算
}
6.5.3 批量操作优化
// 不优化的方式:每次创建新的Clipper对象
foreach (var subject in subjects) {
Clipper64 clipper = new Clipper64(); // 每次创建新对象
clipper.AddSubject(subject);
clipper.AddClip(clip);
Paths64 result = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
// 优化的方式:复用Clipper对象
Clipper64 clipper2 = new Clipper64();
foreach (var subject in subjects) {
clipper2.Clear(); // 清空但保留内存分配
clipper2.AddSubject(subject);
clipper2.AddClip(clip);
Paths64 result = new Paths64();
clipper2.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
6.5.4 并行处理
using System.Threading.Tasks;
static Paths64[] ParallelProcess(List<Paths64> subjects, Paths64 clip, int numThreads)
{
Paths64[] results = new Paths64[subjects.Count];
Parallel.For(0, subjects.Count, i => {
// 每个线程有自己的Clipper实例
Clipper64 clipper = new Clipper64();
clipper.AddSubject(subjects[i]);
clipper.AddClip(clip);
Paths64 result = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
results[i] = result;
});
return results;
}
6.5.5 内存优化
// 预分配内存
Paths64 subject = new Paths64(100); // 预分配100个路径的空间
for (int i = 0; i < 100; i++) {
Path64 path = new Path64(1000); // 每个路径预分配1000个顶点
// ... 填充path
subject.Add(path);
}
6.5.6 选择合适的数据类型
// 如果坐标范围较小,可以考虑使用较小的类型
// 但Clipper2默认使用long(Int64)以确保精度
// 对于浮点数,如果不需要高精度,可以降低精度
PathsD floatPaths = ...;
int precision = 2; // 只保留2位小数
Paths64 intPaths = ConvertToInt64(floatPaths, Math.Pow(10, precision));
6.6 与其他库的集成
6.6.1 与OpenGL集成
using OpenTK.Graphics.OpenGL;
static void RenderPaths(Paths64 paths) {
foreach (var path in paths) {
GL.Begin(PrimitiveType.LineLoop);
foreach (var pt in path) {
GL.Vertex2(pt.X / 1000.0, pt.Y / 1000.0);
}
GL.End();
}
}
static void RenderFilledPaths(Paths64 paths) {
// 使用三角化(需要额外的三角化库)
List<Triangle> triangles = Triangulate(paths);
GL.Begin(PrimitiveType.Triangles);
foreach (var tri in triangles) {
GL.Vertex2(tri.a.X / 1000.0, tri.a.Y / 1000.0);
GL.Vertex2(tri.b.X / 1000.0, tri.b.Y / 1000.0);
GL.Vertex2(tri.c.X / 1000.0, tri.c.Y / 1000.0);
}
GL.End();
}
6.6.2 与SVG集成
Clipper2提供了SVG辅助工具:
using Clipper2Lib;
static void SaveToSVG(Paths64 paths, string filename) {
SvgWriter writer = new SvgWriter();
writer.AddPaths(paths, true, // 闭合路径
FillRule.NonZero,
0x1000AA00, // 填充颜色
0xFF009900, // 描边颜色
1); // 描边宽度
writer.SaveToFile(filename, 800, 600); // 800x600的SVG
}
6.6.3 与GeoJSON集成
using System.Text.Json;
using System.Linq;
// 将Paths转换为GeoJSON
static JsonElement PathsToGeoJSON(Paths64 paths, double scale = 1000.0) {
var features = new List<object>();
foreach (var path in paths) {
var coordinates = new List<List<double>>();
foreach (var pt in path) {
coordinates.Add(new List<double> { pt.X / scale, pt.Y / scale });
}
// 闭合多边形
coordinates.Add(new List<double> { path[0].X / scale, path[0].Y / scale });
features.Add(new {
type = "Feature",
geometry = new {
type = "Polygon",
coordinates = new List<object> { coordinates }
},
properties = new { }
});
}
return JsonSerializer.SerializeToDocument(new {
type = "FeatureCollection",
features
}).RootElement;
}
// 从GeoJSON读取Paths
static Paths64 GeoJSONToPaths(JsonDocument geojson, double scale = 1000.0) {
Paths64 result = new Paths64();
foreach (var feature in geojson.RootElement.GetProperty("features").EnumerateArray()) {
var geometry = feature.GetProperty("geometry");
if (geometry.GetProperty("type").GetString() == "Polygon") {
foreach (var ring in geometry.GetProperty("coordinates").EnumerateArray()) {
Path64 path = new Path64();
var points = ring.EnumerateArray().ToList();
for (int i = 0; i < points.Count - 1; i++) { // 跳过闭合点
path.Add(new Point64(
(long)(points[i][0].GetDouble() * scale),
(long)(points[i][1].GetDouble() * scale)
));
}
result.Add(path);
}
}
}
return result;
}
6.6.4 与GDAL/OGR集成
using OSGeo.OGR;
// 从OGR几何体转换
static Paths64 OGRGeometryToPaths(Geometry geom, double scale = 1000.0) {
Paths64 result = new Paths64();
if (geom.GetGeometryType() == wkbGeometryType.wkbPolygon) {
Polygon polygon = (Polygon)geom;
// 外环
LinearRing exteriorRing = polygon.GetExteriorRing();
Path64 exterior = new Path64();
for (int i = 0; i < exteriorRing.GetPointCount() - 1; i++) {
double[] pt = new double[2];
exteriorRing.GetPoint(i, pt);
exterior.Add(new Point64(
(long)(pt[0] * scale),
(long)(pt[1] * scale)
));
}
result.Add(exterior);
// 内环(孔洞)
for (int r = 0; r < polygon.GetInteriorRingCount(); r++) {
LinearRing ring = polygon.GetInteriorRing(r);
Path64 hole = new Path64();
for (int i = 0; i < ring.GetPointCount() - 1; i++) {
double[] pt = new double[2];
ring.GetPoint(i, pt);
hole.Add(new Point64(
(long)(pt[0] * scale),
(long)(pt[1] * scale)
));
}
result.Add(hole);
}
}
return result;
}
// 转换回OGR几何体
static Geometry PathsToOGRGeometry(Paths64 paths, double scale = 1000.0) {
if (paths.Count == 0) return null;
Polygon polygon = new Polygon();
for (int i = 0; i < paths.Count; i++) {
var path = paths[i];
LinearRing ring = new LinearRing();
foreach (var pt in path) {
ring.AddPoint(pt.X / scale, pt.Y / scale, 0);
}
ring.CloseRings();
polygon.AddRing(ring);
}
return polygon;
}
6.7 调试与可视化
6.7.1 使用SVG进行调试
static void DebugVisualize(Paths64 subject,
Paths64 clip,
Paths64 result,
string filename) {
SvgWriter svg = new SvgWriter();
// 绘制主体(半透明蓝色)
svg.AddPaths(subject, true, FillRule.NonZero,
0x200000FF, 0xFF0000FF, 2);
// 绘制裁剪区域(半透明红色)
svg.AddPaths(clip, true, FillRule.NonZero,
0x20FF0000, 0xFFFF0000, 2);
// 绘制结果(半透明绿色)
svg.AddPaths(result, true, FillRule.NonZero,
0x4000FF00, 0xFF00FF00, 3);
svg.SaveToFile(filename, 800, 600);
}
6.7.2 打印路径信息
static void PrintPathInfo(Paths64 paths, string name) {
Console.WriteLine("===== " + name + " =====");
Console.WriteLine("路径数量: " + paths.Count);
int totalVertices = 0;
foreach (var path in paths) {
totalVertices += path.Count;
}
Console.WriteLine("总顶点数: " + totalVertices);
double totalArea = Clipper.Area(paths);
Console.WriteLine("总面积: " + totalArea);
Rect64 bounds = Clipper.GetBounds(paths);
Console.WriteLine("边界框: (" + bounds.left + ", " + bounds.top
+ ") - (" + bounds.right + ", " + bounds.bottom + ")");
for (int i = 0; i < paths.Count; i++) {
var path = paths[i];
double area = Clipper.Area(path);
bool isHole = area < 0;
Console.WriteLine(" 路径 " + i + ": "
+ path.Count + " 顶点, "
+ "面积 " + Math.Abs(area)
+ (isHole ? " (孔洞)" : " (外边界)"));
}
}
6.7.3 性能分析
using System.Diagnostics;
class Timer : IDisposable {
private readonly string name_;
private readonly Stopwatch stopwatch_ = Stopwatch.StartNew();
public Timer(string name) {
name_ = name;
}
public void Dispose() {
stopwatch_.Stop();
Console.WriteLine(name_ + ": " + stopwatch_.Elapsed.TotalMilliseconds + " ms");
}
}
// 使用
static void ProfileOperation() {
Paths64 subject = ...;
Paths64 clip = ...;
using (Timer t = new Timer("交集运算")) {
Paths64 result = Clipper.Intersect(subject, clip, FillRule.NonZero);
}
using (Timer t = new Timer("偏移运算")) {
Paths64 result = Clipper.InflatePaths(subject, 10, JoinType.Round, EndType.Polygon);
}
}
6.8 最佳实践
6.8.1 代码组织
// 封装Clipper2操作的工具类
class GeometryProcessor {
private double scale_;
public GeometryProcessor() {
scale_ = 1000.0;
}
// 设置精度
public void SetScale(double scale) { scale_ = scale; }
// 布尔运算接口
public PathsD Intersect(PathsD subject, PathsD clip) {
Paths64 subj64 = ConvertToInt(subject);
Paths64 clip64 = ConvertToInt(clip);
Paths64 result = Clipper.Intersect(subj64, clip64, FillRule.NonZero);
return ConvertToDouble(result);
}
public PathsD Union(PathsD paths) {
Paths64 paths64 = ConvertToInt(paths);
Paths64 result = Clipper.Union(paths64, FillRule.NonZero);
return ConvertToDouble(result);
}
public PathsD Offset(PathsD paths, double delta) {
Paths64 paths64 = ConvertToInt(paths);
long delta64 = (long)(delta * scale_);
Paths64 result = Clipper.InflatePaths(paths64, delta64, JoinType.Round, EndType.Polygon);
return ConvertToDouble(result);
}
private Paths64 ConvertToInt(PathsD paths) {
return Clipper.ScalePaths64(paths, scale_);
}
private PathsD ConvertToDouble(Paths64 paths) {
return Clipper.ScalePathsD(paths, 1.0 / scale_);
}
}
6.8.2 错误处理策略
class ClipperException : Exception {
public ClipperException(string msg) : base(msg) {}
}
static Paths64 SafeIntersect(Paths64 subject, Paths64 clip) {
// 输入验证
if (subject.Count == 0) {
throw new ClipperException("主体多边形为空");
}
if (clip.Count == 0) {
throw new ClipperException("裁剪多边形为空");
}
// 检查边界框是否相交
Rect64 subjectBounds = Clipper.GetBounds(subject);
Rect64 clipBounds = Clipper.GetBounds(clip);
if (!subjectBounds.Intersects(clipBounds)) {
return new Paths64(); // 不相交,返回空
}
try {
Clipper64 clipper = new Clipper64();
clipper.AddSubject(subject);
clipper.AddClip(clip);
Paths64 result = new Paths64();
bool success = clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
if (!success) {
throw new ClipperException("Clipper执行失败");
}
return result;
} catch (Exception e) {
throw new ClipperException("Clipper错误: " + e.Message);
}
}
6.8.3 配置管理
struct ClipperConfig {
public double scale;
public FillRule fillRule;
public JoinType joinType;
public EndType endType;
public double miterLimit;
public double arcTolerance;
public bool preserveCollinear;
public bool reverseSolution;
public ClipperConfig() {
scale = 1000.0;
fillRule = FillRule.NonZero;
joinType = JoinType.Round;
endType = EndType.Polygon;
miterLimit = 2.0;
arcTolerance = 0.25;
preserveCollinear = false;
reverseSolution = false;
}
}
class ConfigurableClipper {
private ClipperConfig config_;
public void SetConfig(ClipperConfig config) {
config_ = config;
}
public Paths64 Offset(Paths64 paths, double delta) {
ClipperOffset offsetter = new ClipperOffset();
offsetter.MiterLimit = config_.miterLimit;
offsetter.ArcTolerance = config_.arcTolerance;
offsetter.PreserveCollinear = config_.preserveCollinear;
offsetter.ReverseSolution = config_.reverseSolution;
offsetter.AddPaths(paths, config_.joinType, config_.endType);
Paths64 result = new Paths64();
offsetter.Execute(delta * config_.scale, result);
return result;
}
}
6.9 常见陷阱与解决方案
6.9.1 坐标范围溢出
// 错误:坐标过大可能导致溢出
Path64 badPath = Clipper.MakePath(new long[] {
(long)1e18, 0,
(long)1e18, (long)1e18,
0, (long)1e18
});
// 正确:使用合理的缩放
const double scale = 1000000.0; // 6位小数精度
Path64 goodPath = Clipper.MakePath(new long[] {
(long)(1e12 * scale), 0,
(long)(1e12 * scale), (long)(1e12 * scale),
0, (long)(1e12 * scale)
});
6.9.2 填充规则不匹配
// 问题:使用不匹配的填充规则
Paths64 paths = new Paths64();
paths.Add(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 })); // 逆时针
paths.Add(Clipper.MakePath(new long[] { 25, 25, 25, 75, 75, 75, 75, 25 })); // 顺时针孔洞
// 使用EvenOdd规则可能不会正确识别孔洞
// Paths64 result = Clipper.Union(paths, FillRule.EvenOdd);
// 正确:使用NonZero规则并确保方向正确
Paths64 result = Clipper.Union(paths, FillRule.NonZero);
6.9.3 精度损失
// 问题:浮点数精度不足
double x = 1.23456789012345; // 精度可能丢失
// 解决:使用足够的缩放因子
long xInt = (long)(x * 1e10); // 保留10位小数
6.10 本章小结
本章我们学习了Clipper2的高级应用技巧:
- Z轴支持:启用USINGZ、Z值用途、Z值回调
- 输出格式控制:PolyTree与Paths选择、保留共线点、反转输出
- 错误处理:输入验证、结果验证、异常处理
- 性能优化:减少顶点、边界框预筛选、批量操作、并行处理、内存优化
- 与其他库集成:OpenGL、SVG、GeoJSON、GDAL/OGR
- 调试与可视化:SVG调试、打印信息、性能分析
- 最佳实践:代码组织、错误处理策略、配置管理
- 常见陷阱:坐标溢出、填充规则不匹配、精度损失
通过本章的学习,您应该能够在实际项目中更加高效、可靠地使用Clipper2库。