第五章 矩形裁剪与闵可夫斯基操作(C#版)

5.1 引言

除了布尔运算和多边形偏移之外,Clipper2还提供了两个专门的几何操作:矩形裁剪(Rectangle Clipping)和闵可夫斯基运算(Minkowski Operations)。矩形裁剪是一种针对轴对齐矩形优化的高效裁剪算法,在地图瓦片生成、视窗裁剪等场景下非常有用。闵可夫斯基运算则在碰撞检测、机器人路径规划等领域有着重要的应用。本章将详细介绍Clipper2 C#版本中这两种操作的原理和使用方法。

5.2 矩形裁剪

5.2.1 什么是矩形裁剪

矩形裁剪是使用轴对齐矩形(Axis-Aligned Rectangle)作为裁剪区域对多边形进行裁剪的操作。由于矩形具有特殊的几何性质(四条边分别与坐标轴平行),可以设计比通用布尔运算更高效的算法。

     原始多边形              裁剪矩形              裁剪结果
    
        ╱╲                  ┌─────┐               ┌─────┐
       ╱  ╲                 │     │               │     │
      ╱    ╲         ∩      │     │        =      │     │
     ╱      ╲               │     │               │ ╱╲  │
    ╱────────╲              └─────┘               ╱─┘  └─╲

5.2.2 矩形裁剪的优势

相比于使用通用布尔运算进行裁剪,专用的矩形裁剪算法具有以下优势:

更高的性能

矩形裁剪算法的时间复杂度更低,通常比通用布尔运算快2-5倍,对于简单的多边形可能更快。

更简单的实现

由于矩形边与坐标轴对齐,边与边的交点计算更加简单直接。

内存效率

算法不需要构建复杂的事件队列和活动边列表,内存占用更低。

5.2.3 使用RectClip函数

Clipper2提供了RectClip函数用于裁剪闭合多边形:

using Clipper2Lib;

// 创建要裁剪的多边形
Paths64 subject = new Paths64();
subject.Add(Clipper.MakePath(new long[] { 0, 0, 200, 0, 200, 200, 0, 200 }));
subject.Add(Clipper.MakePath(new long[] { 50, 50, 150, 50, 150, 150, 50, 150 }));  // 孔洞

// 定义裁剪矩形
Rect64 clipRect = new Rect64(25, 25, 175, 175);

// 执行矩形裁剪
Paths64 result = Clipper.RectClip(clipRect, subject);

// result 包含裁剪后的多边形

5.2.4 使用RectClipLines函数

对于开放的折线(而非闭合多边形),使用RectClipLines函数:

// 创建开放折线
Paths64 lines = new Paths64();
lines.Add(Clipper.MakePath(new long[] { 0, 100, 200, 100 }));  // 水平线
lines.Add(Clipper.MakePath(new long[] { 100, 0, 100, 200 }));  // 垂直线
lines.Add(Clipper.MakePath(new long[] { 0, 0, 200, 200 }));    // 对角线

// 定义裁剪矩形
Rect64 clipRect = new Rect64(50, 50, 150, 150);

// 裁剪折线
Paths64 result = Clipper.RectClipLines(clipRect, lines);
// result 包含被裁剪到矩形内的线段

5.2.5 RectClip64类

对于需要多次使用同一矩形进行裁剪的场景,可以使用RectClip64类以获得更好的性能:

// 创建裁剪对象
Rect64 clipRect = new Rect64(0, 0, 100, 100);
RectClip64 rectClipper = new RectClip64(clipRect);

// 裁剪多个多边形
foreach (Path64 polygon in polygons)
{
    Paths64 result = rectClipper.Execute(new Paths64 { polygon });
    ProcessResult(result);
}

5.2.6 RectClipLines64类

类似地,对于折线裁剪:

Rect64 clipRect = new Rect64(0, 0, 100, 100);
RectClipLines64 lineClipper = new RectClipLines64(clipRect);

foreach (Path64 line in lines)
{
    Paths64 result = lineClipper.Execute(new Paths64 { line });
    ProcessResult(result);
}

5.2.7 浮点数版本

使用浮点数坐标的版本:

// 浮点数矩形裁剪
PathsD subject = new PathsD();
subject.Add(Clipper.MakePath(new double[] { 0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0 }));

RectD clipRect = new RectD(2.5, 2.5, 7.5, 7.5);

PathsD result = Clipper.RectClip(clipRect, subject);

5.2.8 矩形裁剪的边界情况

边界上的顶点

当多边形顶点恰好在裁剪矩形的边界上时,算法会正确处理:

// 顶点在边界上的多边形
Paths64 subject = new Paths64();
subject.Add(Clipper.MakePath(new long[] { 50, 0, 100, 50, 50, 100, 0, 50 }));  // 菱形

Rect64 clipRect = new Rect64(0, 0, 100, 100);  // 边界与菱形顶点重合
Paths64 result = Clipper.RectClip(clipRect, subject);

完全在矩形内的多边形

如果多边形完全在裁剪矩形内部,返回原始多边形:

Paths64 subject = new Paths64();
subject.Add(Clipper.MakePath(new long[] { 25, 25, 75, 25, 75, 75, 25, 75 }));

Rect64 clipRect = new Rect64(0, 0, 100, 100);
Paths64 result = Clipper.RectClip(clipRect, subject);
// result 与 subject 相同

完全在矩形外的多边形

如果多边形完全在裁剪矩形外部,返回空结果:

Paths64 subject = new Paths64();
subject.Add(Clipper.MakePath(new long[] { 200, 200, 300, 200, 300, 300, 200, 300 }));

Rect64 clipRect = new Rect64(0, 0, 100, 100);
Paths64 result = Clipper.RectClip(clipRect, subject);
// result 为空

5.2.9 矩形裁剪的应用场景

地图瓦片生成

// 生成256x256的地图瓦片
static void GenerateTile(int tileX, int tileY, int zoom, Paths64 mapData)
{
    // 计算瓦片的地理范围
    int tileSize = 256;

    Rect64 tileBounds = new Rect64(
        tileX * tileSize,
        tileY * tileSize,
        (tileX + 1) * tileSize,
        (tileY + 1) * tileSize
    );

    // 裁剪地图数据到瓦片范围
    Paths64 tileData = Clipper.RectClip(tileBounds, mapData);

    // 渲染瓦片
    RenderTile(tileData);
}

视窗裁剪

// 裁剪到可见视窗区域
static Paths64 ClipToViewport(Paths64 geometry,
                              int viewportWidth, int viewportHeight,
                              int scrollX, int scrollY)
{
    Rect64 viewport = new Rect64(
        scrollX,
        scrollY,
        scrollX + viewportWidth,
        scrollY + viewportHeight
    );

    return Clipper.RectClip(viewport, geometry);
}

空间分区

// 将几何体分配到四叉树节点(QuadTreeNode 为自定义四叉树结构)
static void QuadTreeInsert(QuadTreeNode node, Paths64 geometry)
{
    Rect64 nodeBounds = node.GetBounds();
    Paths64 clipped = Clipper.RectClip(nodeBounds, geometry);

    if (clipped.Count > 0)
    {
        if (node.IsLeaf() || Clipper.Area(clipped) < threshold)
        {
            node.AddGeometry(clipped);
        }
        else
        {
            // 分配到子节点
            foreach (QuadTreeNode child in node.Children())
            {
                QuadTreeInsert(child, clipped);
            }
        }
    }
}

5.3 闵可夫斯基运算

5.3.1 什么是闵可夫斯基运算

闵可夫斯基运算是以德国数学家赫尔曼·闵可夫斯基(Hermann Minkowski)命名的几何操作。主要包括两种运算:

闵可夫斯基和(Minkowski Sum)

两个点集A和B的闵可夫斯基和定义为: A ⊕ B = {a + b | a ∈ A, b ∈ B}

直观理解:将形状B的中心放在形状A的每个边界点上,所有这些B形状的并集就是闵可夫斯基和。

     多边形A              多边形B            闵可夫斯基和
    
    ┌───────┐              ○               ╭───────────╮
    │       │              ↓               │   ╭─────╮ │
    │       │      ⊕                  =    │   │     │ │
    │       │                              │   ╰─────╯ │
    └───────┘                              ╰───────────╯
                                          (圆角矩形)

闵可夫斯基差(Minkowski Difference)

A和B的闵可夫斯基差定义为: A ⊖ B = A ⊕ (-B) = {a - b | a ∈ A, b ∈ B}

其中-B是B关于原点的反射。

5.3.2 闵可夫斯基和的几何意义

闵可夫斯基和有几个重要的几何解释:

形态膨胀

当B是一个以原点为中心的圆时,A ⊕ B 等于A的圆形偏移。

可达区域

如果A表示障碍物,B表示移动物体,那么A ⊕ (-B)表示移动物体中心不能到达的区域。

碰撞检测

两个多边形A和B相交,当且仅当(A ⊖ B)包含原点。

5.3.3 使用MinkowskiSum函数

Clipper2提供了闵可夫斯基和函数:

using Clipper2Lib;

// 创建第一个多边形(矩形)
Path64 pattern = Clipper.MakePath(new long[] { -50, -50, 50, -50, 50, 50, -50, 50 });

// 创建第二个多边形(三角形,以原点为中心)
Path64 path = Clipper.MakePath(new long[] { 0, -30, 26, 15, -26, 15 });

// 计算闵可夫斯基和
Paths64 result = Clipper.MinkowskiSum(pattern, path, false);
// 参数:pattern(图案), path(路径), isClosed(路径是否闭合)

5.3.4 使用MinkowskiDiff函数

// 计算闵可夫斯基差
Path64 polygon1 = Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 });
Path64 polygon2 = Clipper.MakePath(new long[] { -20, -20, 20, -20, 20, 20, -20, 20 });

Paths64 diff = Clipper.MinkowskiDiff(polygon1, polygon2, false);

5.3.5 闵可夫斯基和的参数

public static Paths64 MinkowskiSum(
    Path64 pattern,   // 图案多边形
    Path64 path,      // 路径
    bool isClosed     // 路径是否闭合
);

pattern(图案):被复制到path每个顶点的形状

path(路径):定义图案放置位置的路径

isClosed

  • true:路径是闭合的多边形
  • false:路径是开放的折线

5.3.6 开放路径与闭合路径

using Clipper2Lib;

// 圆盘图案(正多边形近似,中心在原点)
static Path64 MakeDisk(long radius, int edgeCount = 32)
{
    Path64 disk = new Path64(edgeCount);
    for (int i = 0; i < edgeCount; i++)
    {
        double angle = 2 * Math.PI * i / edgeCount;
        disk.Add(new Point64(
            (long)Math.Round(radius * Math.Cos(angle)),
            (long)Math.Round(radius * Math.Sin(angle))));
    }
    return disk;
}

Path64 circle = MakeDisk(20);
Path64 square = Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 });  // 正方形路径

// 闭合路径:产生圆角矩形
Paths64 closedResult = Clipper.MinkowskiSum(circle, square, true);

// 开放路径:产生胶囊形
Path64 line = Clipper.MakePath(new long[] { 0, 0, 100, 0 });  // 线段路径
Paths64 openResult = Clipper.MinkowskiSum(circle, line, false);

5.3.7 实际应用示例

碰撞检测

使用闵可夫斯基差进行碰撞检测:

using Clipper2Lib;

// 两个多边形
Path64 polygonA = Clipper.MakePath(new long[] { 0, 0, 50, 0, 50, 50, 0, 50 });
Path64 polygonB = Clipper.MakePath(new long[] { 30, 30, 80, 30, 80, 80, 30, 80 });

// 计算闵可夫斯基差
Paths64 minkDiff = Clipper.MinkowskiDiff(polygonA, polygonB, true);

// 检查原点是否在闵可夫斯基差内
Point64 origin = new Point64(0, 0);
bool colliding = false;
foreach (Path64 path in minkDiff)
{
    if (Clipper.PointInPolygon(origin, path) != PointInPolygonResult.IsOutside)
    {
        colliding = true;
        break;
    }
}

if (colliding)
{
    Console.WriteLine("多边形相交!");
}

机器人路径规划

使用闵可夫斯基和计算配置空间障碍物:

// 机器人形状(以中心为原点)
Path64 robotShape = Clipper.MakePath(new long[] { -10, -10, 10, -10, 10, 10, -10, 10 });

// 障碍物列表
List<Path64> obstacles = new List<Path64>
{
    Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200, 100, 200 }),
    Clipper.MakePath(new long[] { 300, 50, 400, 50, 400, 150, 300, 150 })
};

// 计算配置空间障碍物
Paths64 configSpaceObstacles = new Paths64();
foreach (Path64 obstacle in obstacles)
{
    // 使用机器人形状的反射
    Path64 reflectedRobot = new Path64(robotShape.Count);
    foreach (Point64 pt in robotShape)
        reflectedRobot.Add(new Point64(-pt.X, -pt.Y));

    // 闵可夫斯基和
    Paths64 expanded = Clipper.MinkowskiSum(reflectedRobot, obstacle, true);

    // 合并到配置空间障碍物
    configSpaceObstacles.AddRange(expanded);
}

// 合并重叠的障碍物
Paths64 mergedObstacles = Clipper.Union(configSpaceObstacles, FillRule.NonZero);

// 现在可以在点空间中进行路径规划,只要路径不穿过这些障碍物

形态学膨胀

使用闵可夫斯基和模拟形态学膨胀:

// 原始形状
Path64 shape = Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 });

// 结构元素(圆盘,中心在原点,MakeDisk 定义见 5.3.6)
Path64 structuringElement = MakeDisk(20);

// 形态学膨胀
Paths64 dilated = Clipper.MinkowskiSum(structuringElement, shape, true);

计算可达边界

// 移动物体的形状
Path64 movingObject = Clipper.MakePath(new long[] { -5, -5, 5, -5, 5, 5, -5, 5 });

// 轨迹路径
Path64 trajectory = Clipper.MakePath(new long[] { 0, 0, 100, 50, 200, 0, 300, 50 });

// 计算移动物体沿轨迹运动时扫过的区域
Paths64 sweptArea = Clipper.MinkowskiSum(movingObject, trajectory, false);

5.3.8 闵可夫斯基运算的性能考虑

复杂度分析

闵可夫斯基和的时间复杂度大约为O(m * n),其中m和n分别是两个多边形的顶点数。对于复杂的多边形,这可能会很慢。

优化策略

// 1. 简化输入多边形
Path64 simplifiedPattern = Clipper.SimplifyPath(pattern, tolerance);
Path64 simplifiedPath = Clipper.SimplifyPath(path, tolerance);
Paths64 result = Clipper.MinkowskiSum(simplifiedPattern, simplifiedPath, true);

// 2. 对于凸多边形使用专门的算法(IsConvex/ConvexMinkowskiSum 需自行实现)
if (IsConvex(pattern) && IsConvex(path))
{
    Paths64 convexResult = ConvexMinkowskiSum(pattern, path);
}

// 3. 分解为凸多边形(ConvexPartition 需自行实现)
Paths64 convexPattern = ConvexPartition(pattern);
Paths64 convexPath = ConvexPartition(path);
Paths64 decomposed = new Paths64();
foreach (Path64 cp in convexPattern)
{
    foreach (Path64 cpp in convexPath)
    {
        Paths64 partial = Clipper.MinkowskiSum(cp, cpp, true);
        decomposed.AddRange(partial);
    }
}
decomposed = Clipper.Union(decomposed, FillRule.NonZero);

5.3.9 闵可夫斯基和的数学性质

交换律

A ⊕ B = B ⊕ A

结合律

(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)

分配律(对于并集)

A ⊕ (B ∪ C) = (A ⊕ B) ∪ (A ⊕ C)

与原点的关系

如果O是只包含原点的点集,则 A ⊕ O = A

这些性质可以用于优化计算:

// 利用分配律分解计算
Path64 A = Clipper.MakePath(new long[] { /* A 的顶点坐标 */ });
Paths64 B = new Paths64();  // B 由多个分离的多边形组成

// 闵可夫斯基和对并集满足分配律,可把 B 分解为单个多边形分别求和后再合并
Paths64 result = new Paths64();
foreach (Path64 bi in B)
{
    Paths64 partial = Clipper.MinkowskiSum(A, bi, true);
    result.AddRange(partial);
}
result = Clipper.Union(result, FillRule.NonZero);

5.3.10 形态学操作(任意结构元素)

5.3.7 中的”形态学膨胀”只是形态学操作的一种。完整的形态学(Morphology)操作还包括腐蚀(Erode)、开运算(Open)、闭运算(Close)和形态学梯度(Gradient),它们广泛应用于矢量数据清理:剔除细小图斑、弥合窄缝、填充凹口、提取边界带等。

当结构元素是以原点为中心的圆盘时,这些操作都可以用第四章介绍的偏移函数(Clipper.InflatePathsJoinType.Round)等价实现。但当结构元素是任意形状(矩形、线段、十字等)时,必须借助闵可夫斯基运算。下面给出基于闵可夫斯基运算的通用实现。

辅助函数

首先需要三个辅助函数:圆盘结构元素(MakeDisk)、包围盒补集(Complement)以及结构元素关于原点的反射(ReflectPath)。

using Clipper2Lib;

// 圆盘结构元素(正多边形近似,中心在原点)
public static Path64 MakeDisk(long radius, int edgeCount = 32)
{
    Path64 disk = new Path64(edgeCount);
    for (int i = 0; i < edgeCount; i++)
    {
        double angle = 2 * Math.PI * i / edgeCount;
        disk.Add(new Point64(
            (long)Math.Round(radius * Math.Cos(angle)),
            (long)Math.Round(radius * Math.Sin(angle))));
    }
    return disk;
}

// 补集:包围盒减去 paths
public static Paths64 Complement(Paths64 paths, Rect64 bounds)
{
    Path64 box = Clipper.MakePath(new long[] {
        bounds.left, bounds.top,
        bounds.right, bounds.top,
        bounds.right, bounds.bottom,
        bounds.left, bounds.bottom });
    return Clipper.Difference(new Paths64 { box }, paths, FillRule.NonZero);
}

// 结构元素关于原点的反射:B^r = {-b : b in B}
public static Path64 ReflectPath(Path64 pattern)
{
    Path64 reflected = new Path64(pattern.Count);
    foreach (Point64 pt in pattern)
        reflected.Add(new Point64(-pt.X, -pt.Y));
    return reflected;
}

膨胀与腐蚀

膨胀定义为闵可夫斯基和;腐蚀则通过”补集 → 膨胀 → 再取补集”实现。

// 形态学膨胀:A ⊕ B = MinkowskiSum(A, B)
public static Paths64 MorphDilate(Path64 shape, Path64 se)
    => Clipper.MinkowskiSum(shape, se, true);

// 形态学腐蚀:A ⊖ B = (A^c ⊕ B^r)^c
public static Paths64 MorphErode(Path64 shape, Path64 se)
{
    Rect64 b = Clipper.GetBounds(shape);
    Rect64 sb = Clipper.GetBounds(se);
    long w = sb.right - sb.left, h = sb.bottom - sb.top;
    Rect64 bounds = new Rect64(b.left - w, b.top - h, b.right + w, b.bottom + h);

    Paths64 comp = Complement(new Paths64 { shape }, bounds);
    Path64 seReflected = ReflectPath(se);
    Paths64 grown = new Paths64();
    foreach (Path64 p in comp)
        grown.AddRange(Clipper.MinkowskiSum(p, seReflected, true));
    return Complement(grown, bounds);
}

开运算与闭运算

开运算是先腐蚀后膨胀;闭运算是先膨胀后腐蚀。

// 开运算:(A ⊖ B) ⊕ B
public static Paths64 MorphOpen(Path64 shape, Path64 se)
{
    Paths64 eroded = MorphErode(shape, se);
    Paths64 opened = new Paths64();
    foreach (Path64 p in eroded)
        opened.AddRange(Clipper.MinkowskiSum(p, se, true));
    return Clipper.Union(opened, FillRule.NonZero);
}

// 闭运算:(A ⊕ B) ⊖ B
public static Paths64 MorphClose(Path64 shape, Path64 se)
{
    Paths64 dilated = MorphDilate(shape, se);
    Paths64 closed = new Paths64();
    foreach (Path64 p in dilated)
        closed.AddRange(MorphErode(p, se));
    return Clipper.Union(closed, FillRule.NonZero);
}

形态学梯度

// 形态学梯度:(A ⊕ B) \ (A ⊖ B)
public static Paths64 MorphGradient(Path64 shape, Path64 se)
    => Clipper.Difference(MorphDilate(shape, se), MorphErode(shape, se), FillRule.NonZero);

注意:闵可夫斯基差不等于腐蚀

闵可夫斯基差 A ⊖ B 定义为 A ⊕ (−B),即把结构元素关于原点反射后再做闵可夫斯基和。它主要用于碰撞检测(见 5.3.7),表示”物体 B 的中心不能进入的区域”。而形态学腐蚀 A ⊖ B 是完全不同的操作——它通过”补集的膨胀再取补集”实现,用于收缩形状、剔除细小结构。二者在数学上并不等价,不可混用。

圆盘结构元素等价于偏移

当结构元素为圆盘时,上述形态学操作与第四章的 Round 偏移完全等价:

Paths64 dilated = Clipper.InflatePaths(paths,  r, JoinType.Round, EndType.Polygon);   // 膨胀
Paths64 eroded  = Clipper.InflatePaths(paths, -r, JoinType.Round, EndType.Polygon);   // 腐蚀
// 开 = 先 -r 后 +r;闭 = 先 +r 后 -r;梯度 = 膨胀 - 腐蚀

圆盘结构元素使用偏移实现更简洁高效;只有在需要非圆盘(如线段、矩形)结构元素时才必须使用本节的闵可夫斯基实现。

5.4 综合应用案例

5.4.1 地图瓦片系统

class TileSystem
{
    private readonly int tileSize;
    private readonly int maxZoom;

    public TileSystem(int tileSize = 256, int maxZoom = 20)
    {
        this.tileSize = tileSize;
        this.maxZoom = maxZoom;
    }

    public Paths64 ClipToTile(Paths64 geometry, int x, int y, int zoom)
    {
        // 计算瓦片边界
        long scale = 1L << zoom;
        long tileX = x * tileSize;
        long tileY = y * tileSize;

        Rect64 tileBounds = new Rect64(
            tileX, tileY,
            tileX + tileSize, tileY + tileSize
        );

        // 使用矩形裁剪
        return Clipper.RectClip(tileBounds, geometry);
    }

    public Dictionary<(int x, int y), Paths64> GenerateTiles(Paths64 geometry, int zoom)
    {
        var tiles = new Dictionary<(int x, int y), Paths64>();

        // 获取几何体的边界框
        Rect64 bounds = Clipper.GetBounds(geometry);

        // 计算涉及的瓦片范围
        int minTileX = (int)(bounds.left / tileSize);
        int minTileY = (int)(bounds.top / tileSize);
        int maxTileX = (int)(bounds.right / tileSize);
        int maxTileY = (int)(bounds.bottom / tileSize);

        // 为每个瓦片裁剪几何体
        for (int ty = minTileY; ty <= maxTileY; ty++)
        {
            for (int tx = minTileX; tx <= maxTileX; tx++)
            {
                Paths64 tileGeometry = ClipToTile(geometry, tx, ty, zoom);
                if (tileGeometry.Count > 0)
                {
                    tiles[(tx, ty)] = tileGeometry;
                }
            }
        }

        return tiles;
    }
}

5.4.2 碰撞检测系统

class CollisionDetector
{
    // 检测两个多边形是否碰撞
    public bool CheckCollision(Path64 polygonA, Path64 polygonB)
    {
        // 计算闵可夫斯基差
        Paths64 minkDiff = Clipper.MinkowskiDiff(polygonA, polygonB, true);

        // 检查原点是否在差集内
        Point64 origin = new Point64(0, 0);
        foreach (Path64 path in minkDiff)
        {
            if (Clipper.PointInPolygon(origin, path) != PointInPolygonResult.IsOutside)
                return true;
        }
        return false;
    }

    // 计算穿透深度和方向
    public (double distance, PointD direction) GetPenetration(Path64 polygonA, Path64 polygonB)
    {
        Paths64 minkDiff = Clipper.MinkowskiDiff(polygonA, polygonB, true);

        Point64 origin = new Point64(0, 0);
        double minDist = double.MaxValue;
        PointD minDirection = new PointD(0, 0);

        foreach (Path64 path in minkDiff)
        {
            if (Clipper.PointInPolygon(origin, path) == PointInPolygonResult.IsOutside)
                continue;

            // 找到原点到边界的最短距离
            for (int i = 0; i < path.Count; i++)
            {
                Point64 p1 = path[i];
                Point64 p2 = path[(i + 1) % path.Count];

                // 计算点到线段的距离
                double dist = DistanceToSegment(origin, p1, p2);
                if (dist < minDist)
                {
                    minDist = dist;
                    // 计算法线方向
                    double dx = p2.X - p1.X;
                    double dy = p2.Y - p1.Y;
                    double len = Math.Sqrt(dx * dx + dy * dy);
                    minDirection = new PointD(-dy / len, dx / len);
                }
            }
        }

        return (minDist, minDirection);
    }

    private double DistanceToSegment(Point64 pt, Point64 a, Point64 b)
    {
        double dx = b.X - a.X;
        double dy = b.Y - a.Y;
        double t = Math.Max(0.0, Math.Min(1.0,
            ((pt.X - a.X) * dx + (pt.Y - a.Y) * dy) / (dx * dx + dy * dy)));
        double projX = a.X + t * dx;
        double projY = a.Y + t * dy;
        return Math.Sqrt((pt.X - projX) * (pt.X - projX) +
                         (pt.Y - projY) * (pt.Y - projY));
    }
}

5.4.3 扫掠体积计算

class SweptVolume
{
    // 计算物体沿路径移动时扫过的区域
    public Paths64 ComputeSweptArea(Path64 objectShape, Path64 motionPath, bool closedPath = false)
    {
        // 使用闵可夫斯基和
        return Clipper.MinkowskiSum(objectShape, motionPath, closedPath);
    }

    // 计算旋转物体的扫掠区域(近似)
    public Paths64 ComputeRotationalSweep(Path64 objectShape, Point64 pivot,
                                          double startAngle, double endAngle, int numSteps = 36)
    {
        Paths64 result = new Paths64();

        double angleStep = (endAngle - startAngle) / numSteps;

        for (int i = 0; i <= numSteps; i++)
        {
            double angle = startAngle + i * angleStep;
            Path64 rotated = RotatePath(objectShape, pivot, angle);
            result.Add(rotated);
        }

        // 合并所有位置
        return Clipper.Union(result, FillRule.NonZero);
    }

    private Path64 RotatePath(Path64 path, Point64 pivot, double angle)
    {
        Path64 result = new Path64();
        double cosA = Math.Cos(angle);
        double sinA = Math.Sin(angle);

        foreach (Point64 pt in path)
        {
            double dx = pt.X - pivot.X;
            double dy = pt.Y - pivot.Y;
            result.Add(new Point64(
                (long)(pivot.X + dx * cosA - dy * sinA),
                (long)(pivot.Y + dx * sinA + dy * cosA)
            ));
        }

        return result;
    }
}

5.4.4 安全区域计算

class SafetyZoneCalculator
{
    // 计算机器人的安全导航区域
    public Paths64 ComputeSafeZone(Paths64 obstacles, Path64 robotShape, double additionalMargin = 0)
    {
        Paths64 expandedObstacles = new Paths64();

        // 计算机器人形状的反射
        Path64 reflectedRobot = new Path64(robotShape.Count);
        foreach (Point64 pt in robotShape)
            reflectedRobot.Add(new Point64(-pt.X, -pt.Y));

        // 如果需要额外边距,先膨胀反射形状
        Path64 marginedRobot = reflectedRobot;
        if (additionalMargin > 0)
        {
            Paths64 inflated = Clipper.InflatePaths(new Paths64 { reflectedRobot },
                                                    additionalMargin,
                                                    JoinType.Round,
                                                    EndType.Polygon);
            if (inflated.Count > 0)
            {
                marginedRobot = inflated[0];
            }
        }

        // 扩展每个障碍物
        foreach (Path64 obstacle in obstacles)
        {
            Paths64 expanded = Clipper.MinkowskiSum(marginedRobot, obstacle, true);
            expandedObstacles.AddRange(expanded);
        }

        // 合并重叠的障碍物
        return Clipper.Union(expandedObstacles, FillRule.NonZero);
    }

    // 计算工作区域内的可用空间
    public Paths64 ComputeFreeSpace(Path64 workspace, Paths64 obstacles, Path64 robotShape)
    {
        Paths64 expandedObstacles = ComputeSafeZone(obstacles, robotShape);
        return Clipper.Difference(new Paths64 { workspace }, expandedObstacles, FillRule.NonZero);
    }
}

5.5 性能对比

5.5.1 矩形裁剪 vs 布尔交集

using System;
using System.Diagnostics;
using Clipper2Lib;

static void BenchmarkRectClip()
{
    // 创建测试数据
    Paths64 subject = new Paths64();
    for (int i = 0; i < 100; i++)
        subject.Add(MakeRandomPolygon());  // MakeRandomPolygon 为自定义随机多边形生成函数

    Rect64 clipRect = new Rect64(0, 0, 500, 500);
    Paths64 clipPolygon = new Paths64();
    clipPolygon.Add(Clipper.MakePath(new long[] { 0, 0, 500, 0, 500, 500, 0, 500 }));

    // 测试矩形裁剪
    var sw1 = Stopwatch.StartNew();
    for (int i = 0; i < 1000; i++)
    {
        Paths64 result = Clipper.RectClip(clipRect, subject);
    }
    sw1.Stop();

    // 测试布尔交集
    var sw2 = Stopwatch.StartNew();
    for (int i = 0; i < 1000; i++)
    {
        Paths64 result = Clipper.Intersect(subject, clipPolygon, FillRule.NonZero);
    }
    sw2.Stop();

    Console.WriteLine($"矩形裁剪: {sw1.ElapsedMilliseconds} ms");
    Console.WriteLine($"布尔交集: {sw2.ElapsedMilliseconds} ms");
}

典型结果显示,矩形裁剪比布尔交集快2-5倍。

5.6 本章小结

本章我们学习了Clipper2的两个专门几何操作:

  1. 矩形裁剪
    • RectClip和RectClipLines函数
    • RectClip64和RectClipLines64类
    • 性能优势和使用场景
    • 地图瓦片生成、视窗裁剪等应用
  2. 闵可夫斯基运算
    • 闵可夫斯基和与闵可夫斯基差
    • MinkowskiSum和MinkowskiDiff函数
    • 碰撞检测、机器人路径规划等应用
    • 性能考虑和优化策略
  3. 形态学操作
    • 膨胀(闵可夫斯基和)与腐蚀(补集法)
    • 开运算、闭运算、形态学梯度
    • 圆盘结构元素等价于偏移,任意结构元素用闵可夫斯基运算
    • 注意闵可夫斯基差不等于腐蚀
  4. 综合应用
    • 地图瓦片系统
    • 碰撞检测系统
    • 扫掠体积计算
    • 安全区域计算

在下一章中,我们将学习Clipper2的高级应用技巧和性能优化方法。


← 上一章 目录 下一章 →