第20章:实际应用与最佳实践
20.1 概述
本章将介绍 Clipper 在实际项目中的应用场景、性能优化技巧和最佳实践。
20.2 典型应用场景
20.2.1 CAD/CAM 应用
刀具补偿:
// CNC 刀具补偿
Path toolPath = GetOriginalPath();
double toolRadius = 5.0; // 刀具半径
ClipperOffset co = new ClipperOffset();
co.AddPath(toolPath, JoinType.jtRound, EndType.etClosedPolygon);
Paths compensated = new Paths();
co.Execute(ref compensated, -toolRadius); // 内缩
// compensated 是补偿后的刀具路径
层叠切割:
// 多层切割路径生成
Path outerBoundary = GetMaterialBoundary();
double stepover = 3.0; // 切削步距
List<Paths> layers = new List<Paths>();
ClipperOffset co = new ClipperOffset();
double offset = 0;
while (true)
{
co.Clear();
co.AddPath(outerBoundary, JoinType.jtRound, EndType.etClosedPolygon);
Paths layer = new Paths();
co.Execute(ref layer, -offset);
if (layer.Count == 0) break;
layers.Add(layer);
offset += stepover;
}
20.2.2 GIS 应用
缓冲区分析:
// 河流缓冲区
Path riverCenterline = LoadRiverPath();
double bufferDistance = 100; // 100米缓冲区
ClipperOffset co = new ClipperOffset();
co.AddPath(riverCenterline, JoinType.jtRound, EndType.etOpenRound);
Paths floodZone = new Paths();
co.Execute(ref floodZone, bufferDistance);
多边形叠加分析:
// 土地利用叠加
Paths landuse = LoadLandusePolygons();
Paths zoning = LoadZoningPolygons();
Clipper c = new Clipper();
c.AddPaths(landuse, PolyType.ptSubject, true);
c.AddPaths(zoning, PolyType.ptClip, true);
Paths intersection = new Paths();
c.Execute(ClipType.ctIntersection, intersection);
20.2.3 图形设计
文字描边:
// 为文字添加轮廓
Path textGlyph = GetTextGlyphPath('A');
ClipperOffset co = new ClipperOffset();
co.AddPath(textGlyph, JoinType.jtRound, EndType.etClosedPolygon);
// 内外轮廓
Paths outerStroke = new Paths();
Paths innerText = new Paths();
co.Execute(ref outerStroke, 3); // 外扩
co.Clear();
co.AddPath(textGlyph, JoinType.jtRound, EndType.etClosedPolygon);
co.Execute(ref innerText, -1); // 内缩(可选的瘦体效果)
20.2.4 游戏开发
碰撞检测:
// 检测两个多边形是否重叠
bool CheckCollision(Path poly1, Path poly2)
{
Clipper c = new Clipper();
c.AddPath(poly1, PolyType.ptSubject, true);
c.AddPath(poly2, PolyType.ptClip, true);
Paths intersection = new Paths();
c.Execute(ClipType.ctIntersection, intersection);
return intersection.Count > 0;
}
// 计算重叠面积
double GetOverlapArea(Path poly1, Path poly2)
{
Clipper c = new Clipper();
c.AddPath(poly1, PolyType.ptSubject, true);
c.AddPath(poly2, PolyType.ptClip, true);
Paths intersection = new Paths();
c.Execute(ClipType.ctIntersection, intersection);
double area = 0;
foreach (var p in intersection)
area += Math.Abs(Clipper.Area(p));
return area;
}
视野计算:
// 视野锥与障碍物的交集
Path viewCone = CreateViewCone(position, direction, angle, distance);
Paths obstacles = GetObstacles();
Clipper c = new Clipper();
c.AddPath(viewCone, PolyType.ptSubject, true);
c.AddPaths(obstacles, PolyType.ptClip, true);
Paths visibleArea = new Paths();
c.Execute(ClipType.ctDifference, visibleArea);
20.3 性能优化
20.3.1 坐标缩放
// 使用整数坐标时的缩放策略
public class ScaledClipper
{
private const double Scale = 1000.0; // 缩放因子
public static IntPoint ToClipperPoint(double x, double y)
{
return new IntPoint((cInt)(x * Scale), (cInt)(y * Scale));
}
public static PointF FromClipperPoint(IntPoint pt)
{
return new PointF((float)(pt.X / Scale), (float)(pt.Y / Scale));
}
public static Path ScalePath(IEnumerable<PointF> points)
{
Path result = new Path();
foreach (var pt in points)
result.Add(ToClipperPoint(pt.X, pt.Y));
return result;
}
public static List<PointF> UnscalePath(Path path)
{
var result = new List<PointF>();
foreach (var pt in path)
result.Add(FromClipperPoint(pt));
return result;
}
}
20.3.2 批量操作
// 批量处理多个布尔运算
public static Paths BatchUnion(List<Path> polygons)
{
if (polygons.Count == 0) return new Paths();
if (polygons.Count == 1) return new Paths { polygons[0] };
// 分治法合并
while (polygons.Count > 1)
{
List<Path> merged = new List<Path>();
Clipper c = new Clipper();
for (int i = 0; i < polygons.Count; i += 2)
{
c.Clear();
c.AddPath(polygons[i], PolyType.ptSubject, true);
if (i + 1 < polygons.Count)
c.AddPath(polygons[i + 1], PolyType.ptClip, true);
Paths result = new Paths();
c.Execute(ClipType.ctUnion, result);
foreach (var p in result)
merged.Add(p);
}
polygons = merged;
}
return new Paths { polygons[0] };
}
20.3.3 预分配内存
// 预分配容量
Path polygon = new Path(expectedPointCount);
Paths result = new Paths(expectedPathCount);
// 复用 Clipper 实例
Clipper clipper = new Clipper();
for (int i = 0; i < iterations; i++)
{
clipper.Clear(); // 清理但保留分配的内存
// ... 执行操作 ...
}
20.3.4 避免高精度模式
// 检查是否需要高精度
bool RequiresHighPrecision(Paths paths)
{
foreach (var path in paths)
foreach (var pt in path)
if (Math.Abs(pt.X) > ClipperBase.loRange ||
Math.Abs(pt.Y) > ClipperBase.loRange)
return true;
return false;
}
// 如果可能,缩放坐标以避免高精度
void NormalizeCoordinates(Paths paths, out double scale)
{
long maxCoord = 1;
foreach (var path in paths)
foreach (var pt in path)
{
maxCoord = Math.Max(maxCoord, Math.Abs(pt.X));
maxCoord = Math.Max(maxCoord, Math.Abs(pt.Y));
}
if (maxCoord > ClipperBase.loRange)
{
scale = (double)ClipperBase.loRange / maxCoord * 0.9;
ScalePaths(paths, scale);
}
else
scale = 1.0;
}
20.4 常见问题与解决方案
20.4.1 自相交处理
// 问题:输入多边形自相交
// 解决:使用 SimplifyPolygon
Path problematic = GetSelfIntersectingPath();
Paths fixed = Clipper.SimplifyPolygon(problematic);
20.4.2 微小间隙
// 问题:微小间隙导致分离
// 解决:先膨胀后收缩
Paths withGaps = GetPathsWithSmallGaps();
ClipperOffset co = new ClipperOffset();
co.AddPaths(withGaps, JoinType.jtRound, EndType.etClosedPolygon);
Paths expanded = new Paths();
co.Execute(ref expanded, 2);
co.Clear();
co.AddPaths(expanded, JoinType.jtRound, EndType.etClosedPolygon);
Paths closed = new Paths();
co.Execute(ref closed, -2);
该模式即形态学闭运算——先膨胀后收缩,用于弥合窄缝、填充凹陷;反向组合(先收缩后膨胀)为开运算,用于剔除细小突起与碎图斑(详见第17章 17.12 节与第19章 19.4.3 节)。
20.4.3 浮点精度问题
// 问题:浮点坐标导致不一致
// 解决:使用适当的缩放因子
const double Scale = 1e6; // 保留6位小数
Path ScaleUp(IEnumerable<PointD> points)
{
Path result = new Path();
foreach (var pt in points)
result.Add(new IntPoint((long)(pt.X * Scale), (long)(pt.Y * Scale)));
return result;
}
List<PointD> ScaleDown(Path path)
{
var result = new List<PointD>();
foreach (var pt in path)
result.Add(new PointD(pt.X / Scale, pt.Y / Scale));
return result;
}
20.4.4 方向不一致
// 确保所有外轮廓逆时针,孔洞顺时针
void NormalizeOrientation(Paths paths)
{
foreach (var path in paths)
{
double area = Clipper.Area(path);
// 假设第一个是外轮廓
if (path == paths[0])
{
if (area < 0) path.Reverse();
}
else
{
// 孔洞应该顺时针
if (area > 0) path.Reverse();
}
}
}
20.5 与其他库的集成
20.5.1 与 System.Drawing 集成
using System.Drawing;
using System.Drawing.Drawing2D;
public static GraphicsPath ToGraphicsPath(Paths paths)
{
GraphicsPath gp = new GraphicsPath();
foreach (var path in paths)
{
if (path.Count < 3) continue;
PointF[] points = new PointF[path.Count];
for (int i = 0; i < path.Count; i++)
points[i] = new PointF(path[i].X, path[i].Y);
gp.AddPolygon(points);
}
return gp;
}
20.5.2 与 WPF 集成
using System.Windows.Media;
public static PathGeometry ToWpfGeometry(Paths paths)
{
PathGeometry pg = new PathGeometry();
foreach (var path in paths)
{
if (path.Count < 3) continue;
PathFigure figure = new PathFigure();
figure.StartPoint = new Point(path[0].X, path[0].Y);
figure.IsClosed = true;
PolyLineSegment segment = new PolyLineSegment();
for (int i = 1; i < path.Count; i++)
segment.Points.Add(new Point(path[i].X, path[i].Y));
figure.Segments.Add(segment);
pg.Figures.Add(figure);
}
return pg;
}
20.5.3 与 Unity 集成
using UnityEngine;
public static Mesh ToUnityMesh(Paths paths)
{
// 使用三角剖分库(如 Poly2Tri)
List<Vector3> vertices = new List<Vector3>();
List<int> triangles = new List<int>();
// 转换并三角剖分
// ...
Mesh mesh = new Mesh();
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles.ToArray();
mesh.RecalculateNormals();
return mesh;
}
20.6 调试技巧
20.6.1 可视化输出
public static void DebugDrawPaths(Paths paths, string filename)
{
IntRect bounds = ClipperBase.GetBounds(paths);
int margin = 10;
int width = (int)(bounds.right - bounds.left + 2 * margin);
int height = (int)(bounds.bottom - bounds.top + 2 * margin);
using (Bitmap bmp = new Bitmap(width, height))
using (Graphics g = Graphics.FromImage(bmp))
{
g.Clear(Color.White);
foreach (var path in paths)
{
if (path.Count < 2) continue;
PointF[] pts = new PointF[path.Count];
for (int i = 0; i < path.Count; i++)
pts[i] = new PointF(
path[i].X - bounds.left + margin,
path[i].Y - bounds.top + margin);
g.DrawPolygon(Pens.Blue, pts);
}
bmp.Save(filename);
}
}
20.6.2 数据验证
public static bool ValidatePaths(Paths paths, out string error)
{
error = null;
foreach (var path in paths)
{
// 检查最小顶点数
if (path.Count < 3)
{
error = "Path has fewer than 3 vertices";
return false;
}
// 检查坐标范围
foreach (var pt in path)
{
if (Math.Abs(pt.X) > ClipperBase.hiRange ||
Math.Abs(pt.Y) > ClipperBase.hiRange)
{
error = "Coordinate out of range";
return false;
}
}
// 检查自相交
Paths simplified = Clipper.SimplifyPolygon(path);
if (simplified.Count != 1)
{
error = "Self-intersecting polygon";
return false;
}
}
return true;
}
20.7 形态学应用案例
形态学的开/闭运算与梯度可由 ClipperOffset 与布尔运算组合实现。以下案例均约定:整数坐标、缩放因子 1e6、外轮廓为 CCW(正面积)方向、结构元素用 jtRound 圆盘、偏移经 Execute(ref solution, delta) 完成。
20.7.1 案例 1:小图斑剔除(开运算)
先负偏移(腐蚀)再正偏移(膨胀),剔除小于结构元素尺度的碎图斑;再按面积阈值过滤残留碎块。
using ClipperLib;
const double scale = 1e6;
const double r = 2.0 * scale; // 结构元素半径 2 米
const double minArea = 100.0 * scale * scale; // 最小图斑面积阈值
public static Paths RemoveSmallPolygons(Paths input)
{
// 开运算:先腐蚀(负偏移)后膨胀(正偏移)
ClipperOffset co = new ClipperOffset();
co.AddPaths(input, JoinType.jtRound, EndType.etClosedPolygon);
Paths eroded = new Paths();
co.Execute(ref eroded, -r); // ref + (solution, delta)
co.Clear();
co.AddPaths(eroded, JoinType.jtRound, EndType.etClosedPolygon);
Paths opened = new Paths();
co.Execute(ref opened, r);
// 面积阈值过滤残留碎图斑
Paths result = new Paths();
foreach (Path p in opened)
if (Math.Abs(Clipper.Area(p)) >= minArea)
result.Add(p);
return result;
}
腐蚀会直接消灭小于结构元素尺度的图斑;开运算后的残留再经 Clipper.Area 按阈值过滤。外轮廓需保持 CCW,否则正负偏移语义颠倒。
20.7.2 案例 2:窄缝弥合与凹口填充(闭运算)
先正偏移(膨胀)后负偏移(收缩),弥合相邻图斑间的窄缝并填充轮廓凹口。
using ClipperLib;
const double scale = 1e6;
const double r = 1.0 * scale; // 弥合间隙的半径
public static Paths CloseGaps(Paths input)
{
ClipperOffset co = new ClipperOffset();
co.AddPaths(input, JoinType.jtRound, EndType.etClosedPolygon);
Paths dilated = new Paths();
co.Execute(ref dilated, r); // 先膨胀:弥合窄缝、填充凹口
co.Clear();
co.AddPaths(dilated, JoinType.jtRound, EndType.etClosedPolygon);
Paths closed = new Paths();
co.Execute(ref closed, -r); // 后收缩:恢复近似原始尺寸
return closed;
}
间隙小于 2r 的两个图斑在膨胀后会粘连成一个连通区域,收缩后仍保持连通;凹口深度小于 2r 时被填平。
20.7.3 案例 3:边界带提取(形态学梯度)
膨胀结果与腐蚀结果的差集即形态学梯度,得到沿轮廓宽约 2r 的边界带。
using ClipperLib;
const double scale = 1e6;
const double r = 1.5 * scale;
public static Paths BoundaryBand(Paths input)
{
ClipperOffset co = new ClipperOffset();
co.AddPaths(input, JoinType.jtRound, EndType.etClosedPolygon);
Paths dilated = new Paths();
co.Execute(ref dilated, r); // 膨胀
co.Clear();
co.AddPaths(input, JoinType.jtRound, EndType.etClosedPolygon);
Paths eroded = new Paths();
co.Execute(ref eroded, -r); // 腐蚀
Clipper clipper = new Clipper();
clipper.AddPaths(dilated, PolyType.ptSubject, true);
clipper.AddPaths(eroded, PolyType.ptClip, true);
Paths gradient = new Paths();
clipper.Execute(ClipType.ctDifference, gradient,
PolyFillType.pftNonZero, PolyFillType.pftNonZero);
return gradient;
}
gradient 为图斑的边界带(膨胀减腐蚀),可直接用于边界可视化或后续沿线缓冲区计算。腐蚀结果为空时梯度退化为纯膨胀结果,调用方需自行判断。
20.8 本章小结
本章介绍了 Clipper 的实际应用与最佳实践:
- 应用场景:
- CAD/CAM:刀具补偿、层叠切割
- GIS:缓冲区分析、叠加分析
- 图形设计:描边、轮廓
- 游戏开发:碰撞检测、视野计算
- 性能优化:
- 坐标缩放
- 批量处理
- 预分配内存
- 避免高精度模式
- 常见问题:
- 自相交处理
- 微小间隙
- 浮点精度
- 方向一致性
- 库集成:
- System.Drawing
- WPF
- Unity
- 调试技巧:
- 可视化输出
- 数据验证
| 上一章:辅助函数与工具 | 返回目录 |