Files
OpenNest/OpenNest.Engine
aj 83c77269a1 feat(engine): remove clamps only for parts that cannot fit with them
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.
2026-10-13 18:15:49 -04:00
..