using Clipper2Lib;
using OpenNest.Engine.Jobs;
using OpenNest.Geometry;
namespace OpenNest.Engine.Opus55;
///
/// One allowed pose of a part type: its rotation, its polygonized outline at that rotation
/// (reference point = snapshot origin), and the outline's conservative bounds.
///
internal sealed class Orientation
{
public required int TypeIndex { get; init; }
public required int Index { get; init; }
public required double Rotation { get; init; }
/// CCW outline whose every point lies within of the true perimeter.
public required PathD Outline { get; init; }
/// Chord deviation used for arcs; footprints are grown by it to stay conservative.
public required double Tolerance { get; init; }
/// Outline bounds grown by the tolerance, so they contain the true perimeter.
public required double MinX { get; init; }
public required double MinY { get; init; }
public required double MaxX { get; init; }
public required double MaxY { get; init; }
public double Width => MaxX - MinX;
public double Height => MaxY - MinY;
}
internal sealed class PartType
{
public required int Index { get; init; }
public required NestJobPart Part { get; init; }
public required double Area { get; init; }
public required IReadOnlyList Orientations { get; init; }
}
///
/// Converts job snapshots into the polygon world the packer works in. Parts whose geometry
/// cannot be read are kept with no orientations, so they surface as unplaced instead of
/// failing the whole job.
///
internal static class PartCatalog
{
/// Finest chord deviation of the working outline from true arcs, in job units.
public const double ChordTolerance = 0.002;
/// Outline vertex count above which arcs are polygonized more coarsely (NFP cost is ~n*m).
private const int TargetVertices = 64;
/// Hard cap on distinct orientations evaluated per part type.
private const int MaxOrientations = 8;
public static IReadOnlyList Build(NestJob job)
{
// Fewer orientations per type for jobs with many distinct parts; every (type, rotation)
// pair costs a feasible-region update per placement.
var perType = System.Math.Clamp(48 / System.Math.Max(1, job.Parts.Count), 2, MaxOrientations);
var types = new List(job.Parts.Count);
for (var index = 0; index < job.Parts.Count; index++)
{
var part = job.Parts[index];
Shape? perimeter;
try
{
perimeter = ReadPerimeter(part.Geometry);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or InvalidOperationException)
{
perimeter = null;
}
if (perimeter == null)
{
types.Add(new PartType { Index = index, Part = part, Area = 0, Orientations = [] });
continue;
}
var angles = RotationCandidates.DistinctOutlines(perimeter,
CandidateAngles(part.Rotation, perimeter, perType));
var tolerance = ChooseTolerance(perimeter);
var orientations = new List();
foreach (var angle in angles)
{
var outline = Polygonize(perimeter, angle, tolerance);
if (outline.Count < 3)
continue;
orientations.Add(MakeOrientation(index, orientations.Count, angle, outline, tolerance));
}
var area = orientations.Count == 0 ? 0 : System.Math.Abs(Clipper.Area(orientations[0].Outline));
types.Add(new PartType { Index = index, Part = part, Area = area, Orientations = orientations });
}
return types;
}
private static Shape? ReadPerimeter(PartGeometrySnapshot geometry) =>
JobPartGeometry.TryRead(geometry)?.Perimeter;
///
/// Coarsens arc polygonization (up to 0.1% of the part size) until the outline is small
/// enough for cheap Minkowski sums. Lines are always exact, so only arc-heavy parts pay.
///
private static double ChooseTolerance(Shape perimeter)
{
var box = perimeter.BoundingBox;
var cap = System.Math.Max(ChordTolerance, 0.001 * System.Math.Max(box.Width, box.Length));
var tolerance = ChordTolerance;
while (tolerance * 2 <= cap && perimeter.ToPolygonWithTolerance(tolerance).Vertices.Count > TargetVertices)
tolerance *= 2;
return tolerance;
}
private static PathD Polygonize(Shape perimeter, double angle, double tolerance)
{
var shape = (Shape)perimeter.Clone();
if (angle != 0)
shape.Rotate(angle);
var polygon = shape.ToPolygonWithTolerance(tolerance);
var path = new PathD(polygon.Vertices.Count);
foreach (var v in polygon.Vertices)
{
if (path.Count > 0 && System.Math.Abs(path[^1].x - v.X) < 1e-9 && System.Math.Abs(path[^1].y - v.Y) < 1e-9)
continue;
path.Add(new PointD(v.X, v.Y));
}
if (path.Count > 1 && System.Math.Abs(path[0].x - path[^1].x) < 1e-9 && System.Math.Abs(path[0].y - path[^1].y) < 1e-9)
path.RemoveAt(path.Count - 1);
if (!Clipper.IsPositive(path))
path.Reverse();
return path;
}
private static Orientation MakeOrientation(int typeIndex, int index, double angle, PathD outline, double tolerance)
{
var bounds = Clipper.GetBounds(outline);
return new Orientation
{
TypeIndex = typeIndex,
Index = index,
Rotation = angle,
Outline = outline,
Tolerance = tolerance,
MinX = bounds.left - tolerance,
MinY = bounds.top - tolerance, // Clipper RectD: top is the minimum Y.
MaxX = bounds.right + tolerance,
MaxY = bounds.bottom + tolerance,
};
}
internal static List CandidateAngles(RotationPolicy policy, Shape perimeter, int limit) =>
RotationCandidates.ForShape(policy, perimeter, limit).ToList();
}