第四章 多边形偏移操作(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的多边形偏移功能:
- 基本概念:偏移、膨胀、收缩的含义及方向约定
- 连接类型:Round、Miter、Square、Bevel及其特点
- 端点类型:Polygon、Joined、Butt、Square、Round及其应用场景
- ClipperOffset类:高级控制选项和多次偏移
- 复杂情况处理:孔洞、自交、消失、岛屿
- 实际应用:CNC加工、3D打印、地图缓冲区、碰撞检测
- 性能优化:弧度精度、连接类型选择、路径预处理
在下一章中,我们将学习Clipper2的矩形裁剪和闵可夫斯基运算功能。