using System; using System.Collections.Generic; using Clipper2Lib; using OpenNest.Geometry; namespace OpenNest.Diagnostics; /// /// Shared material of two hole-aware outlines as connected islands, through Clipper2. /// Callers pass part-local coordinates (rebased near the origin) and add the origin back. /// /// Coordinates are rounded to a 1e-8 model-unit grid, far below the 0.001 chord tolerance /// and the area threshold. Each operand is /// its outline minus the union of its holes (outline positive, holes negative, /// ). /// Nonfinite or out-of-range coordinates throw , which the /// analyzer turns into an incomplete-check issue rather than a clear result. /// /// internal static class OverlapClipping { private const double Scale = 1e8; // Scaled coordinates stay several orders of magnitude inside Clipper2's int64 range. private const double MaxCoordinate = 1e9; /// One connected island: a counterclockwise outline and clockwise holes, both closed. internal sealed record Island(List Outline, List> Holes, PolygonAreaMoments Moments); internal static List Intersect(Polygon outerA, IReadOnlyList holesA, Polygon outerB, IReadOnlyList holesB) { var clipper = new Clipper64(); clipper.AddSubject(Region(outerA, holesA)); clipper.AddClip(Region(outerB, holesB)); var tree = new PolyTree64(); if (!clipper.Execute(ClipType.Intersection, FillRule.Positive, tree)) throw new InvalidOperationException("Clipper2 could not intersect the overlap material."); var islands = new List(); Collect(tree, islands); return islands; } private static void Collect(PolyPath64 node, List islands) { for (var i = 0; i < node.Count; i++) { var outer = (PolyPath64)node[i]; var outline = ToVectors(outer.Polygon, positive: true); var holes = new List>(); for (var h = 0; h < outer.Count; h++) { var hole = (PolyPath64)outer[h]; holes.Add(ToVectors(hole.Polygon, positive: false)); // An island can sit inside a hole formed by both operands' holes together. Collect(hole, islands); } if (TryMoments(outline, holes, out var moments)) islands.Add(new Island(outline, holes, moments)); else if (Clipper.Area(outer.Polygon) / (Scale * Scale) >= OpenNest.Math.Tolerance.Epsilon) throw new ArithmeticException("Overlap island area moments are invalid."); // Otherwise a degenerate dust island below the reporting threshold is skipped. } } /// /// Net area and centroid of an outline minus its holes. Fails for degenerate, /// nonfinite or nonpositive results. /// private static bool TryMoments(List outline, List> holes, out PolygonAreaMoments moments) { moments = default; if (!PolygonAreaMoments.TryCompute(outline, out var outer)) return false; var removed = new List(holes.Count); foreach (var hole in holes) { if (!PolygonAreaMoments.TryCompute(hole, out var hollow)) return false; removed.Add(hollow); } return PolygonAreaMoments.TrySubtract(outer, removed, out moments); } private static Paths64 Region(Polygon outer, IReadOnlyList holes) { var region = new Paths64 { ToPath(outer, positive: true) }; if (holes != null) foreach (var hole in holes) region.Add(ToPath(hole, positive: false)); return region; } private static Path64 ToPath(Polygon polygon, bool positive) { var vertices = polygon.Vertices; var count = vertices.Count; if (count > 1 && vertices[count - 1].X == vertices[0].X && vertices[count - 1].Y == vertices[0].Y) count--; var path = new Path64(count); for (var i = 0; i < count; i++) { var point = vertices[i]; if (!double.IsFinite(point.X) || !double.IsFinite(point.Y) || System.Math.Abs(point.X) > MaxCoordinate || System.Math.Abs(point.Y) > MaxCoordinate) throw new ArithmeticException("Overlap material is outside the exact clipping range."); path.Add(new Point64(System.Math.Round(point.X * Scale), System.Math.Round(point.Y * Scale))); } if (Clipper.IsPositive(path) != positive) path.Reverse(); return path; } private static List ToVectors(Path64 path, bool positive) { var points = new List(path.Count + 1); foreach (var point in path) points.Add(new Vector(point.X / Scale, point.Y / Scale)); if (Clipper.IsPositive(path) != positive) points.Reverse(); points.Add(points[0]); // Regions repeat their closing vertex exactly. return points; } }