第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 的多边形偏移功能:
- ClipperOffset 类:专门用于偏移操作的类
- JoinType:三种连接类型(Round、Square、Miter)及其应用
- EndType:五种端点类型及其选择
- 正负偏移:膨胀和收缩操作
- 实际应用:CNC 路径生成、建筑退界、PCB 设计检查等
- 性能优化:参数选择和问题处理
重点掌握:
- 根据应用场景选择合适的连接类型和端点类型
- 理解正负偏移的几何意义
- 处理偏移操作中的各种边缘情况
- 在实际项目中灵活应用偏移功能
在下一章中,我们将学习填充规则的详细应用、多边形简化以及其他高级特性。
4.9 练习题
-
基础练习:创建一个五角星,分别使用三种连接类型进行偏移,观察差异
-
开放路径:创建一条折线,使用不同的端点类型进行偏移,比较结果
-
CNC 应用:为一个复杂零件生成多层铣削刀具路径
-
建筑设计:计算一块不规则地块的建筑红线和控制线
-
多级偏移:创建等高线效果,生成10层同心轮廓
-
综合应用:实现一个简单的 PCB 设计规则检查工具

浙公网安备 33010602011771号