第4章_多边形偏移操作

第四章 多边形偏移操作

4.1 引言

多边形偏移(Polygon Offsetting),也称为缓冲区生成(Buffering)、膨胀(Inflation)或收缩(Deflation),是几何处理中的另一个核心功能。它指的是将多边形的边界向外或向内平移指定的距离,生成一个新的多边形。这个看似简单的操作,在实际应用中却非常复杂,需要处理各种边角情况、边的连接方式、自相交等问题。

Clipper1 提供了强大而灵活的多边形偏移功能,通过 ClipperOffset 类实现。它能够正确处理各种复杂情况,并提供多种连接类型和端点类型选项,满足不同应用场景的需求。

本章将深入讲解多边形偏移的原理、ClipperOffset 类的使用方法、各种参数的选择以及实际应用案例。

4.2 偏移操作基础

4.2.1 什么是多边形偏移

多边形偏移是指将多边形的每条边沿着其法线方向移动指定的距离。对于封闭多边形:

  • 正偏移(正值):边界向外扩展,多边形变大
  • 负偏移(负值):边界向内收缩,多边形变小

对于开放路径(线段):

  • 偏移会在线段两侧创建平行线
  • 端点的处理方式可以自定义

4.2.2 偏移操作的应用场景

工业制造

  • CNC 铣削刀具路径补偿
  • 激光切割路径生成
  • 3D 打印轮廓计算
  • 线切割编程

地理信息系统(GIS)

  • 缓冲区分析
  • 影响范围评估
  • 道路规划中的车道线生成
  • 洪水淹没范围预测

建筑设计

  • 建筑红线退让
  • 消防通道规划
  • 绿化带规划
  • 日照分析

游戏开发

  • 角色移动区域计算
  • 视野范围计算
  • 安全区域生成
  • 碰撞检测预处理

电子设计

  • PCB 走线规则检查
  • 铜箔间距计算
  • 焊盘设计

4.2.3 ClipperOffset 类简介

ClipperOffset 是 Clipper1 中专门用于多边形偏移的类,其基本使用流程如下:

using ClipperLib;
using Path = System.Collections.Generic.List<ClipperLib.IntPoint>;
using Paths = System.Collections.Generic.List<System.Collections.Generic.List<ClipperLib.IntPoint>>;

// 1. 创建 ClipperOffset 对象
ClipperOffset offset = new ClipperOffset();

// 2. 添加路径
offset.AddPath(path, JoinType.jtRound, EndType.etClosedPolygon);
// 或批量添加
offset.AddPaths(paths, JoinType.jtRound, EndType.etClosedPolygon);

// 3. 执行偏移
Paths solution = new Paths();
offset.Execute(ref solution, delta);

// 4. 处理结果
foreach (var resultPath in solution)
{
    // 处理偏移后的路径
}

4.3 JoinType(连接类型)

4.3.1 三种连接类型

JoinType 定义了多边形边在角点处的连接方式,有三种选择:

public enum JoinType
{
    jtSquare,  // 方形连接
    jtRound,   // 圆形连接
    jtMiter    // 斜接连接
}

4.3.2 jtRound(圆形连接)

特点

  • 在角点处创建圆弧
  • 平滑、美观的外观
  • 适合需要流畅视觉效果的场景

应用场景

  • 用户界面元素
  • 艺术设计
  • 需要平滑边缘的场合

示例代码

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>>;

class RoundJoinExample
{
    static void DemonstrateRoundJoin()
    {
        const double SCALE = 1000.0;

        // 创建一个正方形
        Path square = new Path
        {
            new IntPoint(0, 0),
            new IntPoint((long)(100 * SCALE), 0),
            new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
            new IntPoint(0, (long)(100 * SCALE))
        };

        // 使用圆形连接进行偏移
        ClipperOffset offset = new ClipperOffset();
        offset.AddPath(square, JoinType.jtRound, EndType.etClosedPolygon);

        Paths solution = new Paths();
        offset.Execute(ref solution, 10 * SCALE);  // 向外偏移10个单位

        Console.WriteLine("圆形连接偏移结果:");
        Console.WriteLine($"  产生路径数: {solution.Count}");
        if (solution.Count > 0)
        {
            Console.WriteLine($"  顶点数: {solution[0].Count}");
            double area = Math.Abs(Clipper.Area(solution[0])) / (SCALE * SCALE);
            Console.WriteLine($"  面积: {area:F2}");
        }
    }
}

圆弧质量控制

可以通过设置 ArcTolerance 属性来控制圆弧的精度:

ClipperOffset offset = new ClipperOffset();
offset.ArcTolerance = 0.25 * SCALE;  // 圆弧容差,值越小越精确
offset.AddPath(path, JoinType.jtRound, EndType.etClosedPolygon);
  • ArcTolerance 默认值约为 0.25
  • 值越小,圆弧越精确,但顶点数越多
  • 值越大,圆弧越粗糙,但性能更好

4.3.3 jtSquare(方形连接)

特点

  • 在角点处创建直角
  • 计算简单,性能好
  • 生成的顶点数最少

应用场景

  • 需要直角效果的场合
  • 性能要求高的场景
  • 像素化的图形处理

示例代码

static void DemonstrateSquareJoin()
{
    const double SCALE = 1000.0;

    // 创建一个L形路径
    Path lShape = new Path
    {
        new IntPoint(0, 0),
        new IntPoint((long)(50 * SCALE), 0),
        new IntPoint((long)(50 * SCALE), (long)(50 * SCALE)),
        new IntPoint((long)(100 * SCALE), (long)(50 * SCALE)),
        new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
        new IntPoint(0, (long)(100 * SCALE))
    };

    // 使用方形连接进行偏移
    ClipperOffset offset = new ClipperOffset();
    offset.AddPath(lShape, JoinType.jtSquare, EndType.etClosedPolygon);

    Paths solution = new Paths();
    offset.Execute(ref solution, 5 * SCALE);

    Console.WriteLine("方形连接偏移结果:");
    Console.WriteLine($"  产生路径数: {solution.Count}");
    if (solution.Count > 0)
    {
        Console.WriteLine($"  顶点数: {solution[0].Count}");
        double area = Math.Abs(Clipper.Area(solution[0])) / (SCALE * SCALE);
        Console.WriteLine($"  面积: {area:F2}");
    }
}

4.3.4 jtMiter(斜接连接)

特点

  • 延伸边直到相交形成尖角
  • 保持原始角度
  • 可能产生很长的尖角

应用场景

  • 需要保持精确角度的场合
  • 建筑设计图纸
  • 机械零件设计
  • 需要锐角效果的情况

斜接限制(MiterLimit)

为了避免产生过长的尖角,可以设置 MiterLimit 属性:

ClipperOffset offset = new ClipperOffset();
offset.MiterLimit = 2.0;  // 斜接限制
offset.AddPath(path, JoinType.jtMiter, EndType.etClosedPolygon);

MiterLimit 的含义:

  • 表示斜接长度与偏移距离的最大比值
  • 默认值为 2.0
  • 当斜接长度超过限制时,自动切换为方形连接
  • 较小的值会产生更多的方形切角
  • 较大的值允许更尖的角

示例代码

static void DemonstrateMiterJoin()
{
    const double SCALE = 1000.0;

    // 创建一个带锐角的多边形
    Path sharpPolygon = new Path
    {
        new IntPoint(0, (long)(50 * SCALE)),
        new IntPoint((long)(50 * SCALE), 0),  // 锐角顶点
        new IntPoint((long)(100 * SCALE), (long)(50 * SCALE)),
        new IntPoint((long)(50 * SCALE), (long)(100 * SCALE))
    };

    // 测试不同的 MiterLimit 值
    double[] miterLimits = { 1.5, 2.0, 3.0, 5.0 };

    foreach (double limit in miterLimits)
    {
        ClipperOffset offset = new ClipperOffset();
        offset.MiterLimit = limit;
        offset.AddPath(sharpPolygon, JoinType.jtMiter, EndType.etClosedPolygon);

        Paths solution = new Paths();
        offset.Execute(ref solution, 10 * SCALE);

        Console.WriteLine($"\nMiterLimit = {limit}:");
        if (solution.Count > 0)
        {
            Console.WriteLine($"  顶点数: {solution[0].Count}");
            double area = Math.Abs(Clipper.Area(solution[0])) / (SCALE * SCALE);
            Console.WriteLine($"  面积: {area:F2}");
        }
    }
}

4.3.5 连接类型对比

class JoinTypeComparison
{
    private const double SCALE = 1000.0;

    static void CompareJoinTypes()
    {
        // 创建一个测试多边形(星形)
        Path star = CreateStar(50, 50, 40, 20, 5, SCALE);

        JoinType[] joinTypes = {
            JoinType.jtRound,
            JoinType.jtSquare,
            JoinType.jtMiter
        };

        string[] joinNames = { "圆形", "方形", "斜接" };

        Console.WriteLine("=== 连接类型对比 ===\n");

        for (int i = 0; i < joinTypes.Length; i++)
        {
            ClipperOffset offset = new ClipperOffset();

            if (joinTypes[i] == JoinType.jtMiter)
            {
                offset.MiterLimit = 2.0;
            }
            else if (joinTypes[i] == JoinType.jtRound)
            {
                offset.ArcTolerance = 0.25 * SCALE;
            }

            offset.AddPath(star, joinTypes[i], EndType.etClosedPolygon);

            Paths solution = new Paths();
            offset.Execute(ref solution, 5 * SCALE);

            Console.WriteLine($"{joinNames[i]}连接:");
            if (solution.Count > 0)
            {
                Console.WriteLine($"  顶点数: {solution[0].Count}");
                double area = Math.Abs(Clipper.Area(solution[0])) / (SCALE * SCALE);
                Console.WriteLine($"  面积: {area:F2}");
                double perimeter = CalculatePerimeter(solution[0]) / SCALE;
                Console.WriteLine($"  周长: {perimeter:F2}");
            }
            Console.WriteLine();
        }
    }

    // 创建星形
    static Path CreateStar(double cx, double cy, double outerRadius,
        double innerRadius, int points, double scale)
    {
        Path star = new Path();
        int totalPoints = points * 2;

        for (int i = 0; i < totalPoints; i++)
        {
            double angle = Math.PI * 2 * i / totalPoints - Math.PI / 2;
            double radius = (i % 2 == 0) ? outerRadius : innerRadius;
            double x = cx + radius * Math.Cos(angle);
            double y = cy + radius * Math.Sin(angle);
            star.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
        }

        return star;
    }

    // 计算周长
    static double CalculatePerimeter(Path path)
    {
        if (path.Count < 2) return 0;

        double perimeter = 0;
        for (int i = 0; i < path.Count; i++)
        {
            int next = (i + 1) % path.Count;
            long dx = path[next].X - path[i].X;
            long dy = path[next].Y - path[i].Y;
            perimeter += Math.Sqrt(dx * dx + dy * dy);
        }
        return perimeter;
    }
}

4.4 EndType(端点类型)

4.4.1 五种端点类型

EndType 定义了路径端点的处理方式:

public enum EndType
{
    etClosedPolygon,  // 封闭多边形
    etClosedLine,     // 封闭线
    etOpenSquare,     // 开放路径,方形端点
    etOpenRound,      // 开放路径,圆形端点
    etOpenButt        // 开放路径,平切端点
}

4.4.2 etClosedPolygon(封闭多边形)

用途

  • 用于封闭的多边形
  • 最常用的端点类型
  • 自动闭合路径

示例

static void ClosedPolygonExample()
{
    const double SCALE = 1000.0;

    Path hexagon = CreateRegularPolygon(50, 50, 30, 6, SCALE);

    ClipperOffset offset = new ClipperOffset();
    offset.AddPath(hexagon, JoinType.jtRound, EndType.etClosedPolygon);

    Paths solution = new Paths();
    offset.Execute(ref solution, 10 * SCALE);

    Console.WriteLine("封闭多边形偏移:");
    if (solution.Count > 0)
    {
        double originalArea = Math.Abs(Clipper.Area(hexagon)) / (SCALE * SCALE);
        double offsetArea = Math.Abs(Clipper.Area(solution[0])) / (SCALE * SCALE);
        Console.WriteLine($"  原始面积: {originalArea:F2}");
        Console.WriteLine($"  偏移后面积: {offsetArea:F2}");
        Console.WriteLine($"  面积增加: {(offsetArea - originalArea):F2}");
    }
}

static Path CreateRegularPolygon(double cx, double cy, double radius,
    int sides, double scale)
{
    Path polygon = new Path();
    for (int i = 0; i < sides; i++)
    {
        double angle = Math.PI * 2 * i / sides;
        double x = cx + radius * Math.Cos(angle);
        double y = cy + radius * Math.Sin(angle);
        polygon.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
    }
    return polygon;
}

4.4.3 etClosedLine(封闭线)

用途

  • 将路径视为封闭的线条而非多边形
  • 线条的两侧都会偏移
  • 适合处理线框图形

与 etClosedPolygon 的区别

  • etClosedPolygon:将路径视为多边形的边界
  • etClosedLine:将路径视为中心线,两侧都会偏移

示例

static void ClosedLineExample()
{
    const double SCALE = 1000.0;

    // 创建一个圆形路径
    Path circle = CreateCircle(50, 50, 30, 32, SCALE);

    // 作为封闭多边形偏移
    ClipperOffset offset1 = new ClipperOffset();
    offset1.AddPath(circle, JoinType.jtRound, EndType.etClosedPolygon);
    Paths solution1 = new Paths();
    offset1.Execute(ref solution1, 5 * SCALE);

    // 作为封闭线偏移
    ClipperOffset offset2 = new ClipperOffset();
    offset2.AddPath(circle, JoinType.jtRound, EndType.etClosedLine);
    Paths solution2 = new Paths();
    offset2.Execute(ref solution2, 5 * SCALE);

    Console.WriteLine("封闭多边形 vs 封闭线:");
    Console.WriteLine($"  封闭多边形产生 {solution1.Count} 个路径");
    Console.WriteLine($"  封闭线产生 {solution2.Count} 个路径");

    if (solution1.Count > 0)
    {
        double area1 = Math.Abs(Clipper.Area(solution1[0])) / (SCALE * SCALE);
        Console.WriteLine($"  封闭多边形面积: {area1:F2}");
    }

    double totalArea2 = 0;
    foreach (var path in solution2)
    {
        totalArea2 += Math.Abs(Clipper.Area(path));
    }
    Console.WriteLine($"  封闭线总面积: {(totalArea2 / (SCALE * SCALE)):F2}");
}

static Path CreateCircle(double cx, double cy, double radius,
    int segments, double scale)
{
    Path circle = new Path();
    for (int i = 0; i < segments; i++)
    {
        double angle = 2 * Math.PI * i / segments;
        double x = cx + radius * Math.Cos(angle);
        double y = cy + radius * Math.Sin(angle);
        circle.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
    }
    return circle;
}

4.4.4 开放路径的端点类型

etOpenSquare(方形端点)

  • 端点处延伸一个方形帽
  • 延伸距离等于偏移距离

etOpenRound(圆形端点)

  • 端点处添加半圆形帽
  • 产生平滑的端点

etOpenButt(平切端点)

  • 端点处不延伸
  • 直接平切

示例代码

class OpenPathEndTypes
{
    private const double SCALE = 1000.0;

    static void CompareOpenEndTypes()
    {
        // 创建一条开放路径(折线)
        Path openPath = new Path
        {
            new IntPoint(0, (long)(50 * SCALE)),
            new IntPoint((long)(50 * SCALE), 0),
            new IntPoint((long)(100 * SCALE), (long)(50 * SCALE)),
            new IntPoint((long)(150 * SCALE), 0)
        };

        EndType[] endTypes = {
            EndType.etOpenSquare,
            EndType.etOpenRound,
            EndType.etOpenButt
        };

        string[] endNames = { "方形", "圆形", "平切" };

        Console.WriteLine("=== 开放路径端点类型对比 ===\n");

        for (int i = 0; i < endTypes.Length; i++)
        {
            ClipperOffset offset = new ClipperOffset();
            offset.AddPath(openPath, JoinType.jtRound, endTypes[i]);

            Paths solution = new Paths();
            offset.Execute(ref solution, 5 * SCALE);

            Console.WriteLine($"{endNames[i]}端点:");
            if (solution.Count > 0)
            {
                Console.WriteLine($"  路径数: {solution.Count}");
                Console.WriteLine($"  顶点数: {solution[0].Count}");
                double area = Math.Abs(Clipper.Area(solution[0])) / (SCALE * SCALE);
                Console.WriteLine($"  面积: {area:F2}");
            }
            Console.WriteLine();
        }
    }
}

4.5 正偏移和负偏移

4.5.1 正偏移(膨胀)

正偏移使多边形向外扩展,主要应用:

缓冲区分析

class BufferAnalysis
{
    private const double SCALE = 1000000.0;

    /// <summary>
    /// 创建点的缓冲区
    /// </summary>
    public static Path CreatePointBuffer(double x, double y, double radius)
    {
        // 创建一个代表点的小圆
        Path point = CreateCircle(x, y, 0.1, 8, SCALE);

        ClipperOffset offset = new ClipperOffset();
        offset.AddPath(point, JoinType.jtRound, EndType.etClosedPolygon);

        Paths buffer = new Paths();
        offset.Execute(ref buffer, radius * SCALE);

        return buffer.Count > 0 ? buffer[0] : new Path();
    }

    /// <summary>
    /// 创建线的缓冲区
    /// </summary>
    public static Paths CreateLineBuffer(Path line, double width)
    {
        ClipperOffset offset = new ClipperOffset();
        offset.AddPath(line, JoinType.jtRound, EndType.etOpenRound);

        Paths buffer = new Paths();
        offset.Execute(ref buffer, width / 2.0 * SCALE);

        return buffer;
    }

    /// <summary>
    /// 创建多边形缓冲区
    /// </summary>
    public static Paths CreatePolygonBuffer(Path polygon, double distance)
    {
        ClipperOffset offset = new ClipperOffset();
        offset.AddPath(polygon, JoinType.jtRound, EndType.etClosedPolygon);

        Paths buffer = new Paths();
        offset.Execute(ref buffer, distance * SCALE);

        return buffer;
    }

    private static Path CreateCircle(double cx, double cy, double radius,
        int segments, double scale)
    {
        Path circle = new Path();
        for (int i = 0; i < segments; i++)
        {
            double angle = 2 * Math.PI * i / segments;
            double x = cx + radius * Math.Cos(angle);
            double y = cy + radius * Math.Sin(angle);
            circle.Add(new IntPoint((long)(x * scale), (long)(y * scale)));
        }
        return circle;
    }
}

4.5.2 负偏移(收缩)

负偏移使多边形向内收缩,主要应用:

内部区域计算

class ShrinkOperations
{
    private const double SCALE = 1000.0;

    /// <summary>
    /// 计算建筑物的室内有效面积(扣除墙体厚度)
    /// </summary>
    public static double CalculateInternalArea(Path building, double wallThickness)
    {
        ClipperOffset offset = new ClipperOffset();
        offset.AddPath(building, JoinType.jtMiter, EndType.etClosedPolygon);

        Paths shrunk = new Paths();
        offset.Execute(ref shrunk, -wallThickness * SCALE);

        if (shrunk.Count == 0)
        {
            Console.WriteLine("警告:墙体太厚,没有内部空间");
            return 0;
        }

        double internalArea = 0;
        foreach (var path in shrunk)
        {
            internalArea += Math.Abs(Clipper.Area(path));
        }

        return internalArea / (SCALE * SCALE);
    }

    /// <summary>
    /// 检查收缩是否会导致多边形消失
    /// </summary>
    public static bool CanShrink(Path polygon, double distance)
    {
        ClipperOffset offset = new ClipperOffset();
        offset.AddPath(polygon, JoinType.jtRound, EndType.etClosedPolygon);

        Paths result = new Paths();
        offset.Execute(ref result, -distance * SCALE);

        return result.Count > 0;
    }

    /// <summary>
    /// 计算最大可收缩距离
    /// </summary>
    public static double GetMaxShrinkDistance(Path polygon, double step = 0.1)
    {
        double maxDistance = 0;
        double testDistance = step;
        const double MAX_TEST = 1000;  // 防止无限循环

        while (testDistance < MAX_TEST)
        {
            if (CanShrink(polygon, testDistance))
            {
                maxDistance = testDistance;
                testDistance += step;
            }
            else
            {
                break;
            }
        }

        return maxDistance;
    }
}

4.5.3 多级偏移

可以连续执行多次偏移来创建同心轮廓:

class MultiLevelOffset
{
    private const double SCALE = 1000.0;

    /// <summary>
    /// 创建等高线效果
    /// </summary>
    public static List<Paths> CreateContours(Path baseShape,
        double startOffset, double step, int levels)
    {
        var contours = new List<Paths>();

        for (int i = 0; i < levels; i++)
        {
            double offsetDistance = startOffset + (i * step);

            ClipperOffset offset = new ClipperOffset();
            offset.AddPath(baseShape, JoinType.jtRound, EndType.etClosedPolygon);

            Paths contour = new Paths();
            offset.Execute(ref contour, offsetDistance * SCALE);

            if (contour.Count > 0)
            {
                contours.Add(contour);
            }
            else
            {
                break;  // 多边形已经收缩到消失
            }
        }

        return contours;
    }

    /// <summary>
    /// 创建洋葱圈效果(带与不带区域的交替)
    /// </summary>
    public static Paths CreateOnionRings(Path baseShape,
        double startOffset, double ringWidth, int rings)
    {
        Paths allRings = new Paths();

        for (int i = 0; i < rings; i++)
        {
            double outerOffset = startOffset + (i * ringWidth);
            double innerOffset = outerOffset + ringWidth;

            // 创建外圈
            ClipperOffset offset1 = new ClipperOffset();
            offset1.AddPath(baseShape, JoinType.jtRound, EndType.etClosedPolygon);
            Paths outer = new Paths();
            offset1.Execute(ref outer, outerOffset * SCALE);

            // 创建内圈
            ClipperOffset offset2 = new ClipperOffset();
            offset2.AddPath(baseShape, JoinType.jtRound, EndType.etClosedPolygon);
            Paths inner = new Paths();
            offset2.Execute(ref inner, innerOffset * SCALE);

            // 计算环形区域(外圈 - 内圈)
            if (outer.Count > 0)
            {
                Clipper clipper = new Clipper();
                clipper.AddPaths(outer, PolyType.ptSubject, true);
                if (inner.Count > 0)
                {
                    clipper.AddPaths(inner, PolyType.ptClip, true);
                }

                Paths ring = new Paths();
                clipper.Execute(ClipType.ctDifference, ring);

                if (ring.Count > 0)
                {
                    allRings.AddRange(ring);
                }
            }
        }

        return allRings;
    }
}

4.6 实际应用案例

4.6.1 CNC 刀具路径补偿

class CNCPathGeneration
{
    private const double SCALE = 1000.0;

    /// <summary>
    /// 生成CNC铣削刀具路径
    /// </summary>
    public static Paths GenerateToolPath(
        Path partOutline,
        double toolDiameter,
        double stepOver)
    {
        var toolPaths = new List<Paths>();
        double currentOffset = -(toolDiameter / 2.0);  // 从中心开始

        int iteration = 0;
        const int MAX_ITERATIONS = 100;

        while (iteration < MAX_ITERATIONS)
        {
            ClipperOffset offset = new ClipperOffset();
            offset.AddPath(partOutline, JoinType.jtRound, EndType.etClosedPolygon);

            Paths path = new Paths();
            offset.Execute(ref path, currentOffset * SCALE);

            if (path.Count == 0)
                break;  // 已经到达中心

            toolPaths.Add(path);

            currentOffset -= stepOver;
            iteration++;
        }

        Console.WriteLine($"生成了 {toolPaths.Count} 条刀具路径");

        // 合并所有路径
        Paths allPaths = new Paths();
        foreach (var paths in toolPaths)
        {
            allPaths.AddRange(paths);
        }

        return allPaths;
    }

    /// <summary>
    /// 计算加工时间估算
    /// </summary>
    public static double EstimateMachiningTime(
        Paths toolPaths,
        double feedRate)  // 单位:mm/min
    {
        double totalLength = 0;

        foreach (var path in toolPaths)
        {
            for (int i = 0; i < path.Count; i++)
            {
                int next = (i + 1) % path.Count;
                long dx = path[next].X - path[i].X;
                long dy = path[next].Y - path[i].Y;
                totalLength += Math.Sqrt(dx * dx + dy * dy);
            }
        }

        totalLength /= SCALE;  // 转换回实际单位
        double timeMinutes = totalLength / feedRate;

        return timeMinutes;
    }
}

4.6.2 建筑退界线计算

class BuildingSetback
{
    private const double SCALE = 1000000.0;

    /// <summary>
    /// 计算建筑红线和建筑控制线
    /// </summary>
    public static (Paths redLine, Paths controlLine) CalculateBuildingLines(
        Path landBoundary,
        double redLineSetback,
        double controlLineSetback)
    {
        // 红线(用地边界内缩)
        ClipperOffset redLineOffset = new ClipperOffset();
        redLineOffset.AddPath(landBoundary, JoinType.jtMiter, EndType.etClosedPolygon);
        redLineOffset.MiterLimit = 2.0;

        Paths redLine = new Paths();
        redLineOffset.Execute(ref redLine, -redLineSetback * SCALE);

        // 建筑控制线(在红线基础上再内缩)
        Paths controlLine = new Paths();
        if (redLine.Count > 0)
        {
            ClipperOffset controlLineOffset = new ClipperOffset();
            controlLineOffset.AddPaths(redLine, JoinType.jtMiter, EndType.etClosedPolygon);
            controlLineOffset.MiterLimit = 2.0;

            controlLineOffset.Execute(ref controlLine, -controlLineSetback * SCALE);
        }

        return (redLine, controlLine);
    }

    /// <summary>
    /// 检查建筑物是否符合退界要求
    /// </summary>
    public static bool CheckSetbackCompliance(
        Path building,
        Paths allowedArea)
    {
        // 计算建筑与允许区域的交集
        Clipper clipper = new Clipper();
        clipper.AddPaths(allowedArea, PolyType.ptSubject, true);
        clipper.AddPath(building, PolyType.ptClip, true);

        Paths intersection = new Paths();
        clipper.Execute(ClipType.ctIntersection, intersection);

        if (intersection.Count == 0)
            return false;

        // 检查交集面积是否等于建筑面积
        double buildingArea = Math.Abs(Clipper.Area(building));
        double intersectionArea = 0;
        foreach (var path in intersection)
        {
            intersectionArea += Math.Abs(Clipper.Area(path));
        }

        // 允许0.1%的误差
        return Math.Abs(buildingArea - intersectionArea) / buildingArea < 0.001;
    }

    /// <summary>
    /// 生成退界分析报告
    /// </summary>
    public static void GenerateSetbackReport(
        Path landBoundary,
        Paths redLine,
        Paths controlLine,
        List<Path> buildings)
    {
        double landArea = Math.Abs(Clipper.Area(landBoundary)) / (SCALE * SCALE);

        double redLineArea = 0;
        foreach (var path in redLine)
        {
            redLineArea += Math.Abs(Clipper.Area(path));
        }
        redLineArea /= (SCALE * SCALE);

        double controlLineArea = 0;
        foreach (var path in controlLine)
        {
            controlLineArea += Math.Abs(Clipper.Area(path));
        }
        controlLineArea /= (SCALE * SCALE);

        double buildingArea = 0;
        foreach (var building in buildings)
        {
            buildingArea += Math.Abs(Clipper.Area(building));
        }
        buildingArea /= (SCALE * SCALE);

        Console.WriteLine("=== 建筑退界分析报告 ===");
        Console.WriteLine($"用地面积: {landArea:F2} m²");
        Console.WriteLine($"红线内面积: {redLineArea:F2} m² ({redLineArea/landArea*100:F1}%)");
        Console.WriteLine($"建筑控制线内面积: {controlLineArea:F2} m² ({controlLineArea/landArea*100:F1}%)");
        Console.WriteLine($"建筑占地面积: {buildingArea:F2} m²");
        Console.WriteLine($"建筑密度: {(buildingArea/controlLineArea*100):F1}%");

        int compliantCount = 0;
        foreach (var building in buildings)
        {
            if (CheckSetbackCompliance(building, controlLine))
            {
                compliantCount++;
            }
        }
        Console.WriteLine($"合规建筑: {compliantCount}/{buildings.Count}");
    }
}

4.6.3 PCB 铜箔间距检查

class PCBDesignCheck
{
    private const double SCALE = 1000.0;  // 微米级精度

    /// <summary>
    /// 检查铜箔间距是否满足设计规则
    /// </summary>
    public static List<(int trace1, int trace2, double minDistance)> CheckClearance(
        List<Path> traces,
        double minClearance)
    {
        var violations = new List<(int, int, double)>();

        for (int i = 0; i < traces.Count; i++)
        {
            // 为第一条走线创建缓冲区
            ClipperOffset offset = new ClipperOffset();
            offset.AddPath(traces[i], JoinType.jtRound, EndType.etClosedPolygon);

            Paths buffer = new Paths();
            offset.Execute(ref buffer, minClearance * SCALE);

            // 检查是否与其他走线相交
            for (int j = i + 1; j < traces.Count; j++)
            {
                Clipper clipper = new Clipper();
                clipper.AddPaths(buffer, PolyType.ptSubject, true);
                clipper.AddPath(traces[j], PolyType.ptClip, true);

                Paths intersection = new Paths();
                clipper.Execute(ClipType.ctIntersection, intersection);

                if (intersection.Count > 0)
                {
                    // 有间距违规
                    double actualDistance = CalculateMinDistance(traces[i], traces[j]);
                    violations.Add((i, j, actualDistance / SCALE));
                }
            }
        }

        return violations;
    }

    /// <summary>
    /// 计算两个多边形之间的最小距离
    /// </summary>
    private static double CalculateMinDistance(Path poly1, Path poly2)
    {
        double minDist = double.MaxValue;

        foreach (var pt1 in poly1)
        {
            foreach (var pt2 in poly2)
            {
                long dx = pt2.X - pt1.X;
                long dy = pt2.Y - pt1.Y;
                double dist = Math.Sqrt(dx * dx + dy * dy);
                if (dist < minDist)
                {
                    minDist = dist;
                }
            }
        }

        return minDist;
    }

    /// <summary>
    /// 自动修复间距违规(通过收缩走线)
    /// </summary>
    public static List<Path> AutoFixClearance(
        List<Path> traces,
        double minClearance)
    {
        var fixedTraces = new List<Path>();
        double shrinkAmount = minClearance * 0.6;  // 收缩一定比例

        foreach (var trace in traces)
        {
            ClipperOffset offset = new ClipperOffset();
            offset.AddPath(trace, JoinType.jtRound, EndType.etClosedPolygon);

            Paths shrunk = new Paths();
            offset.Execute(ref shrunk, -shrinkAmount * SCALE);

            if (shrunk.Count > 0)
            {
                fixedTraces.Add(shrunk[0]);
            }
            else
            {
                Console.WriteLine("警告:走线太窄,无法修复");
                fixedTraces.Add(trace);
            }
        }

        return fixedTraces;
    }
}

4.7 性能优化和注意事项

4.7.1 性能优化技巧

class OffsetPerformanceOptimization
{
    /// <summary>
    /// 简化输入多边形以提高性能
    /// </summary>
    public static Path SimplifyForOffset(Path input, double tolerance)
    {
        Path simplified = new Path(input);
        Clipper.CleanPolygon(simplified, tolerance);
        return simplified;
    }

    /// <summary>
    /// 批量偏移(复用对象)
    /// </summary>
    public static List<Paths> BatchOffset(
        List<Path> inputs,
        double distance,
        JoinType joinType)
    {
        var results = new List<Paths>();
        ClipperOffset offset = new ClipperOffset();

        foreach (var input in inputs)
        {
            offset.Clear();
            offset.AddPath(input, joinType, EndType.etClosedPolygon);

            Paths solution = new Paths();
            offset.Execute(ref solution, distance);
            results.Add(solution);
        }

        return results;
    }

    /// <summary>
    /// 根据多边形大小动态选择参数
    /// </summary>
    public static (double arcTolerance, double miterLimit) GetOptimalParameters(
        Path polygon,
        double scale)
    {
        double area = Math.Abs(Clipper.Area(polygon)) / (scale * scale);

        // 大多边形使用较大的容差以提高性能
        double arcTolerance = area > 1000 ? 0.5 * scale : 0.25 * scale;

        // 根据面积调整斜接限制
        double miterLimit = area > 1000 ? 2.5 : 2.0;

        return (arcTolerance, miterLimit);
    }
}

4.7.2 常见问题和解决方案

问题1:偏移后产生自相交

/// <summary>
/// 清理偏移结果中的自相交
/// </summary>
public static Paths CleanOffsetResult(Paths offsetResult)
{
    if (offsetResult.Count == 0)
        return offsetResult;

    // 使用并集运算来消除自相交
    Clipper clipper = new Clipper();
    clipper.AddPaths(offsetResult, PolyType.ptSubject, true);

    Paths cleaned = new Paths();
    clipper.Execute(
        ClipType.ctUnion,
        cleaned,
        PolyFillType.pftNonZero,
        PolyFillType.pftNonZero
    );

    return cleaned;
}

问题2:负偏移导致多边形消失

/// <summary>
/// 安全的负偏移
/// </summary>
public static (bool success, Paths result) SafeShrink(
    Path polygon,
    double distance,
    double scale)
{
    ClipperOffset offset = new ClipperOffset();
    offset.AddPath(polygon, JoinType.jtRound, EndType.etClosedPolygon);

    Paths result = new Paths();
    offset.Execute(ref result, -distance * scale);

    if (result.Count == 0)
    {
        return (false, result);
    }

    return (true, result);
}

问题3:尖角处理

/// <summary>
/// 处理尖角的策略选择
/// </summary>
public static JoinType SelectJoinTypeForPolygon(Path polygon)
{
    // 分析多边形的角度分布
    int sharpCorners = 0;
    const double SHARP_ANGLE = Math.PI / 4;  // 45度

    for (int i = 0; i < polygon.Count; i++)
    {
        int prev = (i - 1 + polygon.Count) % polygon.Count;
        int next = (i + 1) % polygon.Count;

        double angle = CalculateAngle(
            polygon[prev],
            polygon[i],
            polygon[next]
        );

        if (angle < SHARP_ANGLE)
        {
            sharpCorners++;
        }
    }

    // 如果有很多尖角,使用圆形连接避免过长的尖角
    if (sharpCorners > polygon.Count * 0.3)
    {
        return JoinType.jtRound;
    }

    return JoinType.jtMiter;
}

private static double CalculateAngle(IntPoint p1, IntPoint p2, IntPoint p3)
{
    long dx1 = p2.X - p1.X;
    long dy1 = p2.Y - p1.Y;
    long dx2 = p3.X - p2.X;
    long dy2 = p3.Y - p2.Y;

    double dot = dx1 * dx2 + dy1 * dy2;
    double len1 = Math.Sqrt(dx1 * dx1 + dy1 * dy1);
    double len2 = Math.Sqrt(dx2 * dx2 + dy2 * dy2);

    if (len1 == 0 || len2 == 0)
        return 0;

    double cosAngle = dot / (len1 * len2);
    return Math.Acos(Math.Max(-1, Math.Min(1, cosAngle)));
}

4.8 本章小结

在本章中,我们深入学习了 Clipper1 的多边形偏移功能:

  1. ClipperOffset 类:专门用于偏移操作的类
  2. JoinType:三种连接类型(Round、Square、Miter)及其应用
  3. EndType:五种端点类型及其选择
  4. 正负偏移:膨胀和收缩操作
  5. 实际应用:CNC 路径生成、建筑退界、PCB 设计检查等
  6. 性能优化:参数选择和问题处理

重点掌握:

  • 根据应用场景选择合适的连接类型和端点类型
  • 理解正负偏移的几何意义
  • 处理偏移操作中的各种边缘情况
  • 在实际项目中灵活应用偏移功能

在下一章中,我们将学习填充规则的详细应用、多边形简化以及其他高级特性。

4.9 练习题

  1. 基础练习:创建一个五角星,分别使用三种连接类型进行偏移,观察差异

  2. 开放路径:创建一条折线,使用不同的端点类型进行偏移,比较结果

  3. CNC 应用:为一个复杂零件生成多层铣削刀具路径

  4. 建筑设计:计算一块不规则地块的建筑红线和控制线

  5. 多级偏移:创建等高线效果,生成10层同心轮廓

  6. 综合应用:实现一个简单的 PCB 设计规则检查工具

posted @ 2025-12-18 09:26  我才是银古  阅读(19)  评论(0)    收藏  举报