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();
}
}
}