using System.Collections.Generic; using Clipper2Lib; namespace OpenNest.Geometry { /// /// Region offsetting through Clipper2, for CPU-side preparation only: work done /// once per drawing, rotation or spacing whose output is cached and fed to hot /// loops. Per-pair tests () stay hand-rolled so they can /// be ported to a GPU kernel. /// public static class ClipperBridge { /// /// Decimal places Clipper keeps (1e-4 in either inches or mm). /// public const int Precision = 4; private const double MiterLimit = 2.0; /// /// Converts a polygon to a Clipper path, dropping the closing vertex and /// orienting it positive (CCW) or negative (CW). /// public static PathD ToPath(Polygon polygon, bool positive) { var path = ToPath(polygon, new Vector()); if (path.Count >= 3 && Clipper.IsPositive(path) != positive) path.Reverse(); return path; } /// /// Converts a polygon to a Clipper path with an optional offset, dropping the /// closing vertex and keeping the polygon's own winding. /// public static PathD ToPath(Polygon polygon, Vector offset) { var verts = polygon.Vertices; var n = verts.Count; if (n > 1 && verts[0].X == verts[n - 1].X && verts[0].Y == verts[n - 1].Y) n--; var path = new PathD(n); for (var i = 0; i < n; i++) path.Add(new PointD(verts[i].X + offset.X, verts[i].Y + offset.Y)); return path; } /// /// Converts a Clipper path to a closed polygon with updated bounds. /// public static Polygon ToPolygon(PathD path) { var polygon = new Polygon(); foreach (var pt in path) polygon.Vertices.Add(new Vector(pt.x, pt.y)); polygon.Close(); polygon.UpdateBounds(); return polygon; } /// /// Flattens a profile into a Clipper region: perimeter positive, cutouts negative. /// public static PathsD ToRegion(ShapeProfile profile, double tolerance, bool circumscribe) { var region = new PathsD(profile.Cutouts.Count + 1); AddShape(region, profile.Perimeter, tolerance, circumscribe, positive: true); // A cutout is flattened the opposite way: circumscribing it would shrink the // material around it, so inscribe instead to keep the region conservative. foreach (var cutout in profile.Cutouts) AddShape(region, cutout, tolerance, !circumscribe, positive: false); return region; } /// /// Offsets a part region outward by : the perimeter /// grows and the cutouts shrink. Features narrower than twice the distance /// collapse, and cutouts that close up disappear. Joins are round, with chords /// no more than from the true arc. /// /// /// When true, the result never under-estimates the offset: arcs are flattened /// outside the true curve and the inflation is padded by the chord tolerance /// and Clipper's rounding. /// public static OffsetRegion Offset( ShapeProfile profile, double distance, double tolerance, bool circumscribe = false ) { var region = ToRegion(profile, tolerance, circumscribe); return Offset(region, distance, tolerance, circumscribe); } /// /// Offsets an already-flattened region (outers positive, holes negative). /// public static OffsetRegion Offset( PathsD region, double distance, double tolerance, bool circumscribe = false ) { var delta = distance; if (circumscribe) delta += tolerance + 0.5 * System.Math.Pow(10, -Precision); var inflated = delta == 0 ? Union(region) : Clipper.InflatePaths( region, delta, JoinType.Round, EndType.Polygon, MiterLimit, Precision, tolerance ); var result = new OffsetRegion(new List(), new List()); foreach (var path in inflated) { if (path.Count < 3) continue; if (Clipper.IsPositive(path)) result.Outers.Add(ToPolygon(path)); else result.Holes.Add(ToPolygon(path)); } return result; } private static PathsD Union(PathsD region) { var clipper = new ClipperD(Precision); clipper.AddSubject(region); var solution = new PathsD(); clipper.Execute(ClipType.Union, FillRule.NonZero, solution); return solution; } private static void AddShape( PathsD region, Shape shape, double tolerance, bool circumscribe, bool positive ) { var polygon = shape.ToPolygonWithTolerance(tolerance, circumscribe); if (polygon.Vertices.Count < 4) return; var path = ToPath(polygon, positive); if (path.Count >= 3) region.Add(path); } } /// /// Result of : /// outer boundaries (CCW) and holes (CW), as closed polygons. /// public sealed record OffsetRegion(List Outers, List Holes) { /// /// The outer boundary with the largest area, or null when the region is empty. /// public Polygon LargestOuter() { Polygon best = null; var bestArea = 0.0; foreach (var outer in Outers) { var area = outer.Area(); if (best == null || area > bestArea) { best = outer; bestArea = area; } } return best; } } }