#nullable enable
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Clipper2Lib;
using OpenNest.Geometry;
namespace OpenNest.Engine.NestingEngines.Irregular;
///
/// A closed outline the packer fills in place of a rectangular work area: a frame's cutout in
/// frame coordinates, with the parts already seated in it as obstacles. Each orientation's
/// starting free region is its inner-fit polygon: the reference points at which its clearance
/// footprint lies inside the outline eroded by the other half of the clearance, so a part
/// keeps the full clearance from the frame exactly as it does from another part. The inner
/// fit is computed once per orientation and shared by every packing of this boundary.
///
/// Bound to one : its footprints define the inner fits and the
/// obstacle no-fit polygons, so a packer filling this boundary must use that cache.
internal sealed class PackBoundary
{
/// Extra erosion beyond the half clearance, covering the outline's flattening and
/// the round-offset chords at its reflex corners.
private const double Margin = 0.002;
private const double ArcTolerance = 0.001;
private readonly PathsD eroded;
private readonly Orientation[] obstacles;
private readonly ConcurrentDictionary<(int, int), PathsD> innerFits = new();
/// Closed polygon inscribed in the true outline, so flattening never
/// adds room.
/// Polygons circumscribing material already inside the outline, with
/// the chord deviation each was flattened with; every obstacle gets the full clearance.
/// The cache whose footprints this boundary is fitted to.
public PackBoundary(PathD outline, IReadOnlyList<(PathD Outline, double Tolerance)> obstacles, NoFitCache nfps)
{
Nfps = nfps;
var box = Clipper.GetBounds(outline);
Bounds = new Box(box.left, box.top, box.Width, box.Height); // Clipper RectD: top is the minimum Y.
eroded = Clipper.InflatePaths(new PathsD { Positive(outline) }, -(nfps.Clearance / 2 + Margin),
JoinType.Round, EndType.Polygon, 2.0, NoFitCache.Precision, ArcTolerance);
eroded.RemoveAll(p => p.Count < 3);
this.obstacles = new Orientation[obstacles.Count];
for (var i = 0; i < obstacles.Count; i++)
{
var path = Positive(obstacles[i].Outline);
var tolerance = obstacles[i].Tolerance;
var bounds = Clipper.GetBounds(path);
this.obstacles[i] = new Orientation
{
// Real part types are nonnegative, so obstacle footprints never alias them.
TypeIndex = -1,
Index = i,
Rotation = 0,
Outline = path,
Tolerance = tolerance,
MinX = bounds.left - tolerance,
MinY = bounds.top - tolerance,
MaxX = bounds.right + tolerance,
MaxY = bounds.bottom + tolerance,
};
}
}
///
/// A sheet's material as the boundary: is its outline less its
/// obstacles (outer paths positive, holes negative) in the placement frame, and parts keep
/// from every edge and obstacle, as they keep the edge
/// spacing from a rectangular sheet's edges. The outline is eroded by the edge clearance
/// less the half clearance the footprints already carry (grown when that is negative), plus
/// the margin; is the packer's work area and front origin.
///
public PackBoundary(PathsD material, double edgeClearance, Box bounds, NoFitCache nfps)
{
Nfps = nfps;
Bounds = bounds;
var inset = edgeClearance - nfps.Clearance / 2 + Margin;
eroded = Clipper.InflatePaths(material, -inset, JoinType.Round, EndType.Polygon, 2.0,
NoFitCache.Precision, ArcTolerance);
eroded.RemoveAll(p => p.Count < 3);
obstacles = System.Array.Empty();
}
public NoFitCache Nfps { get; }
/// Bounds of the outline: the packer's work area and front origin.
public Box Bounds { get; }
public int ObstacleCount => obstacles.Length;
public Orientation Obstacle(int index) => obstacles[index];
/// Coarse filter against the outline's bounds; the inner fit decides.
public bool Fits(double width, double height) =>
width <= Bounds.Length + 1e-9 && height <= Bounds.Width + 1e-9;
/// A fresh copy of the orientation's starting free region.
public PathsD InnerFit(Orientation orientation) =>
new(innerFits.GetOrAdd((orientation.TypeIndex, orientation.Index),
_ => InnerFit(Nfps.Footprint(orientation), eroded)));
///
/// Reference points t at which lies inside one component of
/// : its anchor vertex is inside the component (t in component - anchor)
/// and its outline never meets the component's boundary (t outside boundary + reflected
/// outline, including the pose that contains the whole component).
///
internal static PathsD InnerFit(PathD grown, PathsD hole)
{
if (grown.Count < 3)
return new PathsD();
var reflected = new PathD(grown.Select(p => new PointD(-p.x, -p.y)));
var anchor = grown[0];
var inside = new PathsD();
PathsD? forbidden = null;
foreach (var component in hole)
{
if (component.Count < 3)
continue;
inside.Add(Clipper.TranslatePath(component, -anchor.x, -anchor.y));
// Clipper.MinkowskiSum rounds to two decimals; pass the job precision explicitly.
var sweep = Clipper.Union(Minkowski.Sum(reflected, component, true, NoFitCache.Precision),
new PathsD { Clipper.TranslatePath(reflected, component[0].x, component[0].y) },
FillRule.NonZero, NoFitCache.Precision);
forbidden = forbidden == null ? sweep : Clipper.Union(forbidden, sweep, FillRule.NonZero, NoFitCache.Precision);
}
return forbidden == null
? new PathsD()
: Clipper.Difference(inside, forbidden, FillRule.NonZero, NoFitCache.Precision);
}
private static PathD Positive(PathD path)
{
if (path.Count >= 3 && !Clipper.IsPositive(path))
{
path = new PathD(path);
path.Reverse();
}
return path;
}
}