using System; using System.Collections.Generic; using OpenNest.Engine.Jobs; using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Engine.Qwen38FlashNext.Engine; using Math = System.Math; /// /// A job requirement prepared once per solve: snapshot motions rebuilt into an owned /// closed contour topology (perimeter + cutouts; rapid/layer-mark geometry dropped), /// flattened collision polygons, and material area. /// internal sealed class PartModel { private PartModel( string id, int quantity, int priority, RotationPolicy rotation, ShapeProfile profile, Shape perimeterShape, List cutoutShapes, double area ) { Id = id; Quantity = quantity; Priority = priority; Rotation = rotation; Profile = profile; PerimeterShape = perimeterShape; CutoutShapes = cutoutShapes; Area = area; } public string Id { get; } public int Quantity { get; } public int Priority { get; } public RotationPolicy Rotation { get; } /// Closed contour topology (perimeter CCW, cutouts) used for region offsets. public ShapeProfile Profile { get; } /// Analytic closed perimeter (arcs preserved) for conservative flattening. public Shape PerimeterShape { get; } public List CutoutShapes { get; } /// Material area (perimeter minus holes), from the analytic shapes. public double Area { get; } internal List? Angles { get; set; } /// /// Chord tolerance for the engine's internal collision polygons. Must stay FINER /// than NestTolerances.ValidationOutline (0.001): any chord cuts the cap off a /// concave arc, and a coarser polygon cuts MORE - so a coarse flattening is a /// subset of the validator's material in notched regions and admits real spacing /// violations (observed on arc-heavy PEP parts at 0.02). Finer than the validator, /// every engine polygon contains the validator's, so a cleared gate is conservative. /// public const double CollisionTolerance = 0.0005; /// /// Returns null when the snapshot has no usable closed contour - such a part can /// never be placed and is reported unplaced rather than failing the whole job. /// public static PartModel? TryCreate(NestJobPart part) { var geometry = JobPartGeometry.TryRead(part.Geometry); if (geometry == null || geometry.MaterialArea <= Tolerance.Epsilon) return null; // Read normalizes winding before the collision and offset preparation below. return new PartModel(part.Id, part.Quantity, part.Priority, part.Rotation, geometry.Profile, geometry.Perimeter, geometry.Cutouts.ToList(), geometry.MaterialArea); } } /// /// One part contour rotated about the snapshot origin - exactly the frame a /// produces (rotate, then translate by X/Y). Bounds, /// convex hull, and the spacing-inflated outline are computed once and reused. /// internal sealed class OrientationModel { internal OrientationModel( double angle, Polygon perimeter, List holes, Polygon? inflatedPerimeter, List inflatedHoles, double spacing ) { Angle = angle; Perimeter = perimeter; Holes = holes; InflatedPerimeter = inflatedPerimeter; InflatedHoles = inflatedHoles; Spacing = spacing; var minX = double.MaxValue; var minY = double.MaxValue; var maxX = double.MinValue; var maxY = double.MinValue; foreach (var v in perimeter.Vertices) { if (v.X < minX) minX = v.X; if (v.X > maxX) maxX = v.X; if (v.Y < minY) minY = v.Y; if (v.Y > maxY) maxY = v.Y; } MinX = minX; MinY = minY; MaxX = maxX; MaxY = maxY; var hullPoints = new List(); try { var hull = ConvexHull.Compute(perimeter.Vertices); foreach (var v in hull.Vertices) { if (hullPoints.Count > 0 && v.Equals(hullPoints[^1])) continue; hullPoints.Add(v); } if (hullPoints.Count > 1 && hullPoints[0].Equals(hullPoints[^1])) hullPoints.RemoveAt(hullPoints.Count - 1); } catch (Exception) { hullPoints.Clear(); } Hull = hullPoints.Count >= 3 ? hullPoints : perimeter.Vertices; // True when the flattened perimeter is itself convex (no concavities) and the // part has no cutouts: for two such parts the convex NFP is EXACT - material // equals hull - so an anchor inside it is forbidden with no material test. var convex = holes.Count == 0; if (convex) { var verts = perimeter.Vertices; var m = verts.Count; if (m > 2 && verts[0].Equals(verts[m - 1])) m--; for (var i = 0; i < m && convex; i++) { var ax = verts[i].X; var ay = verts[i].Y; var bx = verts[(i + 1) % m].X; var by = verts[(i + 1) % m].Y; var cx = verts[(i + 2) % m].X; var cy = verts[(i + 2) % m].Y; if ((bx - ax) * (cy - by) - (by - ay) * (cx - bx) < -1e-9) convex = false; } } IsConvexSolid = convex; } /// No cutouts and a convex perimeter: material equals hull. public bool IsConvexSolid { get; } public double Angle { get; } /// Circumscribed flattened perimeter in the rotated frame (pre-translation). public Polygon Perimeter { get; } public List Holes { get; } /// Material outline inflated by (null when spacing is zero). public Polygon? InflatedPerimeter { get; } /// Cutouts shrunk by ; holes that close up are dropped (treated solid). public List InflatedHoles { get; } public double Spacing { get; } public double MinX { get; } public double MinY { get; } public double MaxX { get; } public double MaxY { get; } public double Width => MaxX - MinX; public double Height => MaxY - MinY; /// Convex hull of the perimeter (open vertex list, at least 3 points). public List Hull { get; } /// /// Fast clearance outline of the raw perimeter in this orientation's local frame /// (lazily built; translated per anchor in O(1) via ). /// public FastPoly? PerimeterFast => _perimeterFast ??= FastPoly.From(Perimeter); private FastPoly? _perimeterFast; /// /// Fast clearance outline of the gate material (spacing-inflated when positive) in /// this orientation's local frame. /// public FastPoly? GateFast => _gateFast ??= FastPoly.From(InflatedPerimeter ?? Perimeter); private FastPoly? _gateFast; /// /// Cached triangulation of the raw material (perimeter + holes) in this /// orientation's local frame for the allocation-free exact gate. /// public TriSet? MaterialTris => _materialTris ??= TriSet.Build(Perimeter, Holes); private TriSet? _materialTris; /// /// Cached triangulation of the gate material (spacing-inflated perimeter with /// shrunk holes) in this orientation's local frame. /// public TriSet? GateTris => _gateTris ??= TriSet.Build(InflatedPerimeter ?? Perimeter, InflatedPerimeter != null ? InflatedHoles : Holes); private TriSet? _gateTris; } /// Builds and caches per-(part, orientation, spacing) geometry for one engine run. internal sealed class PartPreparation { private readonly List models = new(); private readonly Dictionary indexById = new(StringComparer.Ordinal); private readonly Dictionary<(string, double, double), OrientationModel> orientations = new(); /// /// Cross-packer memo of exact material overlap: (placed orientation, placed anchor, /// candidate orientation, candidate anchor) -> overlap. Sheet trials rebuild greedy /// placement deterministically, so identical world poses recur across trials and /// across sheets; the memo collapses the repeated polygon-clipping work. Bounded so /// it can never grow unboundedly on pathological jobs. /// private readonly Dictionary overlaps = new(); internal sealed class OverlapKey : IEquatable { private readonly int _placedHash; private readonly long _px; private readonly long _py; private readonly int _candHash; private readonly long _cx; private readonly long _cy; public OverlapKey(int placedHash, double px, double py, int candHash, double cx, double cy) { _placedHash = placedHash; _px = (long)Math.Round(px * 1e6); _py = (long)Math.Round(py * 1e6); _candHash = candHash; _cx = (long)Math.Round(cx * 1e6); _cy = (long)Math.Round(cy * 1e6); } public bool Equals(OverlapKey? other) => other != null && _placedHash == other._placedHash && _px == other._px && _py == other._py && _candHash == other._candHash && _cx == other._cx && _cy == other._cy; public override bool Equals(object? obj) => Equals(obj as OverlapKey); public override int GetHashCode() { var hash = _placedHash; hash = unchecked(hash * 397 + _px.GetHashCode()); hash = unchecked(hash * 397 + _py.GetHashCode()); hash = unchecked(hash * 397 + _candHash); hash = unchecked(hash * 397 + _cx.GetHashCode()); hash = unchecked(hash * 397 + _cy.GetHashCode()); return hash; } } private const int OverlapMemoCap = 500_000; public bool MaterialOverlapMemo( OrientationModel placed, double placedX, double placedY, OrientationModel candidate, double candidateX, double candidateY, Func compute ) { var key = new OverlapKey( System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(placed), placedX, placedY, System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(candidate), candidateX, candidateY ); if (overlaps.TryGetValue(key, out var known)) return known; if (overlaps.Count >= OverlapMemoCap) overlaps.Clear(); var value = compute(); overlaps[key] = value; return value; } public IReadOnlyList Models => models; public PartPreparation(IReadOnlyList parts) { foreach (var part in parts) { var model = PartModel.TryCreate(part); if (model == null) { InvalidIds.Add(part.Id); continue; } indexById[model.Id] = models.Count; models.Add(model); } } /// Requirements whose snapshot geometry could not be interpreted at all. public List InvalidIds { get; } = new(); public bool TryGetModel(string partId, out PartModel model) { model = null!; if (!indexById.TryGetValue(partId, out var index)) return false; model = models[index]; return true; } public OrientationModel Oriented(PartModel model, double angle, double spacing) { // Round keys so policy-equivalent angles (0 vs 2pi) share one cached orientation. var key = (model.Id, Math.Round(angle, 9), Math.Round(spacing, 9)); if (orientations.TryGetValue(key, out var cached)) return cached; var perimeterShape = (Shape)model.PerimeterShape.Clone(); perimeterShape.Rotate(angle); var perimeter = perimeterShape.ToPolygonWithTolerance( PartModel.CollisionTolerance, circumscribe: true ); var holes = new List(model.CutoutShapes.Count); foreach (var cutout in model.CutoutShapes) { var shape = (Shape)cutout.Clone(); shape.Rotate(angle); holes.Add( shape.ToPolygonWithTolerance(PartModel.CollisionTolerance, circumscribe: true) ); } Polygon? inflated = null; var inflatedHoles = new List(); if (spacing > Tolerance.Epsilon) { // Conservative (circumscribed, padded) region offset: a superset of the // validator's inflation, so accepted clearances never fall short. The // offset commutes with rotation, so inflate the unrotated profile once and // rotate the result into this orientation's frame - an unrotated inflation // would test the candidate against the material of a different angle. var region = ClipperBridge.Offset(model.Profile, spacing, 0.02, circumscribe: true); var outer = region.LargestOuter(); if (outer != null) { outer.Rotate(angle); outer.UpdateBounds(); inflated = outer; } foreach (var hole in region.Holes) if (hole != null) { hole.Rotate(angle); hole.UpdateBounds(); inflatedHoles.Add(hole); } } var result = new OrientationModel(angle, perimeter, holes, inflated, inflatedHoles, spacing); orientations[key] = result; return result; } /// /// Legal orientations for a requirement: exactly the policy angles when the policy /// enumerates them, otherwise 0/90/180/270 degrees plus the minimum-area bounding /// rectangle angle (rotating-calipers), with 180-degree equivalents included. /// public static List CandidateAngles(PartModel model) { if (model.Angles != null) return model.Angles; var angles = model.Rotation.Kind == RotationPolicyKind.Automatic ? RotationCandidates.ForShape(model.Rotation, model.PerimeterShape) : model.Rotation.EnumerateAngles(maxSamples: 4000); // Perimeter symmetry does not establish symmetry of the cutouts. return model.Angles = (model.CutoutShapes.Count == 0 ? RotationCandidates.DistinctOutlines(model.PerimeterShape, angles) : angles).ToList(); } }