mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-18 02:41:37 -04:00
Last-resort removal ran on any clamped stock, but only a rail-following plate can record which clamp to pull and its removal time: an explicit-zone target lost the instruction entirely. NestPlateStock now carries the rail its zones were resolved from (NestStockBuilder, DrawingJobMapper.FromPlate); recovery removes clamps only from rail-backed stock, and commit falls back to the stock's rail when the target plate has none. Runs are now ordered by distance from the origin along the edge, not by zone list order. PM 346d46ed-9336-4706-9392-52ed14d6bd33; PM 6ded8ac4 item (d)
133 lines
6.0 KiB
C#
133 lines
6.0 KiB
C#
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Engine.NestingEngines.Rectangles;
|
|
using OpenNest.Engine.Tests.NestingEngines;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Tests.Jobs;
|
|
|
|
/// <summary>
|
|
/// D8/D9 last-resort clamp removal in the whole-job pipeline: a part blocked by clamps on
|
|
/// every offered stock gets the fewest clamps removed that let it fit (C10/C11), and the
|
|
/// residual solve is merged back into one result; everything else is untouched (C10's
|
|
/// bit-identical guarantee for an unblocked job).
|
|
/// </summary>
|
|
public class ClampRemovalRecoveryTests
|
|
{
|
|
// Two clamps 15" apart on a 23"-long, 3"-wide sheet (no edge/part spacing, so the math
|
|
// below is exact): clamp-1 spans X [5.25, 6.75], clamp-2 spans X [20.25, 21.75]. A 3"-tall
|
|
// part exactly fills the sheet's Y extent, so it always sits at Y = 0 - through the clamp
|
|
// band - for whatever X it lands at.
|
|
private static ClampZone Clamp1() => new("clamp-1", new Box(5.25, 0, 1.5, 0.625));
|
|
private static ClampZone Clamp2() => new("clamp-2", new Box(20.25, 0, 1.5, 0.625));
|
|
|
|
// The machine rail those zones resolve from: only rail-backed clamps can be removed.
|
|
private static readonly ClampRail Rail = new(1.5, 0.625, new[] { 6.0, 21.0 });
|
|
|
|
private static NestPlateStock ClampedSheet(params ClampZone[] zones) =>
|
|
RailSheet(null, zones);
|
|
|
|
private static NestPlateStock RailSheet(int? quantity, params ClampZone[] zones) =>
|
|
new("sheet", new Size(3, 23), quantity, 0, default, 1, null, zones) { ClampRail = Rail };
|
|
|
|
private static readonly RectanglesNestingEngine Engine = new();
|
|
|
|
[Fact]
|
|
public void BlockedPartRemovesOnlyTheNearestOriginCoveredClamp()
|
|
{
|
|
// A 14"-wide part: with both clamps up, no X position in [0, 9] clears both (the gap
|
|
// between them is 13.5" < 14"), so it is genuinely blocked (C10), not merely unlucky.
|
|
var part = JobBuilder.Rectangle("p", w: 14, h: 3, count: 1);
|
|
var stock = ClampedSheet(Clamp1(), Clamp2());
|
|
var job = JobBuilder.Job(new[] { part }, new[] { stock });
|
|
|
|
var raw = Engine.Solve(job);
|
|
Assert.Equal(NestJobStatus.Incomplete, raw.Status); // sanity: today's solve fails
|
|
|
|
var outcome = ClampRemovalRecovery.TryRecover(job, raw, Engine, default);
|
|
|
|
Assert.NotNull(outcome);
|
|
Assert.Equal(NestJobStatus.Complete, outcome!.Result.Status);
|
|
var sheet = Assert.Single(outcome.Result.Plates);
|
|
Assert.Single(sheet.Placements);
|
|
// Removing either clamp alone would free enough room; "clamp-1" is nearest the origin.
|
|
Assert.DoesNotContain(sheet.Stock.ClampZones, z => z.Id == "clamp-1");
|
|
Assert.Contains(sheet.Stock.ClampZones, z => z.Id == "clamp-2");
|
|
var fulfillment = Assert.Single(outcome.Result.Fulfillment);
|
|
Assert.Equal(0, fulfillment.Unplaced);
|
|
// The expanded job the result validates against offers the original stock plus
|
|
// exactly the reduced-clamp variant actually used.
|
|
Assert.Equal(2, outcome.Job.Plates.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemovalFreesSpaceForOtherResidualDemandToo()
|
|
{
|
|
// C12: a sheet nested with a clamp removed may also take other residual parts.
|
|
var blocked = JobBuilder.Rectangle("wide", w: 14, h: 3, count: 1);
|
|
var filler = JobBuilder.Rectangle("filler", w: 2, h: 1, count: 1);
|
|
var stock = ClampedSheet(Clamp1(), Clamp2());
|
|
var job = JobBuilder.Job(new[] { blocked, filler }, new[] { stock });
|
|
|
|
var raw = Engine.Solve(job);
|
|
var outcome = ClampRemovalRecovery.TryRecover(job, raw, Engine, default);
|
|
|
|
Assert.NotNull(outcome);
|
|
Assert.Equal(NestJobStatus.Complete, outcome!.Result.Status);
|
|
Assert.All(outcome.Result.Fulfillment, f => Assert.Equal(0, f.Unplaced));
|
|
}
|
|
|
|
[Fact]
|
|
public void AlreadyCompleteResultIsUntouched()
|
|
{
|
|
// C10: no blocked part exists, so recovery is a strict no-op (bit-identical to Slice 3).
|
|
var part = JobBuilder.Rectangle("p", w: 1, h: 1, count: 1);
|
|
var stock = ClampedSheet(Clamp1(), Clamp2());
|
|
var job = JobBuilder.Job(new[] { part }, new[] { stock });
|
|
var raw = Engine.Solve(job);
|
|
Assert.Equal(NestJobStatus.Complete, raw.Status);
|
|
|
|
Assert.Null(ClampRemovalRecovery.TryRecover(job, raw, Engine, default));
|
|
}
|
|
|
|
[Fact]
|
|
public void NoClampsOfferedNeverRecovers()
|
|
{
|
|
var part = JobBuilder.Rectangle("p", w: 14, h: 3, count: 1);
|
|
var stock = JobBuilder.Stock("sheet", width: 3, length: 10); // too short, no clamps
|
|
var job = JobBuilder.Job(new[] { part }, new[] { stock });
|
|
var raw = Engine.Solve(job);
|
|
Assert.NotEqual(NestJobStatus.Complete, raw.Status);
|
|
|
|
Assert.Null(ClampRemovalRecovery.TryRecover(job, raw, Engine, default));
|
|
}
|
|
|
|
[Fact]
|
|
public void PartBlockedOnlyByStockExhaustionIsNeverTreatedAsClampBlocked()
|
|
{
|
|
// The part fits a fresh clamped sheet on its own; only a single-sheet cap stops the
|
|
// second copy. That is a capacity limit, not a clamp block, so nothing is removed.
|
|
var part = JobBuilder.Rectangle("p", w: 12, h: 3, count: 2);
|
|
var stock = RailSheet(1, Clamp1(), Clamp2());
|
|
var job = JobBuilder.Job(new[] { part }, new[] { stock });
|
|
|
|
var raw = Engine.Solve(job);
|
|
Assert.Equal(NestJobStatus.Incomplete, raw.Status);
|
|
Assert.Equal(NestJobStopReason.StockExhausted, raw.StopReason);
|
|
|
|
Assert.Null(ClampRemovalRecovery.TryRecover(job, raw, Engine, default));
|
|
}
|
|
|
|
[Fact]
|
|
public void UnfitEvenWithoutAnyClampStaysUnplaced()
|
|
{
|
|
// Wider than the whole sheet: removing every clamp still cannot make it fit.
|
|
var part = JobBuilder.Rectangle("p", w: 30, h: 3, count: 1);
|
|
var stock = ClampedSheet(Clamp1(), Clamp2());
|
|
var job = JobBuilder.Job(new[] { part }, new[] { stock });
|
|
var raw = Engine.Solve(job);
|
|
Assert.Equal(NestJobStatus.Incomplete, raw.Status);
|
|
|
|
Assert.Null(ClampRemovalRecovery.TryRecover(job, raw, Engine, default));
|
|
}
|
|
}
|