mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-16 05:21:37 -04:00
Slice 3b task 2 (D8/C10-C12). New OpenNest.Engine.Jobs.ClampRemovalRecovery: after the ordinary clamped solve leaves demand unmet, probes (via the caller's own selected engine, quantity one, one fresh empty sheet) whether each unplaced part fits on any offered stock with every clamp in place; only a part that fits nowhere is 'blocked' (C10). For a blocked part, searches ascending run size then ascending start index over each stock's own clamp zones (already rail-position order) for the smallest contiguous run whose removal lets it fit alone - fewest clamps first, nearest the origin on ties (C11). Builds reduced-clamp stock variants for every resolved (part, stock), re-solves the whole residual demand (so freed space can also take other unmet parts, C12) with original stock carried forward minus phase 1's usage (a finite pool is never offered both full and reduced, so it is never double counted) and merges the result back: phase 2 placements get fresh instance indices continuing from phase 1's, plates are reindexed, and fulfillment/stock usage are recomputed. Returns null (no-op) whenever the job is already complete, no stock has any clamp, no remaining part is genuinely blocked, or even every clamp removed cannot fit a blocked part - so an ordinary job is bit-identical to Slice 3 (C10's last sentence). NestPipeline.RunCore calls it once, right after the main solve, only on the non-cutout-preview path; a recovery replaces 'raw' and expands 'job' (adding exactly the reduced-clamp variants used) before the existing validation/cost/grouping/binding code runs unchanged. NestPipelineCommit.SyncClampZones now derives which rail clamps (if any) are missing from a committed sheet's solved zones and, when the rail recreates exactly that reduced set, reattaches the rail with Plate.SetClampRail(rail, removedClamps) - so a recovered sheet keeps following the rail, minus its removal, through later resizes (C13), using Slice 3b task 1's plumbing. Tests: 7 new (OpenNest.Engine.Tests/Jobs/ClampRemovalRecoveryTests.cs, ClampRemovalCommitTests.cs) covering the minimal-removal + nearest- origin tie-break, freed space taking other residual demand (C12), a no-op on an already-complete job, no-op with no clamps offered, a part blocked only by stock exhaustion never being treated as clamp- blocked, a part unfit even clamp-free staying unplaced, and the full Auto-Nest-to-commit path keeping Plate.RemovedClamps/ClampRail/ ClampRemovalMinutes correct through a resize. Mutation checks (disabling the FitsAnywhere gate; reversing the ascending-start tie- break) turn the relevant assertions red, confirming real coverage. Gates: Release OpenNest.Tests 3884 passed/31 skipped, OpenNest.IO.Tests 105 passed, OpenNest.Engine.Tests 878 passed/2 skipped; desktop cross-build (-p:EnableWindowsTargeting=true) clean; scoped dotnet format --verify-no-changes clean. Desktop surface (removed-clamp display, Auto Nest summary, cut-time minutes, Clamps dialog removal-minutes field) is Slice 3b task 3, not yet done.
49 lines
2.3 KiB
C#
49 lines
2.3 KiB
C#
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Tests.Jobs;
|
|
|
|
/// <summary>
|
|
/// D8/C13: a plate Auto Nest commits with a last-resort clamp removal keeps following its
|
|
/// rail - minus the removed clamp - through later resizes, exactly like an ordinary
|
|
/// rail-following sheet (Slice 3), and the removal itself is real, inspectable plate state.
|
|
/// </summary>
|
|
public class ClampRemovalCommitTests
|
|
{
|
|
[Fact]
|
|
public void CommittedPlateKeepsFollowingTheRailMinusTheRemovedClamp()
|
|
{
|
|
// Same geometry as ClampRemovalRecoveryTests.BlockedPartRemovesOnlyTheNearestOriginCoveredClamp:
|
|
// clamps 15" apart on a 23"-long, 3"-wide sheet; a 14"-wide part can only clear both by
|
|
// removing one, and "clamp-1" (nearest the origin) wins the tie.
|
|
var drawing = new Drawing("wide", TestDrawingFactory.Rectangle(14, 3));
|
|
var nest = new Nest();
|
|
nest.Drawings.Add(drawing);
|
|
nest.PlateDefaults.Size = new Size(3, 23);
|
|
nest.PlateDefaults.EdgeSpacing = new Spacing(0, 0, 0, 0);
|
|
nest.SetClampRail(new ClampRail(1.5, 0.625, new[] { 6.0, 21.0 }));
|
|
nest.CreatePlate(); // empty template the commit fills
|
|
using var manager = new PlateManager(nest);
|
|
|
|
var request = new NestPipelineRequest("Rectangles",
|
|
new[] { new NestItem { Drawing = drawing, Quantity = 1 } },
|
|
NestStockBuilder.SinglePlate(nest.Plates[0]));
|
|
var result = NestPipeline.Run(request);
|
|
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
|
|
|
var applied = NestPipelineCommit.ApplyToEmptyPlates(result, manager);
|
|
var plate = Assert.Single(applied);
|
|
|
|
Assert.Equal(new[] { "clamp-1" }, plate.RemovedClamps);
|
|
Assert.NotNull(plate.ClampRail);
|
|
Assert.DoesNotContain(plate.ClampZones, z => z.Id == "clamp-1");
|
|
Assert.Contains(plate.ClampZones, z => z.Id == "clamp-2");
|
|
Assert.Equal(ClampRail.DefaultRemovalMinutes, plate.ClampRemovalMinutes); // 1 clamp removed
|
|
|
|
// C13: still follows the rail (minus the removal) through a resize/rotate.
|
|
plate.Size = new Size(3, 60);
|
|
Assert.DoesNotContain(plate.ClampZones, z => z.Id == "clamp-1");
|
|
Assert.Contains(plate.ClampZones, z => z.Id == "clamp-2");
|
|
}
|
|
}
|