mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-15 15:51:38 -04:00
InteractiveClamps.MaterialInKeepOut flattened a sub-tolerance keep-out with the 0.001 polygon tolerance and called a part covering a 1e-5 clamp clear, so the fill safety net could pass it. Keep-outs below the tolerance now use KeepOutOverlap. Adds the 1e-5 zone sweep through every fill strategy and a push (characterization; both already stay clear). PM b0062c73-40ef-440e-8efb-b94908f7c2d1
180 lines
7.8 KiB
C#
180 lines
7.8 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Jobs;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="Plate.ClampZones"/> (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
|
|
/// <see cref="Plate.Parts"/>, plates, jobs or files.
|
|
/// </summary>
|
|
internal static class InteractiveClamps
|
|
{
|
|
public static bool HasZones(Plate plate) => plate.ClampZones.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Raw zone rectangles in the parts' placement frame (plate zones are stored in sheet
|
|
/// coordinates, see <see cref="ClampZoneFrame"/>), detached copies, capture order.
|
|
/// </summary>
|
|
public static IReadOnlyList<Box> ZoneRectangles(Plate plate)
|
|
{
|
|
if (plate.ClampZones.Count == 0)
|
|
return Array.Empty<Box>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static IReadOnlyList<Box> ClearanceRectangles(Plate plate) =>
|
|
InflateZones(plate, System.Math.Max(0, plate.EdgeSpacing.Bottom));
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static IReadOnlyList<Box> ValidationKeepOuts(Plate plate) =>
|
|
InflateZones(plate, System.Math.Max(0, plate.EdgeSpacing.Bottom - NestTolerances.SpacingSlack));
|
|
|
|
private static IReadOnlyList<Box> InflateZones(Plate plate, double clearance)
|
|
{
|
|
if (plate.ClampZones.Count == 0)
|
|
return Array.Empty<Box>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static List<Part> ObstacleParts(Plate plate) =>
|
|
ObstaclePartsFor(ZoneRectangles(plate), System.Math.Max(0, plate.EdgeSpacing.Bottom - plate.PartSpacing));
|
|
|
|
/// <summary>Expander walls use the bottom-edge clearance as a fixed floor,
|
|
/// independent of the separation target or its final achieved spacing.</summary>
|
|
public static List<Part> WallParts(Plate plate) =>
|
|
ObstaclePartsFor(ZoneRectangles(plate), System.Math.Max(0, plate.EdgeSpacing.Bottom));
|
|
|
|
private static List<Part> ObstaclePartsFor(IReadOnlyList<Box> rectangles, double inflate)
|
|
{
|
|
var parts = new List<Part>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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.MaterialEnters(partProfile.Perimeter.ToPolygonWithTolerance(0.001),
|
|
HolePolygons(partProfile), 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<Polygon> { outerA };
|
|
var ringsB = new List<Polygon> { 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<Polygon> HolePolygons(ShapeProfile profile)
|
|
{
|
|
if (profile.Cutouts.Count == 0)
|
|
return null;
|
|
var holes = new List<Polygon>(profile.Cutouts.Count);
|
|
foreach (var cutout in profile.Cutouts)
|
|
holes.Add(cutout.ToPolygonWithTolerance(0.001));
|
|
return holes;
|
|
}
|
|
|
|
private static List<Entity> 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);
|
|
});
|
|
}
|