Commit Graph
530 Commits
Author SHA1 Message Date
ajandClaude Opus 5.5 27a03b9caf feat(engine): add FlatBar nesting engine for flat bar stock
Reads each stock as a bar and cuts parts in one row along its length, packing
part lengths with a port of the CutList saw cut-list packer (first-fit
decreasing with swap optimization, exhaustive search for small jobs) plus a
best-fill subset-sum candidate. Each part is packed at its length less its
measured self-nesting gain, and a slide-left compaction pass nests shaped ends
(turning alternate mitred parts) and refills the freed length. Every slid pose
is confirmed with NestLayoutCheck.Clears.

On five synthetic flat-bar jobs every layout is valid and complete; total cost
33696 vs Rectangles 36816 and Default 34080.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:54:48 -04:00
ajandClaude Opus 5.5 0186ccfbff feat(nest): keep parts out of tabbed cutouts
A tabbed cutout's slug stays attached while the sheet is cut, but nesting
saw a closed hole and could place parts inside it. Nesting and layout paths
now read material through NestingMaterial.ForNesting, which drops closed
contours with tab spans so the slug counts as part. That covers part
outlines and collision lines, the cutout router, fill expansion, best-fit,
NestLayoutCheck and the GPU bitmaps. Area, overlap diagnostics and cutting
keep the hole, and programs without tabs read exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:14:22 -04:00
ajandClaude Opus 5.5 ba39c3dc55 feat(cad): add tabbed cutouts with draggable tabs
A hole can now be declared a tabbed cutout in the CAD Converter: pick a
circle or any edge of a closed hole, set the tab count and width, and drag
the tabs around the hole. The definition persists on the drawing and in the
nest file, and stays editable from the source entities.

The builder replaces the hole with open cut pieces and uncut spans on a new
Tab layer. Tabs are material, so area, nesting and overlap checks still see
a closed hole, but they are never cut. Plan Cutting and Assign Lead-ins cut
each piece from the slug with the internal lead-in and no lead-out. A lead
that would reach back over a tab is mirrored, and an arc lead onto an arc
pierces straight in. Planning accounts for each span between tabs and runs
the open-cut proof, so its own tabbed cutouts certify; tabs followed by a
lead-out are still refused. Pre-post review flags programs that still
contain tab motions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:50:11 -04:00
ajandClaude Opus 5.5 42d99f296c feat(sequencing): add per-drawing Cut first priority
Drawings flagged CutFirst (e.g. test squares) are sequenced ahead of
every other part: they get their own route with the chosen method, then
the remaining parts follow. Cutoffs crossing a cut-first part still
precede it. The flag is editable in Edit Drawing, persists in .nest as
`cutFirst` (omitted when false) and is kept by split pieces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:19:03 -04:00
ajandClaude Opus 5.5 6f555345e3 feat(cad): cut breakoff joining lines as tabbed open break cuts
Breakoffs are now picked by their joining line, default to the smaller side as scrap
(flippable), and cut that line between tabs on a new Break layer instead of slots.
Plan Cutting and Assign Lead-ins enter each cut from the scrap with the nest's internal
lead-in, or pierce on the line without one; checks treat break cuts as intentional open
cuts that are not material.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:46:35 -04:00
ajandClaude Opus 5.5 bc363cc904 feat(engine): name every unplaced part and why
NestPipelineResult.Unplaced lists each requirement's unplaced copies with a
typed NestUnplacedReason, counted from the returned placements: no stock fits
at the sampled rotations, reserved in a cutout of an unplaced frame, stock
exhausted, plate limit, no position found, or proposal not keepable. Desktop
Auto Nest, Console --autonest and MCP autonest_plate/autonest_job report them;
the API returns NestResponse.Unplaced, persisted from response schema 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:51:24 -04:00
ajandClaude Opus 5.5 4a1587d6bd feat(engine): fill cutouts first in every whole-job run
NestPipeline.Run now bundles parts that fit other parts' enclosed cutouts
into their frames before the selected engine, for desktop, Console, MCP and
API alike. A valid complete bundled trial is kept; a refused trial falls back
to the ordinary solve (reason on RefusedCutoutTrial), and an incomplete trial
is compared with it, keeping whichever valid proposal places more. An
incomplete but valid trial is now keepable like any incomplete result.
Expanded sheets list each frame's inserts before the frame, so the committed
order cuts every insert before the cutout that releases its slug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:51:18 -04:00
ajandClaude Opus 5.5 6ac56c4a3f feat(core): pre-post checks for parts nested in cutouts
Posting review now proves containment exactly as the cutting planner does
(shared NestedPartContainment) and warns when a nested part is cut after its
host (NestedCutOrder) or when a host's hole lead or the nested part's lead
meets the other's material (NestedLead), including subprogram hole poses.
Ambiguous containment is incomplete, never clear; fixed-lead motions are not
reported twice. A committed cutout preview warns until Plan Cutting cuts the
insert first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:09:55 -04:00
ajandClaude Opus 5.5 170b517a28 refactor(engine): share engine-name classification across front ends
Console and Mcp each re-derived whether a name was a fill strategy, a
whole-job engine or unknown. EngineNameResolver now owns that decision,
trims consistently and accepts renamed legacy engine names; front ends
keep their own message wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:45:16 -04:00
ajandClaude Opus 5.5 2f91f1b036 fix(engine): forged pinned-contour ordinal stays an Incomplete verdict
The 5.5 pinned-entry shortcut in ContourEntryFeasibility looked up the
choice's contour before the probe's own validation, so an owned choice with
an out-of-range contour ordinal threw instead of returning Incomplete as it
did before. The lookup now runs only for an ordinal inside the preparation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:27 -04:00
ajandClaude Opus 5.5 9816326429 fix(engine): legacy lead assignment respects pins
Assign Lead-ins (LeadInAssigner) now leaves every unlocked part with active
drawing pins unchanged and returns those parts, so it never overwrites a pin
with the plate's settings; the desktop names them ("N parts kept their
drawing lead-ins (plate P: parts ...); use Plan Cutting to apply their
drawing pins."). Parts without pins, with pins switched off, or locked are
assigned or skipped exactly as before.

The pre-post review now covers drawing pins. A part with active pins whose
placed program does not cut each pinned contour exactly as the pin's own
emission (re-emitted with the part's cutting settings and matched motion for
motion) warns "Drawing pins not applied", as do locked lead-ins overriding
pins and a pin that no longer matches the geometry; pins switched off are
not reported. The contours that are cut as pinned join locked programs in
the fixed-lead re-check against every part's clean material, and fixed
leads are now also checked against active clamps (Plan Cutting itself does
not model clamps).

AGENTS.md records the fixed-emission safeguard and the characterization
digests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:26 -04:00
ajandClaude Opus 5.5 a18f8c9b56 feat(engine): pinned contours use the fixed-lead warning path
A drawing-pinned contour is now a fixed emission like a locked program. A
regenerated placement maps its execution motions onto the emitted contour
choices (one group per choice after the program's scribes, opened by its
rapids) and owns as fixed the lead-in, cut, lead-out and tab of every pinned
contour; the rapids between contours, including the one into a pinned
contour, stay the planner's and strictly checked. If the motions do not
split cleanly into one group per choice, nothing is treated as fixed.

So a pinned lead that meets another part's material or its own part's no
longer rejects the candidate: the feasibility precheck accepts a pinned
entry as given, the search keeps it in place, and replay reports a FixedLead
warning naming the part, cutting contour, pin and the part hit, with the
advice to increase spacing or switch off drawing pins for the part. The
plate is UNVERIFIED and needs acceptance; the lead is never shortened and
the entry never moved. The unpinned control and the same plate with pins
off are Ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:25 -04:00
ajandClaude Opus 5.5 8d17cf8c59 feat(engine): capture and freshness for pins
Capture now honours a placement's pins only while they are active: a part
with DrawingPinsOff plans from the plate settings with no warning (D1's
per-part escape hatch), and Part.HasActiveDrawingPins is the shared test.
A pinned part is regenerated like any unlocked part whatever lead-ins or
settings it carried. A locked part with active pins keeps its exact program
and warns "Locked lead-ins override drawing pins on part N; Clear Lead-ins
to apply the pins." instead of being refused; an unlocked part a caller left
ineligible warns that its pins are not applied.

Apply's freshness record now holds each drawing's pin list (reference and an
explicit StateFingerprint of every pin's point, leads and tab) and each
part's pins-off flag, so editing either after capture returns Stale. The
fingerprint writer is shared between settings and pins; a reflection test
pins ContourCutPin's public surface to what the fingerprint covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:25 -04:00
ajandClaude Opus 5.5 7949d41b6d feat(core): pinned contour preparation
PreparedContours.Capture(program, parameters, pins) takes CutPinResolver
results for the same contours and fixes every resolved pin's contour: its
one and only entry is the pin's point, and it is emitted through the
existing emitter with the pin's own lead-in, lead-out and tab (angle
rounding off, so the entry never moves). Every catalogue returns the pinned
entry alone and emission refuses any other entry for that contour, so no
search path can relocate it. The certified tab size is the pinned outside's
own, so a pinned tab gap is genuine and replay certifies it. Unpinned
contours keep the shared settings and automatic selection; without pins the
existing preparation is untouched (5.1 digests unchanged).

Capture now resolves a regenerated part's drawing pins. A pin that does not
resolve (not on a contour, on several, sharing one, or unplaceable) is never
guessed: its contour stays automatic and the plate gets a FixedLead warning
naming the pin, making the proposal UNVERIFIED. A tab pinned on an internal
contour cannot be cut as pinned (only the outside is tabbed) and warns the
same way. Pins-off, locked parts with pins and freshness follow in 5.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:24 -04:00
ajandClaude Opus 5.5 d56f67a99a feat(engine): fixed-lead violations warn instead of refusing
D1a (user, 2026-10-14): one locked cutout must not refuse the whole layout.
With confirmed parameters a locked program is now a fixed emission, accepted
as given. The replay checker and the lead validator take a per-motion owner:
a violation inside the fixed emission (a lead meeting another part's or its
own material, a rapid inside the locked program crossing a cut contour, an
uncertifiable tab gap or a partial/reversed/retraced cut) is recorded apart
instead of failing the candidate. The search never rejects or learns an order
rule from them, so fixed emissions never steer it; the planner's rapid into
the locked program and every regenerated lead keep every strict check.

Replay turns those violations into typed FixedLead findings that name the
part, contour and the part hit. A plate whose only findings are these is
BestEffort with IndependentlyReplayed set: never Ready, applicable only with
the per-proposal acceptance, and CuttingPlanService.Apply still rejects it.
Overlap, unreadable programs, stale input and cancellation still block, and
unlocked programs a caller left ineligible and the no-parameter route keep
refusing as before.

Pre-post review now re-checks locked leads against every part's clean
material, so the same violations reappear before posting.

Existing tests that encoded the old locked refusal now assert the warning;
two batch tests that used a locked crossing as their hard block use an
overlap instead. The 5.1 digests are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:23 -04:00
ajandClaude Opus 5.5 15654c0690 feat(core,io): drawing library link
A nest drawing copied from the drawing library has to remember exactly which
revision it is a copy of, so later slices can tell when the library moved on
(D2b) and so a save can target the right drawing. `Drawing.Library` holds the
server key, id, revision and a snapshot of that revision's cut type, thickness,
material, grade, units, notes and package hash. It is immutable, so copies can
share it.

`DrawingDto.library` is additive and omitted when null. It is read leniently:
anything that cannot be trusted as a link drops the link with a reader warning
and the drawing still loads, because a nest must never fail to open over
provenance. Enum names are checked as single names since Enum.TryParse would
accept numbers and comma lists.

Copy paths: converter edits update drawings in place and so keep the link;
canonical engine copies carry it; split pieces are new geometry and get none.
Packages strip it so linking never changes a drawing's package bytes or hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:17 -04:00
ajandClaude Opus 5.5 7a5c06bc99 fix(bestfit): bind loaded pair candidates to a canonical copy
Saved pairs are in the drawing's canonical frame, but loaded candidates
held the source drawing, so BuildCanonicalParts/BuildSourceParts rebuilt
a different pair for any drawing with a non-zero canonical angle (a 30
degree T-shape came back 16.57 x 10.39 instead of its saved 16.92 x 8).
That reached the Irregular engine's pair catalog and the best-fit viewer
after opening a saved nest. Import now binds every candidate to one
canonical copy, as computed candidates are; fills already passed the
canonical drawing explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:05:43 -04:00
ajandClaude Opus 5.5 6e66a1efe7 refactor(io): persist best fits through an explicit IBestFitStore
Reading a nest used to fill the engine's static BestFitCache as a side
effect, which is the only reason OpenNest.IO referenced OpenNest.Engine.
Core now defines SavedPairSet/SavedPairResult and IBestFitStore; the
mapping to BestFitResult moved verbatim into Engine's BestFitCacheStore.
NestReader/NestWriter take the store as an optional BestFitStore property
and skip bestfits/ entries without one.

Every production reader and writer opts into BestFitCacheStore.Shared, so
behaviour is unchanged; the golden round-trip test still matches byte for
byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:54:43 -04:00
ajandClaude Opus 5.5 4e0699de7a fix(rectangles): break exact material-area ties the same way on every platform
ZeroClampMixedShapeLayoutIsBitIdenticalToBaseline has failed on Windows since
it was added in de9a087 while passing on Linux CI (bisect pointing at
c374e02 was an artifact: the assumed-good start commit fails too). No
commit changed the layout; the two platforms always chose differently.

In that fixture two candidate sheets (ContactPoint/Global and
BestShortSide/Ordered) both place all 30 parts at the same cost, so Choose
falls to its "more material placed" tie-break. SheetPacker summed material
area in placement order, and the disc's area differs in the last bit
between the Windows CRT and glibc math libraries. On Linux the two orders
rounded to ...253 vs ...257, so the later candidate won on rounding noise;
on Windows both rounded to ...257 and the tie fell to enumeration order as
Choose documents.

Sum a plan's area per type in type order instead (the same form as the
engine's demand area), so plans placing the same parts get bit-identical
areas and exact ties keep enumeration order everywhere. No tolerance is
loosened. The Windows layout is unchanged; Linux now matches it, so the
frozen hash moves to the Windows value, verified identical on both.

Verified: full Engine.Tests pass on Windows and Linux (1105 passed, 2
skipped); Rectangles and Default benchmark results on the six synthetic
nests are identical before and after (validity, placed parts, cost,
utilization, plates).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:20:20 -04:00
ajandClaude Opus 5.5 c3fa8e79e0 feat(desktop): mark cut-plan issues on the plate and trim the inspection panel
Plan Cutting findings dropped the checker's geometry, so selecting one could only
zoom to part extents, and overlap-blocked plates (no preview) did nothing at all.
Findings now keep the check's world-coordinate witness (overlap regions, rapid,
contacts); selecting a row draws it with the Validate Nest overlay, on the proposal
or a blocked plate's live sheet. The witness is excluded from finding equality so
repeated search rejections still collapse to one finding.

The side panel's scrolling report was a wall of text operators skipped. It is now
a colored outcome line, one line for the shown plate, and one remedy or marker
hint above the issue grid, with secondary detail moved to tooltips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:11:13 -04:00
aj 6424512e8c fix(expand): distinguish open-path material entry from bounds 2026-10-13 22:34:21 -04:00
aj 27f8c9799b fix(expand): measure open cutoff segments and refuse uncertain curved gaps 2026-10-13 22:11:12 -04:00
aj 775ece5d21 fix(expand): certify disk and cutoff paths and report invalid input 2026-10-13 21:49:28 -04:00
aj cad1083287 test(expand): bound pinned relaxation passes 2026-10-13 21:31:57 -04:00
aj 4183af4142 fix(expand): retain convex exits and report tiny clamp overlap 2026-10-13 21:31:57 -04:00
aj 73be301ebf test(expand): verify cancellation after scratch movement 2026-10-13 21:31:57 -04:00
aj fdc6f074ee fix(expand): distinguish part blockers from plate edges 2026-10-13 21:31:57 -04:00
aj dc4b07ddf5 fix(expand): separate parts touching cutout walls 2026-10-13 21:31:57 -04:00
aj 6c76617905 fix(expand): clip translations against material along their path 2026-10-13 21:31:57 -04:00
aj 43e08965b9 fix(expand): preserve entry clearances and retry blocked pairs 2026-10-13 21:31:57 -04:00
aj ddd1e422be feat(cutoffs): cut sheet-separating cut-offs after every part 2026-10-13 21:09:35 -04:00
aj 4226c0f902 fix(engine): find the fewest clamps to remove, not only neighbouring runs
Recovery only tried contiguous runs of clamps, so a part with a notch over
a middle clamp had all three clamps removed although the two outer ones
were enough (D8: 'the fewest clamps that let it fit'). One probe per clamp
now finds the clamps every removal must include (a part that cannot fit with
only that clamp left needs it off; removal is monotone), and the search
tries every set by size, mandatory clamps plus the optional ones nearest the
origin first. Full-sheet parts resolve with one probe per clamp.

PM 3df03af2-35a6-49c2-9082-fc6208f5b708
2026-10-13 18:15:50 -04:00
aj 6908e00969 fix(clamps): judge tiny clamps against the part's real material
The tolerance-free rectangle test was fed approximated material: the layout
check passed it the 1e-4-grid validation outline (a strip of real material
that wholly contains a 1e-5 clamp could vanish), and interactive fill and
the overlap check passed 0.001 inscribed arc chords (a clamp inside a disk
but outside its chords read as clear). All three now flatten the native
contours at a quarter of the clamp's smaller side, lines kept exact, so the
arc-chord sliver can never hold the clamp. Ordinary clamps are unchanged.

PM 407f5eb7-329f-41e4-947e-64ad52aa4034
2026-10-13 18:15:50 -04:00
aj a1914e147a fix(engine): reuse clamp-removal probes across identical stock
Offering several identical sheets (same size, spacing and clamps) multiplied
every feasibility probe, so eight full-sheet parts on four 48x120 options
with the eight-clamp rail spent the 1000-probe budget and recovered nothing
although each part fits with its clamps removed. A probe's answer is now
reused for the same part, engine and physically identical stock.

Delta review (s3b-delta-review.md) policy gate 3; PM 0e655fdb-a99a-40d8-968a-1863d8068c3e
2026-10-13 18:15:50 -04:00
aj 64ebda2f6f fix(engine): let clamp recovery move on to another stock with sheets left
Each blocked requirement was tied to the one stock where it needed the
fewest removals; once that stock's finite pool ran out the part stayed
unplaced even with another offered stock untouched. Recovery now runs in
rounds: a blocked part with demand left takes its best removal among the
stocks it has not tried that still have sheets (that stock's own minimal
run, C11). A variant used in several rounds stays one stock entry.

PM bf4438ac-929a-405f-ba26-fcfaa9b1c55e
2026-10-13 18:15:50 -04:00
aj 360f58c101 fix(engine): record a committed clamp removal whatever the target sheet holds
SyncClampZones returned early when the empty target already held exactly the
solved reduced zones, and otherwise preferred the target's own rail over the
stock's, so a removal committed onto such a sheet kept no rail, removed ids
or time. The stock's rail now wins, and the rail/removal state is reconciled
even when the zones themselves need no replacement.

PM 60f7a673-455e-4a87-b865-a4806f417ee6
2026-10-13 18:15:50 -04:00
aj 2b208c9b6a fix(engine): bound the clamp removal search
Recovery searched every contiguous clamp run per blocked part and stock with
whole-engine solves and no bound: 25 parts on four 20-clamp stocks cost
21,025 extra solves even though none could fit at all. Each stock is now
first probed once with every clamp removed (dismissing stock the part cannot
use and choosing the lanes worth asking); the run search goes size by size
across the remaining stock and stops at the first fit; and one recovery may
spend at most 1000 probe solves, after which it keeps the ordinary result.

PM 0e655fdb-a99a-40d8-968a-1863d8068c3e
2026-10-13 18:15:49 -04:00
aj b46310e01b fix(engine): solve each clamp removal on its own sheets within the stock pool
Phase 2 offered every removal variant to all residual demand in one solve,
each variant as unlimited stock. A sheet could take more clamps off than its
own parts needed (C11), and a finite pool could yield extra physical sheets
(C12). One root cause, one repair: each variant is now solved separately,
fewest removals first, with its own blocked parts plus unblocked residual
demand; a sheet is kept only when it holds a part that needed exactly that
removal; kept sheets come out of the base stock's remaining pool and the
plate cap. The expanded job reduces each finite base by what its variants
took, so validation sees the true inventory. Variants are cloned from the
exact sheet-frame zones instead of a translate round trip.

PM f91e054a-6f62-4080-91ed-2e1ce1f6eb7e; PM 245a555f-4f16-40b0-a3c9-430c661ad6a6
2026-10-13 18:15:49 -04:00
aj f19a825c4e fix(engine): treat a part as clamp-blocked only when no lane can place it
Last-resort removal treated the selected engine's failure to place a lone
part as proof it could not fit with the clamps in place. Rectangles packs
bounding boxes, so a concave part whose material clears a clamp lost that
clamp for nothing (C10). A part now counts as blocked only when the caller's
engine, Irregular (true material, NFP) and Rectangles all fail on every
offered stock; Default already runs both lanes.

PM c5984c6e-3882-4d15-8a42-8de76b4c2080
2026-10-13 18:15:49 -04:00
aj af50da7609 fix(engine): remove only rail clamps and always name them on the sheet
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)
2026-10-13 18:15:49 -04:00
aj 3bf63b74f9 fix(engine): keep the ordinary Auto Nest result when clamp recovery fails
A throwing feasibility probe or residual solve inside last-resort clamp
removal propagated out of NestPipeline.Run and discarded the usable ordinary
proposal. Recovery is optional: any non-cancellation failure now returns no
recovery; cancellation still propagates.

PM 1db4333f-96fc-4e3e-8643-47e1120f312c
2026-10-13 18:15:49 -04:00
aj 9b5885e5a2 fix(clamps): judge tiny keep-outs tolerance-free in interactive fill
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
2026-10-13 18:15:49 -04:00
aj 8f0c0692fd fix(validation): check tiny clamp zones without outline collapse
A clamp zone smaller than the validation outline tolerance went through
ClipperBridge.OffsetForValidation, collapsed to a null outline and made
NestLayoutCheck (and Default scoring) throw. The keep-out is now an exact
four-vertex ring, and a keep-out below 0.001 on either axis is judged by a
tolerance-free rectangle test (KeepOutOverlap): positive-area overlap only,
hole-aware, never a silent clear. Tolerances are unchanged.

PM 6859db53-e39b-4135-85c6-4967037cf880
2026-10-13 18:15:49 -04:00
aj 3f0a2703fd fix(rectangles): keep large-spacing clamp blocks off the clamp
When part spacing exceeds a clamp's inflated span the per-axis block
interval [zone - clearance + s, zone + clearance] inverts. The old
collapse to the zone centerline was not conservative: a part could
start exactly on the centerline with material inside the clamp
(PM d86d22c9 - the probe rail Box(0,0,5,0.625) at part spacing 4
completed at y=0.3125). Per decision D11, an inverted span now blocks
a zero-extent line at the clamp edge facing the sheet interior:
zone max + clearance when the zone center is in the bin's lower half
on that axis, else zone min - clearance + s, so interior-side parts
keep the exact bottom-edge clearance and the unavoidable extra
exclusion lands toward the nearer sheet edge.

Failing-first: the PM probe fixture in quadrants 1-4 and two X-inverted
fixtures (mid-rail and far-rail) were red on ab50900 against an
independent material-vs-inflated-footprint oracle; all six now pass.
Non-inverted spans stay bit-for-bit identical: four keep-row layout
digests captured from the unfixed ab50900 packer are unchanged, the
between-clamp rows still pack the gap, and the three zero-clamp C2
baseline hashes are untouched. Full Engine suite 929 passed/2 existing
skips.
2026-10-13 18:15:49 -04:00
aj 92d8964339 fix(engine): return detached clamp zones from plate stock
NestPlateStock is documented as immutable input, but ClampZones handed
out its own zones, whose bounds are mutable. Editing one after the
stock was built moved the stock's clamp while plates materialized from
the stock (layout check, fill, posting) kept the original zone, so a
part or cut on the moved clamp validated and posted clear.

Return detached copies on every read, so the stock and its plates can
never disagree. Copies keep the exact bits; engine results are
unchanged.

Project Memory: opennest 14f93b26-2e41-41f1-b09f-87dc895bb4da
2026-10-13 18:15:49 -04:00
aj df2f1b796d fix(engine): keep every plate's clamp zones in the sheet frame
Plates the engine built from stock (DrawingJobMapper.CreatePlate, used
by the public result materializer, the layout check and the placement
fillers) stored placement-frame zones, while live plates store them
relative to the sheet's lower-left corner. The same Plate.ClampZones
therefore meant two frames: the new clamp cut checks double-translated
materialized plates in quadrants 2-4, and interactive fill and push on
live plates read sheet-frame zones as placement coordinates, so they
could put parts on clamps there.

Plates built from stock now keep the stock's input zones (sheet frame,
no lossy translate-and-back), and the layout check and interactive
clamp obstacles map through ClampZoneFrame.ToPlacement, which
reproduces the stock's placement zones bit for bit, so engine
placements and validation are unchanged.

Project Memory: opennest 14f93b26-2e41-41f1-b09f-87dc895bb4da
2026-10-13 18:15:49 -04:00
aj 104b0aef99 fix(clamps): retain complete groups ahead of partial fill tails 2026-10-13 18:15:49 -04:00
aj 74544c689a fix(clamps): preserve interactive clearance and group integrity 2026-10-13 18:15:49 -04:00
aj 3e58a9e93f feat(engine): respect clamp zones in interactive placement and diagnostics 2026-10-13 18:15:49 -04:00
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