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 - the same
/// convention the Rectangles lane blocks and engines honor, so fill and pack produce
/// placements with at least that clearance from the raw zone.
///
public static IReadOnlyList ClearanceRectangles(Plate plate) =>
InflateZones(plate, plate.ClampClearance);
///
/// The validator's exact C1 rectangle (NestLayoutCheck: clamp clearance 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.ClampClearance - 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: each zone grown square by
/// , the layout check's keep-out, which the kernels then
/// hold at zero spacing (never the part spacing). A round spacing offset would let a part
/// tuck into the keep-out's corners.
///
public static List ObstacleParts(Plate plate) =>
ObstaclePartsFor(ZoneRectangles(plate), plate.ClampClearance);
/// Expander walls use the clamp clearance as a fixed floor,
/// independent of the separation target or its final achieved spacing.
public static List WallParts(Plate plate) =>
ObstaclePartsFor(ZoneRectangles(plate), plate.ClampClearance);
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;
// The profiles are already in world space. Facets at the polygons' tolerance understate
// an arc gap by more than the slack, which would reject a pair placed at the spacing.
return NativeContours.Of(a, new Vector(0, 0))
.TooClose(NativeContours.Of(b, new Vector(0, 0)), spacing);
}
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;
}
// Built from lines, never a program: a program's opening rapid from the origin would join
// the outline and stretch the keep-out into a wedge back to (0, 0), rejecting clear parts.
private static List RectangleEntities(Box box)
{
var bottomLeft = new Vector(box.Left, box.Bottom);
var bottomRight = new Vector(box.Right, box.Bottom);
var topRight = new Vector(box.Right, box.Top);
var topLeft = new Vector(box.Left, box.Top);
return new List
{
new Line(bottomLeft, bottomRight),
new Line(bottomRight, topRight),
new Line(topRight, topLeft),
new Line(topLeft, bottomLeft),
};
}
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);
});
}