mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-21 19:01:38 -04:00
feat(clamps): keep the whole footprint of a partly used clamp
A clamp is an avoidance area, so a clamp that only partly reaches a sheet (for example centered at 60.5" on a 60" sheet, or near the origin) now keeps its whole 1.5" x 0.625" footprint instead of being clipped to the sheet; clamps wholly beyond the sheet are still left off. Job validation accepts a zone that hangs past the sheet edges and still rejects one that does not reach the sheet at all. Both packing lanes already block keep-outs extending past the bin; a hanging-clamp solve through Rectangles, Irregular and Default keeps every part clear of the whole inflated footprint. User decision 2026-10-12, settling the plan's pending D2 question.
This commit is contained in:
1 parent
37700b3ef9
commit
bafb311735
7 files changed
+96
-43
No files matched your search
+11
-14
@@ -9,8 +9,9 @@ namespace OpenNest
|
||||
/// The machine's fixed clamp rail: identical rectangular clamps whose X centers are
|
||||
/// measured from the plate origin along the origin-side (operator) edge. Plates always
|
||||
/// start at the origin, so the same rail resolves onto every sheet size without
|
||||
/// re-scaling; a clamp whose footprint lies wholly beyond a sheet is skipped and one
|
||||
/// that still reaches onto it is kept, clipped to the sheet.
|
||||
/// re-scaling. A clamp whose footprint lies wholly beyond a sheet is skipped; one that
|
||||
/// still reaches onto it is kept whole: the zone is an avoidance area, so its full
|
||||
/// footprint applies even where it hangs past the sheet.
|
||||
/// </summary>
|
||||
public sealed class ClampRail : IEquatable<ClampRail>
|
||||
{
|
||||
@@ -57,8 +58,10 @@ namespace OpenNest
|
||||
/// The zones this rail places on a sheet, in plate coordinates (the frame of
|
||||
/// <see cref="Plate.ClampZones"/>: origin at the sheet's lower-left corner). The
|
||||
/// quadrant locates the plate origin, and with it the origin-side edge, on the sheet.
|
||||
/// Ids are <c>clamp-N</c> by rail position, so one physical clamp keeps its id on
|
||||
/// every sheet even when others are skipped.
|
||||
/// Zones are whole clamp footprints, never clipped, so one reaching past a sheet end
|
||||
/// (or deeper than a narrow sheet) extends beyond the sheet. Ids are <c>clamp-N</c> by
|
||||
/// rail position, so one physical clamp keeps its id on every sheet even when others
|
||||
/// are skipped.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ClampZone> ResolveFor(Size size, int quadrant)
|
||||
{
|
||||
@@ -69,8 +72,8 @@ namespace OpenNest
|
||||
|
||||
var extendsLeft = quadrant is 2 or 3;
|
||||
var extendsDown = quadrant is 3 or 4;
|
||||
var depth = System.Math.Min(Depth, sheetWidth);
|
||||
var y = extendsDown ? sheetWidth - depth : 0;
|
||||
// The origin-side edge is y = 0 of the sheet, or its top when it extends to -Y.
|
||||
var y = extendsDown ? sheetWidth - Depth : 0;
|
||||
var half = Width / 2;
|
||||
|
||||
var zones = new List<ClampZone>(Centers.Count);
|
||||
@@ -82,16 +85,10 @@ namespace OpenNest
|
||||
if (near >= length)
|
||||
continue; // wholly beyond this sheet
|
||||
|
||||
var clipped = near < 0 || far > length;
|
||||
var start = near < 0 ? 0 : near;
|
||||
var extent = clipped ? System.Math.Min(far, length) - start : Width;
|
||||
if (!(extent > 0))
|
||||
continue;
|
||||
|
||||
// Plate coordinates are measured from the lower-left corner; a sheet extending
|
||||
// to -X from the origin has its origin edge at x = length.
|
||||
var x = extendsLeft ? length - (start + extent) : start;
|
||||
zones.Add(new ClampZone($"clamp-{i + 1}", new Box(x, y, extent, depth)));
|
||||
var x = extendsLeft ? length - far : near;
|
||||
zones.Add(new ClampZone($"clamp-{i + 1}", new Box(x, y, Width, Depth)));
|
||||
}
|
||||
|
||||
return zones;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Geometry;
|
||||
@@ -54,6 +55,41 @@ public class ClampRailStockTests
|
||||
NestJobValidator.Validate(new NestJob([], stock));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Rectangles")]
|
||||
[InlineData("Irregular")]
|
||||
[InlineData("Default")]
|
||||
public void LanesAvoidTheWholeFootprintOfAClampHangingPastTheSheetEnd(string engine)
|
||||
{
|
||||
// 12" sheet, clamp centered 12.25" from the origin: 1" of its 1.5" lies past the end.
|
||||
var template = new Plate(10, 12) { EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25), PartSpacing = 0.25 };
|
||||
template.SetClampRail(new ClampRail(1.5, 0.625, new[] { 12.25 }));
|
||||
var zone = Assert.Single(template.ClampZones);
|
||||
Assert.Equal(11.5, zone.Bounds.X);
|
||||
Assert.Equal(1.5, zone.Bounds.Length);
|
||||
var drawing = new Drawing("square", TestDrawingFactory.Rectangle(1, 1));
|
||||
|
||||
var result = NestPipeline.Run(new NestPipelineRequest(engine,
|
||||
[new NestItem { Drawing = drawing, Quantity = 80 }], NestStockBuilder.SinglePlate(template)));
|
||||
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
var parts = result.Plates.SelectMany(p => p.Parts).ToList();
|
||||
Assert.NotEmpty(parts);
|
||||
// Independent oracle: the whole clamp grown by the bottom edge spacing.
|
||||
var keepOut = zone.Bounds.Offset(0.25);
|
||||
Assert.All(parts, part =>
|
||||
{
|
||||
var box = part.BoundingBox;
|
||||
var overlapX = System.Math.Min(box.Right, keepOut.Right) - System.Math.Max(box.Left, keepOut.Left);
|
||||
var overlapY = System.Math.Min(box.Top, keepOut.Top) - System.Math.Max(box.Bottom, keepOut.Bottom);
|
||||
Assert.False(overlapX > 1e-6 && overlapY > 1e-6, $"{box} enters {keepOut}");
|
||||
});
|
||||
// Rectangles still uses the front edge beside the clamp. (Irregular may lift its whole
|
||||
// block above the clamp band at the same count; that is a layout choice, not a leak.)
|
||||
if (engine == "Rectangles")
|
||||
Assert.Contains(parts, p => p.BoundingBox.Bottom < keepOut.Top && p.BoundingBox.Right <= keepOut.Left + 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleSheetStockCarriesTheTemplatesRailZones()
|
||||
{
|
||||
|
||||
@@ -111,14 +111,26 @@ public class ClampZoneEngineTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsZonesOutsideTheSheet()
|
||||
public void ValidatorAcceptsWholeClampsHangingPastTheSheet()
|
||||
{
|
||||
var beyond = new NestJob([], [Stock(zones: Zone(x: 95.5))]);
|
||||
Assert.Throws<ArgumentException>(() => NestJobValidator.Validate(beyond));
|
||||
// A clamp is an avoidance area: a footprint reaching onto the sheet is kept whole.
|
||||
NestJobValidator.Validate(new NestJob([], [Stock(zones: Zone(x: 95.5))]));
|
||||
NestJobValidator.Validate(new NestJob([], [new NestPlateStock("s", new Size(48, 96),
|
||||
clampZones: [new ClampZone("under", new Box(5, -0.5, 1.5, 0.625))])]));
|
||||
NestJobValidator.Validate(new NestJob([], [Stock(quadrant: 3, zones: Zone(x: -0.5))]));
|
||||
}
|
||||
|
||||
var belowEdge = new NestJob([], [new NestPlateStock("s", new Size(48, 96),
|
||||
clampZones: [new ClampZone("under", new Box(5, -0.5, 1.5, 0.625))])]);
|
||||
Assert.Throws<ArgumentException>(() => NestJobValidator.Validate(belowEdge));
|
||||
[Theory]
|
||||
[InlineData(96, 0)] // touches the far end only
|
||||
[InlineData(100, 0)]
|
||||
[InlineData(-1.5, 0)] // touches the origin end only
|
||||
[InlineData(5, -0.625)] // below the front edge, touching it
|
||||
[InlineData(5, 48)] // beyond the back edge
|
||||
public void ValidatorRejectsZonesWhollyOffTheSheet(double x, double y)
|
||||
{
|
||||
var job = new NestJob([], [new NestPlateStock("s", new Size(48, 96),
|
||||
clampZones: [new ClampZone("off", new Box(x, y, 1.5, 0.625))])]);
|
||||
Assert.Throws<ArgumentException>(() => NestJobValidator.Validate(job));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -73,9 +73,11 @@ public static class NestJobValidator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every stock zone must be uniquely identified and lie within its own sheet
|
||||
/// (edge-inclusive; geometry and finiteness already enforced by <see cref="ClampZone"/>).
|
||||
/// Zones are compared in the placement frame, where the stock carries them.
|
||||
/// Every stock zone must be uniquely identified and reach onto its own sheet (a positive
|
||||
/// overlap). A clamp is an avoidance area, so its whole footprint may hang past the sheet
|
||||
/// edges; a zone wholly off the sheet is malformed. Geometry and finiteness are already
|
||||
/// enforced by <see cref="ClampZone"/>. Zones are compared in the placement frame, where
|
||||
/// the stock carries them.
|
||||
/// </summary>
|
||||
private static void ValidateClampZones(NestPlateStock stock, NestJob job)
|
||||
{
|
||||
@@ -85,7 +87,6 @@ public static class NestJobValidator
|
||||
var ids = new HashSet<string>(StringComparer.Ordinal);
|
||||
var left = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length;
|
||||
var bottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width;
|
||||
const double slack = NestTolerances.WorkAreaSlack;
|
||||
|
||||
foreach (var zone in stock.ClampZones)
|
||||
{
|
||||
@@ -97,10 +98,10 @@ public static class NestJobValidator
|
||||
|
||||
var b = zone.Bounds;
|
||||
if (
|
||||
b.Left < left - slack
|
||||
|| b.Right > left + stock.Size.Length + slack
|
||||
|| b.Bottom < bottom - slack
|
||||
|| b.Top > bottom + stock.Size.Width + slack
|
||||
!(b.Right > left)
|
||||
|| !(b.Left < left + stock.Size.Length)
|
||||
|| !(b.Top > bottom)
|
||||
|| !(b.Bottom < bottom + stock.Size.Width)
|
||||
)
|
||||
throw new ArgumentException(
|
||||
$"Clamp zone '{zone.Id}' on stock {stock.Id} lies outside the sheet.",
|
||||
|
||||
@@ -74,31 +74,36 @@ public class ClampRailTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClampStraddlingTheSheetEndIsClippedNotDropped()
|
||||
public void ClampReachingOntoTheSheetIsKeptWhole()
|
||||
{
|
||||
// A clamp is an avoidance area: if any of it reaches the sheet, all of it applies.
|
||||
var rail = new ClampRail(1.5, 0.625, new[] { 59.5, 60.5, 60.75, 61 });
|
||||
|
||||
var zones = rail.ResolveFor(new Size(48, 60), quadrant: 1);
|
||||
|
||||
// 59.5 covers [58.75, 60]; 60.5 still reaches onto the sheet [59.75, 60];
|
||||
// 60.75 only touches the end and 61 lies wholly beyond it.
|
||||
// 59.5 covers [58.75, 60.25] and 60.5 still reaches the last 0.25" [59.75, 61.25];
|
||||
// 60.75 only touches the sheet end and 61 lies wholly beyond it.
|
||||
Assert.Equal(new[] { "clamp-1", "clamp-2" }, zones.Select(z => z.Id));
|
||||
Assert.Equal(58.75, zones[0].Bounds.X);
|
||||
Assert.Equal(1.25, zones[0].Bounds.Length);
|
||||
Assert.Equal(59.75, zones[1].Bounds.X);
|
||||
Assert.Equal(0.25, zones[1].Bounds.Length);
|
||||
Assert.Equal(new[] { 58.75, 59.75 }, zones.Select(z => z.Bounds.X));
|
||||
Assert.All(zones, z => Assert.Equal(1.5, z.Bounds.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClampAtTheOriginAndDeepClampsAreClippedToTheSheet()
|
||||
public void ClampsNearTheOriginOrDeeperThanTheSheetStayWhole()
|
||||
{
|
||||
var rail = new ClampRail(1.5, 0.625, new[] { 0.25 });
|
||||
var origin = Assert.Single(rail.ResolveFor(new Size(48, 96), quadrant: 1));
|
||||
Assert.Equal(0, origin.Bounds.X);
|
||||
Assert.Equal(1.0, origin.Bounds.Length);
|
||||
Assert.Equal(-0.5, origin.Bounds.X);
|
||||
Assert.Equal(1.5, origin.Bounds.Length);
|
||||
|
||||
var deep = Assert.Single(new ClampRail(1.5, 10, new[] { 6.0 }).ResolveFor(new Size(4, 96), 1));
|
||||
Assert.Equal(4, deep.Bounds.Width);
|
||||
var deep = new ClampRail(1.5, 10, new[] { 6.0 });
|
||||
var front = Assert.Single(deep.ResolveFor(new Size(4, 96), 1));
|
||||
Assert.Equal(0, front.Bounds.Y);
|
||||
Assert.Equal(10, front.Bounds.Width);
|
||||
// Sheets extending to -Y have the origin edge on top; the clamp hangs below the sheet.
|
||||
var top = Assert.Single(deep.ResolveFor(new Size(4, 96), 3));
|
||||
Assert.Equal(4.0, top.Bounds.Top);
|
||||
Assert.Equal(10, top.Bounds.Width);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -60,8 +60,9 @@ file keeps clamps off; an unreadable file, or one with ignored invalid values
|
||||
match the machine.
|
||||
|
||||
Every plate follows the nest's rail at its own size and quadrant: clamps wholly
|
||||
beyond a shorter sheet are left off it, one still reaching onto the sheet is
|
||||
clipped to it, and resizing or rotating a plate re-resolves its clamps. Auto Nest
|
||||
beyond a shorter sheet are left off it, one still reaching onto the sheet keeps
|
||||
its whole footprint as the avoidance area (it may hang past the sheet edge), and
|
||||
resizing or rotating a plate re-resolves its clamps. Auto Nest
|
||||
offers each stock size with the rail resolved for that size, and every committed
|
||||
sheet keeps exactly the clamps its layout was validated against. Clamps are
|
||||
drawn as hatched amber rectangles over the sheet; they cannot be selected,
|
||||
|
||||
@@ -73,7 +73,8 @@ dropped while the rest of the nest loads.
|
||||
`width` (along the edge), `depth` (into the sheet) and `centers` (X positions
|
||||
measured from the plate origin along the origin-side edge). New plates follow
|
||||
the rail and resolve it for their own size and quadrant; clamps wholly beyond a
|
||||
sheet are skipped and one still reaching onto it is clipped. The key is omitted
|
||||
sheet are skipped and one still reaching onto it keeps its whole footprint, so
|
||||
a zone may extend past the sheet edge. The key is omitted
|
||||
when a nest has no rail, so such files are unchanged; a missing, empty or
|
||||
malformed rail loads as none. On load a plate follows the rail again only when
|
||||
its saved `clamps` are exactly the rail's zones for that sheet; any other saved
|
||||
|
||||
Reference in new issue
Block a user