第4章_多边形偏移操作

第四章 多边形偏移操作(C#版)

4.1 引言

多边形偏移(Polygon Offsetting)也称为多边形膨胀(Inflate)或收缩(Deflate),是指将多边形的边界向内或向外移动指定的距离。这是一个在实际应用中极为常见的操作,从CNC加工的刀具补偿到地图制作的缓冲区分析,从3D打印的轮廓生成到游戏开发的碰撞边界创建,多边形偏移操作无处不在。

Clipper2提供了强大而灵活的偏移功能,支持多种连接类型和端点类型,能够处理复杂的多边形结构。本章将详细介绍Clipper2 C#版本中多边形偏移的使用方法和高级技巧。

4.2 偏移的基本概念

4.2.1 什么是多边形偏移

多边形偏移是将多边形的每条边沿其法线方向移动指定距离的操作。当偏移距离为正值时,多边形向外扩张;当偏移距离为负值时,多边形向内收缩。

原始多边形          正向偏移(膨胀)      负向偏移(收缩)
┌─────────┐        ┌───────────────┐    ┌───────┐
│         │        │               │    │       │
│         │   →    │   ┌─────┐     │    │       │
│         │        │   │     │     │    └───────┘
└─────────┘        └───┴─────┴─────┘

4.2.2 偏移中的几何问题

在多边形偏移过程中,会遇到几个重要的几何问题:

拐角处理

当两条边在顶点相遇时,它们的偏移边会在拐角处产生间隙(外角)或重叠(内角)。处理这些拐角的方式称为"连接类型"(Join Type)。

       原始拐角              圆角连接            斜角连接            方角连接
                           (Round)            (Miter)            (Square)
      ╱                    ╱  ‾‾‾             ╱      ╲            ╱──────╲
     ╱                    ╱      ╲           ╱        ╲          ╱        ╲
────╱              ──────╱        ╲    ─────╱          ╲   ─────╱          ╲

端点处理

对于开放路径(折线),需要决定如何处理两端的端点。处理端点的方式称为"端点类型"(End Type)。

原始折线        方角端点           圆角端点         平头端点
  ─────        ┌─────┐           (─────)         ═════
               └─────┘

自交处理

当偏移距离过大时,偏移后的多边形可能会自交。Clipper2能够自动检测并正确处理这些自交情况。

4.2.3 偏移方向与符号约定

在Clipper2中:

  • 正值偏移:使多边形区域增大(向外扩张)
  • 负值偏移:使多边形区域减小(向内收缩)

这个约定与多边形的顶点方向无关——无论多边形是顺时针还是逆时针,正值始终表示扩张。

4.3 使用简化API进行偏移

Clipper2提供了简化的偏移函数,可以用一行代码完成偏移操作。

4.3.1 InflatePaths函数

using System;
using Clipper2Lib;

class InflatePathsExample
{
    static void Main()
    {
        // 创建一个正方形
        Paths64 paths = new Paths64();
        paths.Add(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 200, 100, 200 }));
        
        // 向外偏移10个单位,使用圆角连接
        Paths64 inflated = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon);
        Console.WriteLine($"膨胀后顶点数: {inflated[0].Count}");
        
        // 向内偏移10个单位
        Paths64 deflated = Clipper.InflatePaths(paths, -10, JoinType.Round, EndType.Polygon);
        Console.WriteLine($"收缩后顶点数: {deflated[0].Count}");
    }
}

4.3.2 方法签名

public static Paths64 InflatePaths(
    Paths64 paths,              // 输入路径
    double delta,               // 偏移距离(正值膨胀,负值收缩)
    JoinType joinType,          // 连接类型
    EndType endType,            // 端点类型
    double miterLimit = 2.0,    // 斜角限制(仅对Miter连接有效)
    double arcTolerance = 0.0   // 弧度容差(仅对Round连接有效)
)

4.3.3 使用浮点数坐标

using Clipper2Lib;

class FloatOffsetExample
{
    static void Main()
    {
        // 使用浮点数坐标
        PathsD pathsD = new PathsD();
        pathsD.Add(Clipper.MakePath(new double[] { 1.5, 1.5, 2.5, 1.5, 2.5, 2.5, 1.5, 2.5 }));

        // 偏移0.1个单位
        PathsD result = Clipper.InflatePaths(pathsD, 0.1, JoinType.Round, EndType.Polygon, 2.0, 0.01);
        Console.WriteLine($"结果顶点数: {result[0].Count}");
    }
}

4.4 连接类型(JoinType)

连接类型决定了在多边形的拐角处如何连接偏移后的边。

4.4.1 Round(圆角连接)

圆角连接在拐角处创建圆弧形的过渡。这是最常用的连接类型,产生的结果最为平滑。

Paths64 result = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon);

特点

  • 拐角处为圆弧形
  • 视觉效果最平滑
  • 顶点数量较多
  • 适合CNC加工、3D打印等需要圆滑轮廓的应用

弧度容差(Arc Tolerance)

arcTolerance参数控制圆弧的精度。值越小,圆弧越精确,但顶点数量越多:

// 高精度圆弧(更多顶点)
Paths64 highQuality = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon, 2.0, 0.1);

// 低精度圆弧(较少顶点)
Paths64 lowQuality = Clipper.InflatePaths(paths, 10, JoinType.Round, EndType.Polygon, 2.0, 1.0);

如果arcTolerance为0或负值,Clipper2会根据偏移距离自动选择一个合适的值。

4.4.2 Miter(斜角连接)

斜角连接延长相邻的偏移边直到它们相交。这会在锐角处产生尖锐的突出。

Paths64 result = Clipper.InflatePaths(paths, 10, JoinType.Miter, EndType.Polygon);

特点

  • 锐角处会产生尖锐的尖角
  • 顶点数量与原始多边形相同
  • 在非常锐的角度时,尖角可能延伸得非常远

斜角限制(Miter Limit)

miter_limit参数用于限制斜角的最大长度。当斜角延伸超过此限制时,会被截断为平头:

// miter_limit = 2.0(默认值)
Paths64 result1 = InflatePaths(paths, 10, JoinType::Miter, EndType::Polygon, 2.0);

// miter_limit = 4.0(允许更长的斜角)
Paths64 result2 = InflatePaths(paths, 10, JoinType::Miter, EndType::Polygon, 4.0);

// miter_limit = 1.0(斜角很快被截断)
Paths64 result3 = InflatePaths(paths, 10, JoinType::Miter, EndType::Polygon, 1.0);

斜角限制的计算方式:当斜角长度超过 delta * miter_limit 时,斜角会被截断。

4.4.3 Square(方角连接)

方角连接在拐角处创建矩形的突出,每个拐角固定向外延伸一个偏移距离的长度。

Paths64 result = InflatePaths(paths, 10, JoinType::Square, EndType::Polygon);

特点

  • 每个拐角处添加固定大小的矩形突出
  • 产生的形状较为规则
  • 顶点数量是原始多边形的两倍
  • 适合需要均匀边界厚度的应用

4.4.4 Bevel(斜切连接)

斜切连接(也称为倒角)在拐角处创建一个平面截断,而不是尖角或圆弧。

Paths64 result = InflatePaths(paths, 10, JoinType::Bevel, EndType::Polygon);

特点

  • 拐角处为斜切平面
  • 类似于miter_limit=1.0时的Miter连接
  • 顶点数量是原始多边形的两倍
  • 视觉效果介于Miter和Square之间

4.4.5 连接类型对比

// 创建一个三角形来展示不同连接类型的效果
Paths64 triangle;
triangle.push_back(MakePath({0, 100, 50, 0, 100, 100}));

// 分别使用不同的连接类型偏移
Paths64 roundResult = InflatePaths(triangle, 10, JoinType::Round, EndType::Polygon);
Paths64 miterResult = InflatePaths(triangle, 10, JoinType::Miter, EndType::Polygon);
Paths64 squareResult = InflatePaths(triangle, 10, JoinType::Square, EndType::Polygon);
Paths64 bevelResult = InflatePaths(triangle, 10, JoinType::Bevel, EndType::Polygon);
连接类型 锐角表现 顶点数量 适用场景
Round 圆弧过渡 平滑外观、CNC加工
Miter 尖锐延伸 等于原始 保持尖角外观
Square 矩形突出 2倍原始 规则边界
Bevel 斜切平面 2倍原始 倒角效果

4.5 端点类型(EndType)

端点类型决定了开放路径(折线)的两端如何处理。对于闭合路径(多边形),端点类型应设为Polygon或Joined。

4.5.1 Polygon端点

用于闭合的多边形,路径会自动闭合。

Paths64 paths;
paths.push_back(MakePath({100, 100, 200, 100, 200, 200, 100, 200}));
Paths64 result = InflatePaths(paths, 10, JoinType::Round, EndType::Polygon);

4.5.2 Joined端点

类似于Polygon,但专门用于开放路径的处理,路径两端会连接起来形成闭合的偏移区域。

Paths64 openPath;
openPath.push_back(MakePath({100, 100, 200, 100, 200, 200}));  // 一条折线
Paths64 result = InflatePaths(openPath, 10, JoinType::Round, EndType::Joined);
// 结果是一个闭合的管状区域

4.5.3 Butt端点(平头端点)

在路径端点处创建垂直于路径方向的平头截断。

Paths64 openPath;
openPath.push_back(MakePath({100, 100, 200, 100, 200, 200}));
Paths64 result = InflatePaths(openPath, 10, JoinType::Round, EndType::Butt);
// 路径两端是平头的

特点

  • 端点处与路径方向垂直
  • 产生平整的端面
  • 偏移后的长度与原始路径相同

4.5.4 Square端点(方角端点)

类似于Butt端点,但在端点处向外延伸一个偏移距离的长度。

Paths64 openPath;
openPath.push_back(MakePath({100, 100, 200, 100, 200, 200}));
Paths64 result = InflatePaths(openPath, 10, JoinType::Round, EndType::Square);
// 路径两端是方形的,向外延伸

特点

  • 端点处创建方形突出
  • 偏移后的长度增加 2 * delta

4.5.5 Round端点(圆角端点)

在路径端点处创建半圆形的端帽。

Paths64 openPath;
openPath.push_back(MakePath({100, 100, 200, 100, 200, 200}));
Paths64 result = InflatePaths(openPath, 10, JoinType::Round, EndType::Round);
// 路径两端是圆形的

特点

  • 端点处为半圆形
  • 视觉效果平滑
  • 偏移后的长度增加 2 * delta

4.5.6 端点类型对比

端点类型 描述 长度变化 适用场景
Polygon 闭合多边形 不适用 闭合图形
Joined 开放路径闭合 不适用 创建管状区域
Butt 平头截断 无变化 精确边界
Square 方形延伸 +2*delta 方形端帽
Round 圆形端帽 +2*delta 圆滑端帽

4.6 使用ClipperOffset类

对于需要更多控制的场景,可以使用ClipperOffset类。

4.6.1 基本使用

#include "clipper2/clipper.h"
using namespace Clipper2Lib;

int main() {
    // 创建ClipperOffset对象
    ClipperOffset offsetter;
    
    // 设置属性
    offsetter.MiterLimit(2.0);
    offsetter.ArcTolerance(0.25);
    
    // 添加路径
    Paths64 paths;
    paths.push_back(MakePath({100, 100, 200, 100, 200, 200, 100, 200}));
    offsetter.AddPaths(paths, JoinType::Round, EndType::Polygon);
    
    // 执行偏移
    Paths64 result;
    offsetter.Execute(10, result);  // 偏移10个单位
    
    return 0;
}

4.6.2 多次偏移

使用ClipperOffset类可以对同一组路径执行多次不同距离的偏移:

ClipperOffset offsetter;
offsetter.AddPaths(paths, JoinType::Round, EndType::Polygon);

// 执行多个不同距离的偏移
Paths64 offset5, offset10, offset15;
offsetter.Execute(5, offset5);
offsetter.Execute(10, offset10);
offsetter.Execute(15, offset15);

// 所有偏移都基于原始路径

4.6.3 使用PolyTree输出

ClipperOffset offsetter;
offsetter.AddPaths(paths, JoinType::Round, EndType::Polygon);

PolyTree64 tree;
offsetter.Execute(10, tree);

// 遍历PolyTree获取层次结构
// 这对于负向偏移产生的孔洞很有用

4.6.4 清空和复用

ClipperOffset offsetter;

for (int i = 0; i < 100; i++) {
    // 清空之前的数据
    offsetter.Clear();
    
    // 添加新的路径
    offsetter.AddPaths(GetPaths(i), JoinType::Round, EndType::Polygon);
    
    // 执行偏移
    Paths64 result;
    offsetter.Execute(10, result);
}

4.6.5 属性设置

ClipperOffset offsetter;

// 设置斜角限制(用于Miter连接)
offsetter.MiterLimit(4.0);

// 设置弧度容差(用于Round连接)
offsetter.ArcTolerance(0.1);

// 设置是否保留共线点
offsetter.PreserveCollinear(true);

// 设置是否反转解决方案
offsetter.ReverseSolution(false);

4.7 处理复杂情况

4.7.1 带孔洞的多边形

当对带孔洞的多边形进行偏移时,外边界和孔洞会以相反的方向偏移:

// 创建带孔洞的多边形
Paths64 paths;
// 外边界(逆时针)
paths.push_back(MakePath({0, 0, 100, 0, 100, 100, 0, 100}));
// 孔洞(顺时针)
paths.push_back(MakePath({25, 25, 25, 75, 75, 75, 75, 25}));

// 正向偏移:外边界扩张,孔洞收缩
Paths64 inflated = InflatePaths(paths, 10, JoinType::Round, EndType::Polygon);

// 负向偏移:外边界收缩,孔洞扩张
Paths64 deflated = InflatePaths(paths, -10, JoinType::Round, EndType::Polygon);

4.7.2 自交处理

当偏移距离较大时,可能会产生自交。Clipper2会自动检测并处理这些情况:

// 创建一个凹多边形
Paths64 paths;
paths.push_back(MakePath({0, 0, 100, 0, 100, 50, 50, 50, 50, 100, 0, 100}));

// 大距离负向偏移可能导致多边形消失或分裂
Paths64 result = InflatePaths(paths, -20, JoinType::Round, EndType::Polygon);
// result 可能为空或包含多个分离的多边形

4.7.3 多边形消失

当负向偏移距离足够大时,多边形可能完全消失:

// 一个小正方形
Paths64 paths;
paths.push_back(MakePath({0, 0, 20, 0, 20, 20, 0, 20}));

// 偏移距离大于宽度的一半
Paths64 result = InflatePaths(paths, -15, JoinType::Round, EndType::Polygon);
// result 为空,因为多边形已完全收缩消失

4.7.4 岛屿生成

对于带孔洞的多边形,负向偏移可能导致孔洞扩张并与外边界相遇,产生新的岛屿:

// 创建一个环形(外边界加孔洞)
Paths64 ring;
ring.push_back(MakePath({0, 0, 100, 0, 100, 100, 0, 100}));  // 外边界
ring.push_back(MakePath({25, 25, 25, 75, 75, 75, 75, 25}));  // 孔洞

// 负向偏移使孔洞扩张
Paths64 deflated = InflatePaths(ring, -20, JoinType::Round, EndType::Polygon);
// 当偏移距离足够大时,可能产生复杂的结果

4.7.5 使用PolyTree追踪层次

使用PolyTree输出可以追踪偏移后多边形的层次关系:

ClipperOffset offsetter;
offsetter.AddPaths(paths, JoinType::Round, EndType::Polygon);

PolyTree64 tree;
offsetter.Execute(-10, tree);

// 遍历PolyTree
void ProcessTree(const PolyPath64* node, int depth) {
    if (!node->Polygon().empty()) {
        std::cout << std::string(depth * 2, ' ');
        std::cout << (node->IsHole() ? "孔洞" : "多边形");
        std::cout << " 顶点数: " << node->Polygon().size() << std::endl;
    }
    for (auto child = node->begin(); child != node->end(); ++child) {
        ProcessTree(*child, depth + 1);
    }
}

for (auto child = tree.begin(); child != tree.end(); ++child) {
    ProcessTree(*child, 0);
}

4.8 实际应用示例

4.8.1 CNC刀具路径补偿

// 零件轮廓
Paths64 partContour;
partContour.push_back(MakePath({...}));

// 刀具半径(例如5mm刀具)
double toolRadius = 5.0 * 1000;  // 假设使用0.001mm作为单位

// 外轮廓加工:向外偏移刀具半径
Paths64 outerToolPath = InflatePaths(partContour, toolRadius, 
                                      JoinType::Round, EndType::Polygon);

// 内轮廓(口袋)加工:向内偏移刀具半径
Paths64 innerToolPath = InflatePaths(partContour, -toolRadius,
                                      JoinType::Round, EndType::Polygon);

4.8.2 3D打印轮廓生成

// 切片轮廓
Paths64 sliceContour;
sliceContour.push_back(MakePath({...}));

// 挤出线宽(例如0.4mm)
double lineWidth = 0.4 * 1000;  // 使用0.001mm作为单位

// 生成多层偏移轮廓
std::vector<Paths64> perimeterLayers;
for (int i = 0; i < 3; i++) {
    double offset = -lineWidth * (0.5 + i);  // 第一层偏移半个线宽
    Paths64 perimeter = InflatePaths(sliceContour, offset,
                                      JoinType::Miter, EndType::Polygon);
    if (!perimeter.empty()) {
        perimeterLayers.push_back(perimeter);
    }
}

4.8.3 地图缓冲区分析

// 道路中心线
Paths64 roadCenterlines;
roadCenterlines.push_back(MakePath({...}));

// 创建道路缓冲区(道路宽度的一半)
double halfRoadWidth = 7.5 * 1000;  // 15米宽道路
Paths64 roadBuffer = InflatePaths(roadCenterlines, halfRoadWidth,
                                   JoinType::Round, EndType::Round);

// 创建噪音影响区域(道路两侧50米)
double noiseZone = 50.0 * 1000;
Paths64 noiseBuffer = InflatePaths(roadCenterlines, noiseZone,
                                    JoinType::Round, EndType::Round);

4.8.4 安全边界生成

// 建筑物轮廓
Paths64 buildings;
for (const auto& building : buildingList) {
    buildings.push_back(building.getContour());
}

// 创建安全间距(5米)
double safetyDistance = 5.0 * 1000;
Paths64 safetyZones = InflatePaths(buildings, safetyDistance,
                                    JoinType::Round, EndType::Polygon);

// 合并重叠的安全区域
Paths64 mergedSafety = Union(safetyZones, FillRule::NonZero);

4.8.5 字体描边效果

// 文字轮廓(已转换为多边形)
Paths64 textContour;
textContour.push_back(MakePath({...}));  // 字符轮廓

// 创建描边效果
double strokeWidth = 2.0 * 1000;  // 2mm描边

// 外描边
Paths64 outerStroke = InflatePaths(textContour, strokeWidth,
                                    JoinType::Round, EndType::Polygon);
// 描边区域 = 外描边 - 原始轮廓
Paths64 strokeOnly = Difference(outerStroke, textContour, FillRule::NonZero);

// 或者创建内描边
Paths64 innerStroke = InflatePaths(textContour, -strokeWidth,
                                    JoinType::Round, EndType::Polygon);
Paths64 innerStrokeOnly = Difference(textContour, innerStroke, FillRule::NonZero);

4.8.6 碰撞边界扩展

// 游戏中的角色边界
Paths64 characterBounds;
characterBounds.push_back(MakePath({...}));

// 扩展碰撞检测范围(预警区域)
double warningZone = 10.0;  // 10像素
Paths64 collisionWarning = InflatePaths(characterBounds, warningZone,
                                         JoinType::Round, EndType::Polygon);

// 检查是否有物体进入预警区域
Paths64 obstacles = GetObstacles();
Paths64 potentialCollisions = Intersect(collisionWarning, obstacles, FillRule::NonZero);
if (!potentialCollisions.empty()) {
    // 有潜在碰撞风险
}

4.9 性能优化

4.9.1 控制圆弧精度

圆弧精度是影响性能的主要因素之一:

// 低精度(快速)
Paths64 fast = InflatePaths(paths, 10, JoinType::Round, EndType::Polygon, 2.0, 2.0);

// 高精度(慢速)
Paths64 precise = InflatePaths(paths, 10, JoinType::Round, EndType::Polygon, 2.0, 0.1);

选择合适的精度

// 根据偏移距离自动计算合适的弧度容差
double arcTolerance = delta / 50.0;  // 经验值
if (arcTolerance < 0.1) arcTolerance = 0.1;
if (arcTolerance > 1.0) arcTolerance = 1.0;

Paths64 result = InflatePaths(paths, delta, JoinType::Round, EndType::Polygon, 2.0, arcTolerance);

4.9.2 使用Miter连接

当不需要圆滑的拐角时,使用Miter连接可以显著提高性能:

// Round连接需要生成多个圆弧顶点
Paths64 roundResult = InflatePaths(paths, 10, JoinType::Round, EndType::Polygon);
// 顶点数量可能增加很多

// Miter连接保持相同的顶点数量
Paths64 miterResult = InflatePaths(paths, 10, JoinType::Miter, EndType::Polygon);
// 顶点数量与原始相同

4.9.3 预处理路径

在偏移之前简化路径可以提高性能:

// 简化路径,移除共线点和接近的点
Paths64 simplified = SimplifyPaths(paths, 0.5);

// 然后进行偏移
Paths64 result = InflatePaths(simplified, 10, JoinType::Round, EndType::Polygon);

4.9.4 批量处理

复用ClipperOffset对象:

ClipperOffset offsetter;
offsetter.ArcTolerance(0.25);

std::vector<Paths64> results(inputs.size());

for (size_t i = 0; i < inputs.size(); i++) {
    offsetter.Clear();
    offsetter.AddPaths(inputs[i], JoinType::Round, EndType::Polygon);
    offsetter.Execute(10, results[i]);
}

4.10 常见问题与解决方案

4.10.1 偏移结果为空

问题:负向偏移后结果为空。

解决方案

// 检查偏移距离是否过大
double minDistance = GetMinimumOffset(paths);  // 计算最大可能的内偏移距离
if (std::abs(delta) > minDistance) {
    // 警告用户偏移距离过大
}

4.10.2 意外的自交

问题:偏移后出现意外的自交或孔洞。

解决方案

// 使用Union清理结果
Paths64 result = InflatePaths(paths, delta, JoinType::Round, EndType::Polygon);
Paths64 cleaned = Union(result, FillRule::NonZero);

4.10.3 性能问题

问题:偏移操作非常慢。

解决方案

// 1. 降低圆弧精度
double arcTol = delta / 20.0;

// 2. 使用Miter连接代替Round
Paths64 result = InflatePaths(paths, delta, JoinType::Miter, EndType::Polygon);

// 3. 预先简化路径
Paths64 simplified = SimplifyPaths(paths, 1.0);
Paths64 result = InflatePaths(simplified, delta, JoinType::Round, EndType::Polygon);

4.10.4 圆弧质量问题

问题:圆弧看起来不够圆滑。

解决方案

// 降低弧度容差
Paths64 result = InflatePaths(paths, delta, JoinType::Round, EndType::Polygon, 2.0, 0.1);
// 或者使用自动计算
Paths64 result = InflatePaths(paths, delta, JoinType::Round, EndType::Polygon, 2.0, 0.0);

4.11 本章小结

本章我们深入学习了Clipper2的多边形偏移功能:

  1. 基本概念:偏移、膨胀、收缩的含义及方向约定
  2. 连接类型:Round、Miter、Square、Bevel及其特点
  3. 端点类型:Polygon、Joined、Butt、Square、Round及其应用场景
  4. ClipperOffset类:高级控制选项和多次偏移
  5. 复杂情况处理:孔洞、自交、消失、岛屿
  6. 实际应用:CNC加工、3D打印、地图缓冲区、碰撞检测
  7. 性能优化:弧度精度、连接类型选择、路径预处理

在下一章中,我们将学习Clipper2的矩形裁剪和闵可夫斯基运算功能。

posted @ 2025-11-29 13:40  我才是银古  阅读(3)  评论(0)    收藏  举报