第5章_填充规则与高级特性
第五章 填充规则与高级特性
5.1 引言
在前面的章节中,我们学习了 Clipper1 的核心功能:布尔运算和多边形偏移。在本章中,我们将深入探讨填充规则(Fill Rules)的细节,以及 Clipper1 提供的其他高级特性,包括多边形简化、方向性处理、Z 轴值处理等。这些功能虽然不像布尔运算那样显眼,但在处理复杂几何问题时同样重要。
理解填充规则对于正确使用 Clipper1 至关重要,因为它直接影响布尔运算的结果。而掌握高级特性则能让您更高效地解决实际问题,处理各种边缘情况。
5.2 填充规则深入解析
5.2.1 填充规则的基本概念
填充规则(Fill Rule)决定了如何判断平面上的一个点是在多边形的"内部"还是"外部"。这个看似简单的问题,在处理复杂多边形(特别是自相交多边形)时变得非常微妙。
Clipper1 支持四种填充规则:
- EvenOdd(奇偶规则)
- NonZero(非零规则)
- Positive(正数规则)
- Negative(负数规则)
5.2.2 EvenOdd(奇偶规则)
原理:
从测试点向任意方向发射一条射线,计算射线与多边形边的交点数:
- 奇数个交点 → 点在内部
- 偶数个交点 → 点在外部
特点:
- 最简单直观的规则
- 不考虑多边形的方向
- 对自相交多边形的处理方式独特
示例代码:
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 EvenOddFillExample
{
private const double SCALE = 1000.0;
static void DemonstrateEvenOdd()
{
// 创建一个自相交的八字形
Path figureEight = new Path
{
new IntPoint(0, 0),
new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
new IntPoint((long)(100 * SCALE), 0),
new IntPoint(0, (long)(100 * SCALE))
};
// 使用 EvenOdd 规则进行并集运算
Clipper clipper = new Clipper();
clipper.AddPath(figureEight, PolyType.ptSubject, true);
Paths solution = new Paths();
clipper.Execute(
ClipType.ctUnion,
solution,
PolyFillType.pftEvenOdd,
PolyFillType.pftEvenOdd
);
Console.WriteLine("EvenOdd 规则结果:");
Console.WriteLine($" 产生多边形数: {solution.Count}");
double totalArea = 0;
for (int i = 0; i < solution.Count; i++)
{
double area = Math.Abs(Clipper.Area(solution[i])) / (SCALE * SCALE);
totalArea += area;
Console.WriteLine($" 多边形 {i + 1} 面积: {area:F2}");
}
Console.WriteLine($" 总面积: {totalArea:F2}");
}
}
应用场景:
- 处理重叠区域时需要"挖空"效果
- SVG 路径的某些填充模式
- 需要交替填充效果的图形
5.2.3 NonZero(非零规则)
原理:
从测试点发射射线,计算环绕数(Winding Number):
- 边从左到右穿过射线:+1
- 边从右到左穿过射线:-1
- 环绕数不为零 → 点在内部
- 环绕数为零 → 点在外部
特点:
- 考虑多边形的方向
- 最符合直觉的填充方式
- Clipper1 推荐使用的默认规则
示例代码:
class NonZeroFillExample
{
private const double SCALE = 1000.0;
static void DemonstrateNonZero()
{
// 创建同样的八字形
Path figureEight = new Path
{
new IntPoint(0, 0),
new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
new IntPoint((long)(100 * SCALE), 0),
new IntPoint(0, (long)(100 * SCALE))
};
// 使用 NonZero 规则进行并集运算
Clipper clipper = new Clipper();
clipper.AddPath(figureEight, PolyType.ptSubject, true);
Paths solution = new Paths();
clipper.Execute(
ClipType.ctUnion,
solution,
PolyFillType.pftNonZero,
PolyFillType.pftNonZero
);
Console.WriteLine("NonZero 规则结果:");
Console.WriteLine($" 产生多边形数: {solution.Count}");
double totalArea = 0;
for (int i = 0; i < solution.Count; i++)
{
double area = Math.Abs(Clipper.Area(solution[i])) / (SCALE * SCALE);
totalArea += area;
Console.WriteLine($" 多边形 {i + 1} 面积: {area:F2}");
}
Console.WriteLine($" 总面积: {totalArea:F2}");
}
}
应用场景:
- 大多数常规的几何运算
- 需要考虑方向性的场合
- 带孔洞的多边形处理
5.2.4 Positive 和 Negative 规则
Positive 规则:
- 只有正环绕数的区域被认为在内部
- 用于只考虑逆时针多边形的情况
Negative 规则:
- 只有负环绕数的区域被认为在内部
- 用于只考虑顺时针多边形的情况
示例代码:
class PositiveNegativeFillExample
{
private const double SCALE = 1000.0;
static void CompareAllFillTypes()
{
// 创建两个重叠的正方形
Path square1 = 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))
};
Path square2 = new Path
{
new IntPoint((long)(50 * SCALE), (long)(50 * SCALE)),
new IntPoint((long)(150 * SCALE), (long)(50 * SCALE)),
new IntPoint((long)(150 * SCALE), (long)(150 * SCALE)),
new IntPoint((long)(50 * SCALE), (long)(150 * SCALE))
};
// 反转第二个正方形的方向
square2.Reverse();
PolyFillType[] fillTypes = {
PolyFillType.pftEvenOdd,
PolyFillType.pftNonZero,
PolyFillType.pftPositive,
PolyFillType.pftNegative
};
string[] names = { "EvenOdd", "NonZero", "Positive", "Negative" };
Console.WriteLine("=== 填充规则对比 ===\n");
for (int i = 0; i < fillTypes.Length; i++)
{
Clipper clipper = new Clipper();
clipper.AddPath(square1, PolyType.ptSubject, true);
clipper.AddPath(square2, PolyType.ptSubject, true);
Paths solution = new Paths();
clipper.Execute(
ClipType.ctUnion,
solution,
fillTypes[i],
fillTypes[i]
);
Console.WriteLine($"{names[i]} 规则:");
Console.WriteLine($" 产生多边形数: {solution.Count}");
double totalArea = 0;
foreach (var path in solution)
{
totalArea += Math.Abs(Clipper.Area(path));
}
Console.WriteLine($" 总面积: {(totalArea / (SCALE * SCALE)):F2}\n");
}
}
}
5.2.5 填充规则的选择指南
class FillRuleSelector
{
/// <summary>
/// 根据场景选择填充规则
/// </summary>
public static PolyFillType SelectFillRule(string scenario)
{
switch (scenario.ToLower())
{
case "normal":
case "default":
// 大多数常规场景
return PolyFillType.pftNonZero;
case "svg":
case "alternating":
// SVG 路径或需要交替填充
return PolyFillType.pftEvenOdd;
case "counterclockwise":
case "outer":
// 只考虑逆时针(外轮廓)
return PolyFillType.pftPositive;
case "clockwise":
case "hole":
// 只考虑顺时针(孔洞)
return PolyFillType.pftNegative;
default:
return PolyFillType.pftNonZero;
}
}
/// <summary>
/// 分析多边形并推荐填充规则
/// </summary>
public static PolyFillType RecommendFillRule(Paths paths)
{
int clockwiseCount = 0;
int counterClockwiseCount = 0;
foreach (var path in paths)
{
bool isCounterClockwise = Clipper.Orientation(path);
if (isCounterClockwise)
counterClockwiseCount++;
else
clockwiseCount++;
}
// 如果全是同一方向,使用 NonZero
if (clockwiseCount == 0 || counterClockwiseCount == 0)
{
return PolyFillType.pftNonZero;
}
// 如果方向混合,可能有孔洞,使用 NonZero
return PolyFillType.pftNonZero;
}
}
5.3 多边形简化
5.3.1 SimplifyPolygon 方法
SimplifyPolygon 用于简化单个多边形,主要功能:
- 移除自相交
- 解开复杂的环路
- 标准化多边形表示
方法签名:
public static Paths SimplifyPolygon(
Path poly,
PolyFillType fillType = PolyFillType.pftEvenOdd
)
示例代码:
class PolygonSimplification
{
private const double SCALE = 1000.0;
/// <summary>
/// 简化自相交多边形
/// </summary>
static void SimplifySelfIntersecting()
{
// 创建一个自相交的多边形(蝴蝶结形)
Path complex = new Path
{
new IntPoint(0, 0),
new IntPoint((long)(100 * SCALE), (long)(100 * SCALE)),
new IntPoint((long)(100 * SCALE), 0),
new IntPoint(0, (long)(100 * SCALE)),
new IntPoint((long)(50 * SCALE), (long)(50 * SCALE)) // 自相交点
};
Console.WriteLine("原始多边形:");
Console.WriteLine($" 顶点数: {complex.Count}");
// 使用 NonZero 规则简化
Paths simplified = Clipper.SimplifyPolygon(
complex,
PolyFillType.pftNonZero
);
Console.WriteLine("\n简化后(NonZero):");
Console.WriteLine($" 产生多边形数: {simplified.Count}");
for (int i = 0; i < simplified.Count; i++)
{
double area = Math.Abs(Clipper.Area(simplified[i])) / (SCALE * SCALE);
Console.WriteLine($" 多边形 {i + 1}: 顶点数 = {simplified[i].Count}, 面积 = {area:F2}");
}
// 使用 EvenOdd 规则简化
Paths simplifiedEO = Clipper.SimplifyPolygon(
complex,
PolyFillType.pftEvenOdd
);
Console.WriteLine("\n简化后(EvenOdd):");
Console.WriteLine($" 产生多边形数: {simplifiedEO.Count}");
for (int i = 0; i < simplifiedEO.Count; i++)
{
double area = Math.Abs(Clipper.Area(simplifiedEO[i])) / (SCALE * SCALE);
Console.WriteLine($" 多边形 {i + 1}: 顶点数 = {simplifiedEO[i].Count}, 面积 = {area:F2}");
}
}
}
5.3.2 SimplifyPolygons 方法
批量简化多个多边形:
public static Paths SimplifyPolygons(
Paths polys,
PolyFillType fillType = PolyFillType.pftEvenOdd
)
示例:
class BatchSimplification
{
private const double SCALE = 1000.0;
/// <summary>
/// 批量简化多个多边形
/// </summary>
static void SimplifyMultiplePolygons()
{
Paths complexPolygons = new Paths();
// 创建几个复杂的多边形
for (int i = 0; i < 5; i++)
{
Path poly = CreateComplexPolygon(i * 30, i * 30);
complexPolygons.Add(poly);
}
Console.WriteLine($"原始多边形数: {complexPolygons.Count}");
int totalVerticesBefore = 0;
foreach (var poly in complexPolygons)
{
totalVerticesBefore += poly.Count;
}
Console.WriteLine($"总顶点数: {totalVerticesBefore}");
// 批量简化
Paths simplified = Clipper.SimplifyPolygons(
complexPolygons,
PolyFillType.pftNonZero
);
Console.WriteLine($"\n简化后多边形数: {simplified.Count}");
int totalVerticesAfter = 0;
double totalArea = 0;
foreach (var poly in simplified)
{
totalVerticesAfter += poly.Count;
totalArea += Math.Abs(Clipper.Area(poly));
}
Console.WriteLine($"总顶点数: {totalVerticesAfter}");
Console.WriteLine($"总面积: {(totalArea / (SCALE * SCALE)):F2}");
Console.WriteLine($"顶点减少: {((totalVerticesBefore - totalVerticesAfter) * 100.0 / totalVerticesBefore):F1}%");
}
static Path CreateComplexPolygon(double offsetX, double offsetY)
{
Path poly = new Path();
Random rand = new Random();
for (int i = 0; i < 20; i++)
{
double angle = 2 * Math.PI * i / 20;
double radius = 20 + rand.NextDouble() * 10;
double x = offsetX + radius * Math.Cos(angle);
double y = offsetY + radius * Math.Sin(angle);
poly.Add(new IntPoint((long)(x * SCALE), (long)(y * SCALE)));
}
return poly;
}
}
5.3.3 CleanPolygon 和 CleanPolygons
这些方法用于清理多边形,移除:
- 几乎共线的点
- 重复的点
- 非常短的边
方法签名:
public static void CleanPolygon(Path path, double distance = 1.415)
public static void CleanPolygons(Paths polys, double distance = 1.415)
参数:
distance:距离阈值,小于此值的点会被移除
示例:
class PolygonCleaning
{
private const double SCALE = 1000.0;
/// <summary>
/// 清理多边形,移除冗余点
/// </summary>
static void CleanRedundantPoints()
{
// 创建一个带有冗余点的多边形
Path messy = new Path
{
new IntPoint(0, 0),
new IntPoint((long)(10 * SCALE), 0),
new IntPoint((long)(10.001 * SCALE), 0), // 几乎重复
new IntPoint((long)(20 * SCALE), 0),
new IntPoint((long)(20 * SCALE), (long)(10 * SCALE)),
new IntPoint((long)(20 * SCALE), (long)(10.001 * SCALE)), // 几乎重复
new IntPoint((long)(20 * SCALE), (long)(20 * SCALE)),
new IntPoint((long)(10 * SCALE), (long)(20 * SCALE)),
new IntPoint((long)(10 * SCALE), (long)(19.999 * SCALE)), // 几乎重复
new IntPoint(0, (long)(20 * SCALE))
};
Console.WriteLine("清理前:");
Console.WriteLine($" 顶点数: {messy.Count}");
Console.WriteLine($" 面积: {Math.Abs(Clipper.Area(messy)) / (SCALE * SCALE):F2}");
// 清理多边形
Path cleaned = new Path(messy);
Clipper.CleanPolygon(cleaned, 1.0 * SCALE); // 1单位的容差
Console.WriteLine("\n清理后:");
Console.WriteLine($" 顶点数: {cleaned.Count}");
Console.WriteLine($" 面积: {Math.Abs(Clipper.Area(cleaned)) / (SCALE * SCALE):F2}");
Console.WriteLine($" 移除了 {messy.Count - cleaned.Count} 个冗余点");
}
/// <summary>
/// 自适应清理:根据多边形大小选择容差
/// </summary>
static double GetCleaningTolerance(Path polygon, double scale)
{
// 计算多边形的平均边长
double totalLength = 0;
int edgeCount = 0;
for (int i = 0; i < polygon.Count; i++)
{
int next = (i + 1) % polygon.Count;
long dx = polygon[next].X - polygon[i].X;
long dy = polygon[next].Y - polygon[i].Y;
totalLength += Math.Sqrt(dx * dx + dy * dy);
edgeCount++;
}
double avgLength = totalLength / edgeCount;
// 使用平均边长的1%作为容差
return avgLength * 0.01;
}
}
5.4 多边形方向性
5.4.1 Orientation 方法
判断多边形的方向(顺时针或逆时针):
public static bool Orientation(Path poly)
返回值:
true:逆时针方向(通常是外轮廓)false:顺时针方向(通常是孔洞)
示例:
class OrientationExample
{
private const double SCALE = 1000.0;
/// <summary>
/// 检查和标准化多边形方向
/// </summary>
static void NormalizeOrientation()
{
Path polygon = 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))
};
bool isCounterClockwise = Clipper.Orientation(polygon);
double area = Clipper.Area(polygon);
Console.WriteLine("多边形方向分析:");
Console.WriteLine($" 方向: {(isCounterClockwise ? "逆时针" : "顺时针")}");
Console.WriteLine($" 面积: {area / (SCALE * SCALE):F2}");
Console.WriteLine($" 面积符号: {(area > 0 ? "正" : "负")}");
// 确保外轮廓为逆时针
if (!isCounterClockwise)
{
Console.WriteLine("\n调整为逆时针方向...");
polygon.Reverse();
isCounterClockwise = Clipper.Orientation(polygon);
area = Clipper.Area(polygon);
Console.WriteLine($" 新方向: {(isCounterClockwise ? "逆时针" : "顺时针")}");
Console.WriteLine($" 新面积符号: {(area > 0 ? "正" : "负")}");
}
}
}
5.4.2 ReversePath 和 ReversePaths
反转路径方向:
public static void ReversePath(Path path)
public static void ReversePaths(Paths paths)
示例:
class PathReversalExample
{
/// <summary>
/// 确保外轮廓和孔洞的方向正确
/// </summary>
static void NormalizePathsOrientation(Paths paths)
{
foreach (var path in paths)
{
double area = Clipper.Area(path);
bool isOuter = area > 0; // 假设正面积为外轮廓
bool isCounterClockwise = Clipper.Orientation(path);
// 外轮廓应该是逆时针
if (isOuter && !isCounterClockwise)
{
Clipper.ReversePath(path);
}
// 孔洞应该是顺时针
else if (!isOuter && isCounterClockwise)
{
Clipper.ReversePath(path);
}
}
Console.WriteLine("所有路径方向已标准化");
}
}
5.5 点和多边形的关系
5.5.1 PointInPolygon 方法
判断点是否在多边形内部:
public static int PointInPolygon(IntPoint pt, Path path)
返回值:
1:点在内部0:点在外部-1:点在边界上
示例:
class PointInPolygonExample
{
private const double SCALE = 1000.0;
/// <summary>
/// 测试点与多边形的关系
/// </summary>
static void TestPointLocations()
{
// 创建一个正方形
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))
};
// 测试不同位置的点
(double x, double y, string description)[] testPoints = {
(50, 50, "中心点"),
(0, 0, "顶点"),
(50, 0, "边上"),
(150, 50, "外部"),
(0.001, 0.001, "接近顶点")
};
Console.WriteLine("点位置测试:");
foreach (var (x, y, desc) in testPoints)
{
IntPoint pt = new IntPoint((long)(x * SCALE), (long)(y * SCALE));
int result = Clipper.PointInPolygon(pt, square);
string location = result == 1 ? "内部" :
result == -1 ? "边上" : "外部";
Console.WriteLine($" {desc} ({x}, {y}): {location}");
}
}
/// <summary>
/// 过滤多边形内的点
/// </summary>
static List<IntPoint> FilterPointsInPolygon(
List<IntPoint> points,
Path polygon)
{
var insidePoints = new List<IntPoint>();
foreach (var point in points)
{
int result = Clipper.PointInPolygon(point, polygon);
if (result == 1) // 只要内部的点
{
insidePoints.Add(point);
}
}
return insidePoints;
}
}
5.6 面积计算
5.6.1 Area 方法
计算多边形面积(带符号):
public static double Area(Path poly)
返回值:
- 正值:逆时针方向
- 负值:顺时针方向
- 绝对值:实际面积
示例:
class AreaCalculation
{
private const double SCALE = 1000.0;
/// <summary>
/// 计算复杂多边形的净面积
/// </summary>
static double CalculateNetArea(Paths paths)
{
double netArea = 0;
foreach (var path in paths)
{
// 使用带符号的面积,自动处理孔洞
netArea += Clipper.Area(path);
}
return Math.Abs(netArea) / (SCALE * SCALE);
}
/// <summary>
/// 分析多边形的面积组成
/// </summary>
static void AnalyzeAreaComposition(Paths paths)
{
double positiveArea = 0; // 外轮廓面积
double negativeArea = 0; // 孔洞面积
int outerCount = 0;
int holeCount = 0;
foreach (var path in paths)
{
double area = Clipper.Area(path);
if (area > 0)
{
positiveArea += area;
outerCount++;
}
else
{
negativeArea += Math.Abs(area);
holeCount++;
}
}
double netArea = positiveArea - negativeArea;
Console.WriteLine("面积组成分析:");
Console.WriteLine($" 外轮廓数量: {outerCount}");
Console.WriteLine($" 外轮廓总面积: {(positiveArea / (SCALE * SCALE)):F2}");
Console.WriteLine($" 孔洞数量: {holeCount}");
Console.WriteLine($" 孔洞总面积: {(negativeArea / (SCALE * SCALE)):F2}");
Console.WriteLine($" 净面积: {(netArea / (SCALE * SCALE)):F2}");
}
}
5.7 边界框计算
5.7.1 GetBounds 方法
获取多边形的边界框:
public static IntRect GetBounds(Paths paths)
示例:
class BoundsCalculation
{
private const double SCALE = 1000.0;
/// <summary>
/// 计算并显示边界框信息
/// </summary>
static void CalculateBounds(Paths paths)
{
IntRect bounds = Clipper.GetBounds(paths);
double left = bounds.left / SCALE;
double top = bounds.top / SCALE;
double right = bounds.right / SCALE;
double bottom = bounds.bottom / SCALE;
double width = right - left;
double height = bottom - top;
Console.WriteLine("边界框信息:");
Console.WriteLine($" 左: {left:F2}, 上: {top:F2}");
Console.WriteLine($" 右: {right:F2}, 下: {bottom:F2}");
Console.WriteLine($" 宽度: {width:F2}");
Console.WriteLine($" 高度: {height:F2}");
Console.WriteLine($" 中心: ({(left + width / 2):F2}, {(top + height / 2):F2})");
}
/// <summary>
/// 检查两个多边形的边界框是否相交
/// </summary>
static bool BoundsIntersect(Paths paths1, Paths paths2)
{
IntRect bounds1 = Clipper.GetBounds(paths1);
IntRect bounds2 = Clipper.GetBounds(paths2);
// 检查是否有重叠
bool noOverlap = bounds1.right < bounds2.left ||
bounds2.right < bounds1.left ||
bounds1.bottom < bounds2.top ||
bounds2.bottom < bounds1.top;
return !noOverlap;
}
/// <summary>
/// 创建边界框的多边形表示
/// </summary>
static Path BoundsToPolygon(IntRect bounds)
{
return new Path
{
new IntPoint(bounds.left, bounds.top),
new IntPoint(bounds.right, bounds.top),
new IntPoint(bounds.right, bounds.bottom),
new IntPoint(bounds.left, bounds.bottom)
};
}
}
5.8 Z 轴值处理
5.8.1 Z 轴回调机制
Clipper1 可以处理带 Z 轴值的点,通过回调函数:
public delegate void ZFillCallback(
IntPoint e1bot, IntPoint e1top,
IntPoint e2bot, IntPoint e2top,
ref IntPoint pt
);
应用场景:
- 3D 数据的 2D 投影处理
- 高程数据处理
- 时间序列数据
示例:
class ZAxisHandling
{
private const double SCALE = 1000.0;
/// <summary>
/// Z 值插值回调
/// </summary>
static void ZFillCallbackInterpolate(
IntPoint e1bot, IntPoint e1top,
IntPoint e2bot, IntPoint e2top,
ref IntPoint pt)
{
// 简单的线性插值
// 实际应用中可以根据需求实现更复杂的逻辑
// 如果一条边有 Z 值,使用它
if (e1bot.Z != 0 || e1top.Z != 0)
{
// 在边上进行插值
long dz = e1top.Z - e1bot.Z;
long dx = e1top.X - e1bot.X;
long dy = e1top.Y - e1bot.Y;
long len = (long)Math.Sqrt(dx * dx + dy * dy);
if (len > 0)
{
long ptlen = (long)Math.Sqrt(
(pt.X - e1bot.X) * (pt.X - e1bot.X) +
(pt.Y - e1bot.Y) * (pt.Y - e1bot.Y)
);
pt.Z = e1bot.Z + dz * ptlen / len;
}
else
{
pt.Z = e1bot.Z;
}
}
else if (e2bot.Z != 0 || e2top.Z != 0)
{
// 使用第二条边的 Z 值
long dz = e2top.Z - e2bot.Z;
long dx = e2top.X - e2bot.X;
long dy = e2top.Y - e2bot.Y;
long len = (long)Math.Sqrt(dx * dx + dy * dy);
if (len > 0)
{
long ptlen = (long)Math.Sqrt(
(pt.X - e2bot.X) * (pt.X - e2bot.X) +
(pt.Y - e2bot.Y) * (pt.Y - e2bot.Y)
);
pt.Z = e2bot.Z + dz * ptlen / len;
}
else
{
pt.Z = e2bot.Z;
}
}
}
/// <summary>
/// 使用 Z 轴回调进行裁剪
/// </summary>
static void ClipWithZValues()
{
// 创建带 Z 值的路径
Path path1 = new Path
{
new IntPoint(0, 0, 100),
new IntPoint((long)(100 * SCALE), 0, 200),
new IntPoint((long)(100 * SCALE), (long)(100 * SCALE), 300),
new IntPoint(0, (long)(100 * SCALE), 400)
};
Path path2 = new Path
{
new IntPoint((long)(50 * SCALE), (long)(50 * SCALE), 150),
new IntPoint((long)(150 * SCALE), (long)(50 * SCALE), 250),
new IntPoint((long)(150 * SCALE), (long)(150 * SCALE), 350),
new IntPoint((long)(50 * SCALE), (long)(150 * SCALE), 450)
};
Clipper clipper = new Clipper();
clipper.ZFillFunction = ZFillCallbackInterpolate;
clipper.AddPath(path1, PolyType.ptSubject, true);
clipper.AddPath(path2, PolyType.ptClip, true);
Paths solution = new Paths();
clipper.Execute(ClipType.ctIntersection, solution);
Console.WriteLine("带 Z 值的裁剪结果:");
if (solution.Count > 0)
{
Console.WriteLine($" 顶点数: {solution[0].Count}");
Console.WriteLine(" 顶点 Z 值:");
foreach (var pt in solution[0])
{
Console.WriteLine($" ({pt.X / SCALE:F2}, {pt.Y / SCALE:F2}, Z={pt.Z})");
}
}
}
}
5.9 StrictlySimple 选项
5.9.1 StrictlySimple 属性
确保输出的多边形是"严格简单"的(无自相交,无重叠边):
clipper.StrictlySimple = true;
示例:
class StrictlySimpleExample
{
private const double SCALE = 1000.0;
/// <summary>
/// 对比启用和不启用 StrictlySimple
/// </summary>
static void CompareStrictlySimple()
{
// 创建可能产生复杂结果的输入
Path path1 = CreateComplexShape(0);
Path path2 = CreateComplexShape(30);
// 不启用 StrictlySimple
Clipper clipper1 = new Clipper();
clipper1.StrictlySimple = false;
clipper1.AddPath(path1, PolyType.ptSubject, true);
clipper1.AddPath(path2, PolyType.ptClip, true);
Paths solution1 = new Paths();
clipper1.Execute(ClipType.ctIntersection, solution1);
// 启用 StrictlySimple
Clipper clipper2 = new Clipper();
clipper2.StrictlySimple = true;
clipper2.AddPath(path1, PolyType.ptSubject, true);
clipper2.AddPath(path2, PolyType.ptClip, true);
Paths solution2 = new Paths();
clipper2.Execute(ClipType.ctIntersection, solution2);
Console.WriteLine("StrictlySimple 对比:");
Console.WriteLine($"\n不启用:");
Console.WriteLine($" 多边形数: {solution1.Count}");
Console.WriteLine($" 总顶点数: {solution1.Sum(p => p.Count)}");
Console.WriteLine($"\n启用:");
Console.WriteLine($" 多边形数: {solution2.Count}");
Console.WriteLine($" 总顶点数: {solution2.Sum(p => p.Count)}");
}
static Path CreateComplexShape(double offset)
{
Path shape = new Path();
for (int i = 0; i < 10; i++)
{
double angle = 2 * Math.PI * i / 10;
double radius = 50 + 20 * Math.Sin(5 * angle);
double x = offset + 50 + radius * Math.Cos(angle);
double y = offset + 50 + radius * Math.Sin(angle);
shape.Add(new IntPoint((long)(x * SCALE), (long)(y * SCALE)));
}
return shape;
}
}
5.10 实用工具类
5.10.1 综合工具类
class ClipperUtilities
{
private const double SCALE = 1000.0;
/// <summary>
/// 验证多边形有效性
/// </summary>
public static (bool isValid, string message) ValidatePolygon(Path polygon)
{
if (polygon == null)
return (false, "多边形为 null");
if (polygon.Count < 3)
return (false, $"顶点数不足,需要至少3个,当前{polygon.Count}个");
// 检查面积
double area = Math.Abs(Clipper.Area(polygon));
if (area < 0.001)
return (false, "面积几乎为零");
// 检查重复点
for (int i = 0; i < polygon.Count; i++)
{
int next = (i + 1) % polygon.Count;
if (polygon[i].X == polygon[next].X && polygon[i].Y == polygon[next].Y)
{
return (false, $"存在重复点,索引 {i} 和 {next}");
}
}
return (true, "多边形有效");
}
/// <summary>
/// 标准化多边形集合
/// </summary>
public static Paths NormalizePaths(Paths input)
{
// 简化
Paths simplified = Clipper.SimplifyPolygons(input);
// 清理
Clipper.CleanPolygons(simplified);
// 调整方向
foreach (var path in simplified)
{
double area = Clipper.Area(path);
bool isOuter = area > 0;
bool isCounterClockwise = Clipper.Orientation(path);
if (isOuter && !isCounterClockwise)
{
path.Reverse();
}
else if (!isOuter && isCounterClockwise)
{
path.Reverse();
}
}
return simplified;
}
/// <summary>
/// 计算多边形的质心
/// </summary>
public static IntPoint CalculateCentroid(Path polygon)
{
if (polygon.Count == 0)
return new IntPoint(0, 0);
long sumX = 0, sumY = 0;
foreach (var pt in polygon)
{
sumX += pt.X;
sumY += pt.Y;
}
return new IntPoint(
sumX / polygon.Count,
sumY / polygon.Count
);
}
/// <summary>
/// 缩放路径
/// </summary>
public static Path ScalePath(Path input, double scaleFactor)
{
IntPoint centroid = CalculateCentroid(input);
Path scaled = new Path();
foreach (var pt in input)
{
long dx = pt.X - centroid.X;
long dy = pt.Y - centroid.Y;
scaled.Add(new IntPoint(
centroid.X + (long)(dx * scaleFactor),
centroid.Y + (long)(dy * scaleFactor)
));
}
return scaled;
}
/// <summary>
/// 平移路径
/// </summary>
public static Path TranslatePath(Path input, long dx, long dy)
{
Path translated = new Path();
foreach (var pt in input)
{
translated.Add(new IntPoint(pt.X + dx, pt.Y + dy));
}
return translated;
}
/// <summary>
/// 旋转路径
/// </summary>
public static Path RotatePath(Path input, double angleRadians)
{
IntPoint centroid = CalculateCentroid(input);
Path rotated = new Path();
double cos = Math.Cos(angleRadians);
double sin = Math.Sin(angleRadians);
foreach (var pt in input)
{
long dx = pt.X - centroid.X;
long dy = pt.Y - centroid.Y;
rotated.Add(new IntPoint(
centroid.X + (long)(dx * cos - dy * sin),
centroid.Y + (long)(dx * sin + dy * cos)
));
}
return rotated;
}
}
5.11 本章小结
在本章中,我们深入学习了 Clipper1 的填充规则和高级特性:
- 填充规则:EvenOdd、NonZero、Positive、Negative 的原理和应用
- 多边形简化:SimplifyPolygon、CleanPolygon 等方法
- 方向性处理:Orientation、ReversePath 等
- 点与多边形关系:PointInPolygon 方法
- 面积和边界框:Area、GetBounds 方法
- Z 轴处理:回调机制
- StrictlySimple:严格简单多边形选项
- 实用工具:验证、标准化、变换等
重点掌握:
- 根据应用场景选择合适的填充规则
- 使用简化和清理方法优化多边形
- 正确处理多边形的方向性
- 灵活使用各种辅助方法
在下一章中,我们将通过完整的实际应用案例,综合运用前面学到的所有知识,并讨论最佳实践和性能优化技巧。
5.12 练习题
-
填充规则:创建自相交多边形,使用不同填充规则观察结果差异
-
多边形简化:实现一个函数,自动简化和清理用户输入的多边形
-
方向检测:编写程序自动检测并修正多边形方向
-
点位置判断:实现一个区域查询系统,快速判断大量点的位置
-
Z 轴处理:实现一个地形裁剪系统,保持高程信息
-
综合应用:创建一个多边形编辑器,支持验证、简化、变换等操作

浙公网安备 33010602011771号