第六章 高级应用与性能优化(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的高级应用技巧:

  1. Z轴支持:启用USINGZ、Z值用途、Z值回调
  2. 输出格式控制:PolyTree与Paths选择、保留共线点、反转输出
  3. 错误处理:输入验证、结果验证、异常处理
  4. 性能优化:减少顶点、边界框预筛选、批量操作、并行处理、内存优化
  5. 与其他库集成:OpenGL、SVG、GeoJSON、GDAL/OGR
  6. 调试与可视化:SVG调试、打印信息、性能分析
  7. 最佳实践:代码组织、错误处理策略、配置管理
  8. 常见陷阱:坐标溢出、填充规则不匹配、精度损失

通过本章的学习,您应该能够在实际项目中更加高效、可靠地使用Clipper2库。


← 上一章 目录