using System; using System.Collections.Concurrent; using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; namespace OpenNest.Engine.Jobs; /// /// Interactive-path clamp keep-outs (Slice 4): the inflated rectangles and static obstacle /// parts that make the single-plate fill, push and pack surfaces respect /// (mapped into the placement frame) like existing parts. Zones are never material: the fake /// obstacle parts exist only inside a single call - they are never added to /// , plates, jobs or files. /// internal static class InteractiveClamps { public static bool HasZones(Plate plate) => plate.ClampZones.Count > 0; /// /// Raw zone rectangles in the parts' placement frame (plate zones are stored in sheet /// coordinates, see ), detached copies, capture order. /// public static IReadOnlyList ZoneRectangles(Plate plate) { if (plate.ClampZones.Count == 0) return Array.Empty(); var zones = ClampZoneFrame.ToPlacement(plate); var boxes = new Box[zones.Count]; for (var i = 0; i < boxes.Length; i++) boxes[i] = Copy(zones[i].Bounds); return boxes; } /// /// Placement keep-outs inflated by the full bottom-edge clearance (D6) - the same /// convention the Rectangles lane blocks and engines honour, so fill and pack produce /// placements with at least that clearance from the raw zone. /// public static IReadOnlyList ClearanceRectangles(Plate plate) => InflateZones(plate, System.Math.Max(0, plate.EdgeSpacing.Bottom)); /// /// The validator's exact C1 rectangle (NestLayoutCheck: bottom spacing less the shared /// spacing slack). Used by the service safety net so flush engine-clear placements are /// never filtered by rounding. /// public static IReadOnlyList ValidationKeepOuts(Plate plate) => InflateZones(plate, System.Math.Max(0, plate.EdgeSpacing.Bottom - NestTolerances.SpacingSlack)); private static IReadOnlyList InflateZones(Plate plate, double clearance) { if (plate.ClampZones.Count == 0) return Array.Empty(); var zones = ClampZoneFrame.ToPlacement(plate); var boxes = new Box[zones.Count]; for (var i = 0; i < boxes.Length; i++) boxes[i] = zones[i].Bounds.Offset(clearance); return boxes; } /// /// Static obstacle parts for the directional push kernels. The kernel enforces /// part-spacing between every obstacle pair, so the rectangles carry only the /// clearance the kernel would otherwise miss: max(0, bottom spacing - part spacing). /// Final material clearance from a raw zone is then at least max(bottom, partSpacing), /// never below the C1 keep-out. /// public static List ObstacleParts(Plate plate) => ObstaclePartsFor(ZoneRectangles(plate), System.Math.Max(0, plate.EdgeSpacing.Bottom - plate.PartSpacing)); /// Expander walls use the bottom-edge clearance as a fixed floor, /// independent of the separation target or its final achieved spacing. public static List WallParts(Plate plate) => ObstaclePartsFor(ZoneRectangles(plate), System.Math.Max(0, plate.EdgeSpacing.Bottom)); private static List ObstaclePartsFor(IReadOnlyList rectangles, double inflate) { var parts = new List(rectangles.Count); foreach (var box in rectangles) { var inflated = box.Offset(inflate); var drawing = RectangleDrawing(inflated.Length, inflated.Width); parts.Add(new Part(drawing, new Vector(inflated.Left, inflated.Bottom))); } return parts; } /// /// Exact material overlap oracle for the service safety net: part outline with its /// holes subtracted against the keep-out rectangle, matching the C1 checker's verdict /// (positive-area overlap only - flush contact is clear). /// public static bool MaterialInKeepOut(Part part, Box keepOut) { var partProfile = new ShapeProfile(PartGeometry.GetPartEntities(part)); if (partProfile.Perimeter == null) return true; // A keep-out below the polygon tolerance collapses in the tolerant test and would // read as clear (PM b0062c73); judge it tolerance-free instead. if (KeepOutOverlap.NeedsToleranceFreeTest(keepOut)) return KeepOutOverlap.ProfileEnters(partProfile, new Vector(0, 0), keepOut); var zoneProfile = new ShapeProfile(RectangleEntities(keepOut)); if (zoneProfile.Perimeter == null) return true; return Collision.HasOverlap( partProfile.Perimeter.ToPolygonWithTolerance(0.001), zoneProfile.Perimeter.ToPolygonWithTolerance(0.001), HolePolygons(partProfile), HolePolygons(zoneProfile)); } public static bool MaterialsTooClose(Part first, Part second, double spacing) { var a = new ShapeProfile(PartGeometry.GetPartEntities(first)); var b = new ShapeProfile(PartGeometry.GetPartEntities(second)); if (a.Perimeter == null || b.Perimeter == null) return true; var outerA = a.Perimeter.ToPolygonWithTolerance(0.001); var outerB = b.Perimeter.ToPolygonWithTolerance(0.001); var holesA = HolePolygons(a); var holesB = HolePolygons(b); if (Collision.HasOverlap(outerA, outerB, holesA, holesB)) return true; if (spacing <= NestTolerances.SpacingSlack) return false; var ringsA = new List { outerA }; var ringsB = new List { outerB }; if (holesA != null) ringsA.AddRange(holesA); if (holesB != null) ringsB.AddRange(holesB); foreach (var ringA in ringsA) foreach (var ringB in ringsB) if (Clearance.BoundaryDistance(ringA, ringB).Distance < spacing - NestTolerances.SpacingSlack) return true; return false; } private static List HolePolygons(ShapeProfile profile) { if (profile.Cutouts.Count == 0) return null; var holes = new List(profile.Cutouts.Count); foreach (var cutout in profile.Cutouts) holes.Add(cutout.ToPolygonWithTolerance(0.001)); return holes; } private static List RectangleEntities(Box box) { var program = new Program(); program.MoveTo(box.Left, box.Bottom); program.LineTo(box.Right, box.Bottom); program.LineTo(box.Right, box.Top); program.LineTo(box.Left, box.Top); program.LineTo(box.Left, box.Bottom); return ConvertProgram.ToGeometry(program); } private static Box Copy(Box box) => new(box.X, box.Y, box.Length, box.Width); // Rectangular obstacle drawings are immutable and shared across calls; rail clamps // all share one size, so this stays tiny. Key equality implies identical geometry. private static readonly ConcurrentDictionary<(double Length, double Width), Drawing> Drawings = new(); private static Drawing RectangleDrawing(double length, double width) => Drawings.GetOrAdd((length, width), size => { var program = new Program(); program.MoveTo(0, 0); program.LineTo(size.Length, 0); program.LineTo(size.Length, size.Width); program.LineTo(0, size.Width); program.LineTo(0, 0); return new Drawing("clamp-zone-obstacle", program); }); }