using System; using System.Collections.Generic; using OpenNest.Math; namespace OpenNest.Geometry { /// /// A part's material as world-space polygons for part-spacing checks: the perimeter and its /// cutouts, optionally grown by a clearance (perimeter offset outward, cutouts inward, in one /// Clipper region offset). Auto Nest's layout check and the pre-post review both judge /// spacing with this outline and , so they cannot disagree. /// public sealed class SpacingOutline { /// Arc chord tolerance of the outline. Perimeter arcs are circumscribed and /// cutout arcs inscribed, so flattening never hides material. public const double ChordTolerance = 0.001; /// How far under the part spacing a gap may fall and still pass. Layouts placed /// exactly at the spacing land up to ~1e-4 short once rotated, rounded coordinates /// (e.g. PEP's 4-decimal exports) meet the Clipper grid; 0.0005 covers that with margin /// and is far below anything a cutting machine can resolve. public const double SpacingSlack = 0.0005; private bool prepared; private TriangulatedRegion triangles; public SpacingOutline(Polygon perimeter, List holes) { Perimeter = perimeter ?? throw new ArgumentNullException(nameof(perimeter)); Holes = holes ?? new List(); } public Polygon Perimeter { get; } public List Holes { get; } /// Prepared on the first overlap test; null preparation is cached too. public TriangulatedRegion Triangles { get { if (!prepared) { triangles = TriangulatedRegion.Build(Perimeter, Holes); prepared = true; } return triangles; } } /// Inflation that enforces less . /// Inflating one part of a pair by it and testing against the other's raw outline covers /// both sides: a part inside the other's (shrunk) cutout, or the inflated outline inside /// the other's raw cutout. public static double InflationFor(double spacing) => System.Math.Max(0, spacing - SpacingSlack); /// /// Outlines (already rotated) at , /// grown by . A cutout that closes up under the offset is /// dropped, which treats it as solid: conservative, since it has no room for another part /// at the required spacing anyway. Null when the offset leaves no perimeter. /// public static SpacingOutline Build(ShapeProfile profile, Vector location, double inflateBy) { ArgumentNullException.ThrowIfNull(profile); // Adaptive tolerance instead of Shape.ToPolygon()'s default (up to 1000 segments per // arc): arc-heavy real parts otherwise produce thousands of vertices. var region = ClipperBridge.OffsetForValidation( profile, inflateBy > Tolerance.Epsilon ? inflateBy : 0, ChordTolerance ); var perimeter = region.LargestOuter(); if (perimeter == null) return null; ToWorld(perimeter, location); foreach (var hole in region.Holes) ToWorld(hole, location); return new SpacingOutline(perimeter, region.Holes); } /// True when the two outlines share material (holes subtracted). public bool Overlaps(SpacingOutline other) { ArgumentNullException.ThrowIfNull(other); var at = Triangles; var bt = other.Triangles; // Keeping translations at zero preserves the reference's floating-point operation order. return (at != null && bt != null ? at.Overlaps(bt, 0, 0, 0, 0) : null) ?? Collision.HasOverlap(Perimeter, other.Perimeter, Holes, other.Holes); } private static void ToWorld(Polygon polygon, Vector location) { polygon.Offset(location); polygon.UpdateBounds(); } } }