第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 的填充规则和高级特性:

  1. 填充规则:EvenOdd、NonZero、Positive、Negative 的原理和应用
  2. 多边形简化:SimplifyPolygon、CleanPolygon 等方法
  3. 方向性处理:Orientation、ReversePath 等
  4. 点与多边形关系:PointInPolygon 方法
  5. 面积和边界框:Area、GetBounds 方法
  6. Z 轴处理:回调机制
  7. StrictlySimple:严格简单多边形选项
  8. 实用工具:验证、标准化、变换等

重点掌握:

  • 根据应用场景选择合适的填充规则
  • 使用简化和清理方法优化多边形
  • 正确处理多边形的方向性
  • 灵活使用各种辅助方法

在下一章中,我们将通过完整的实际应用案例,综合运用前面学到的所有知识,并讨论最佳实践和性能优化技巧。

5.12 练习题

  1. 填充规则:创建自相交多边形,使用不同填充规则观察结果差异

  2. 多边形简化:实现一个函数,自动简化和清理用户输入的多边形

  3. 方向检测:编写程序自动检测并修正多边形方向

  4. 点位置判断:实现一个区域查询系统,快速判断大量点的位置

  5. Z 轴处理:实现一个地形裁剪系统,保持高程信息

  6. 综合应用:创建一个多边形编辑器,支持验证、简化、变换等操作

posted @ 2025-12-18 09:26  我才是银古  阅读(17)  评论(0)    收藏  举报