Operators sometimes pull a clamp for a manual layout, but only Auto Nest's
last-resort search could take clamps off a sheet. Right-clicking a clamp in
Select mode now offers to remove it from that sheet or put it back, reusing
the existing per-sheet RemovedClamps, so the zones, cut-time charge, display
and .nest persistence all behave as they do for Auto Nest removals.
The change is refused while a nest run or fill may be reading the clamps,
sharing the guard the Clamps dialog already used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec review found two places where the marker and threshold
contract does not hold.
A clamp narrower than the chord tolerance is still judged
tolerance-free against finely re-read material and reported as the
whole clamp rectangle. That existing safety policy is kept, so its
marker is the clamp's centre: within half the clamp's size (under
0.0005) of the material, though not necessarily on it. Document this
on PlateOverlapPair.Marker and in the overlap docs, and lock it in the
tiny-clamp region test.
Also document that 1e-8 grid rounding can move an area within
rounding error of the 1e-5 threshold to either side of it.
Far from the origin a grid step smaller than the coordinate's
precision leaves x (or y) unchanged: at 1e16, 1e16 + 1 == 1e16. The
starting grid loop then never ended and allocated cells without limit,
even with the new maxCells budget (a hang dump reached 4.3 GB). Stop a
row or column once its step no longer advances.
Grids that advance are untouched, so the bit-identical differential
against the former sorted-list implementation still passes. Found by
the independent quality review of the Clipper2 overlap branch.
The overlap crosshair sat on the pair's area centroid. For a split,
holed or concave overlap, that point can be off the red material:
between two patches, in a hole, or inside a curve.
PlateOverlapPair.Marker is a point inside the pair's largest island.
It is the island's pole of inaccessibility (PolyLabel), searched to a
tenth of the island's area/perimeter within a 4096-cell budget. If
that bounded search does not land inside (a long thin sliver), the
marker falls back to the midpoint of the widest interior run of a
horizontal scan line placed between vertex heights.
The overlay crosshair, marker hit testing, hover coordinates
("Marker:") and the cutting-inspection ring now use the marker.
Centroid stays the true area centroid. View > Overlap Check > Display
> Centroids is now labelled Markers.
Before the marker existed, its call sites used the centroid. Two
patches, a covered frame, a half annulus and a thin crescent then put
it off the material; now all four land on it. Disabling the scan-line
fallback fails its test. On the 389-plate corpus, markers add about
0.4 ms per overlapping pair; pair and area results are unchanged.
PolyLabel kept its candidate cells in a sorted List, which costs
O(n^2) through Insert and RemoveAt(0). A 48 x 0.002 sliver searched
to 1e-4 took about 27 s.
Use a PriorityQueue keyed by (MaxDist, insertion order). Equal bounds
still pop newest first, and initial cells in sorted order, so every
existing caller gets bit-identical poles. A frozen copy of the
sorted-list implementation checks this on 1800 random shapes and on
symmetric shapes with tied poles. Swapping the tie order fails 25 of
the differential rows.
A new Find(outer, holes, precision, maxCells) overload stops after
about maxCells distance evaluations and returns the best cell so far.
When the starting grid would exceed a quarter of the budget, it is
coarsened so the whole shape is sampled first. The unbounded overload
behaves as before.
The material-overlap diagnostic clipped every triangle of one part
against every triangle of the other, then carved each fragment by
every hole triangle. Curved overlaps produced thousands of fragments,
and a part sitting inside another part's cutout was slow even when
clear.
PlateOverlapAnalyzer now intersects each candidate pair's hole-aware
material once with Clipper2, in part-local coordinates on a 1e-8 grid.
Each operand is its outline minus the union of its holes (positive
fill rule). Regions become connected islands: a counterclockwise
outline plus clockwise Holes, including an island that both parts'
holes enclose together. PolygonAreaMoments.TrySubtract gives each
island's net moments. A pair is reported when its total shared area
exceeds Tolerance.Epsilon. Out-of-range or nonfinite coordinates make
the pair incomplete rather than clear. The overlay and the cutting
inspection highlight fill outlines with their holes.
On 389 real PEP-converted plates the pair, clamp-finding and issue
sets are identical to the previous kernel. Pair areas are equal or up
to 0.0002 larger: the kernel drops fragments below its area threshold.
With material prepared, the per-plate clipping maximum fell from
2.6 s to 52 ms (total 5.0 s to 0.5 s).
Engine and validation checks still use Collision.
PlateOverlapRegion vertices repeat their closing vertex, and the
desktop overlay drops that last vertex when it builds the fill path.
The tolerance-free tiny-clamp finding returned four open corners, so
the overlay drew it as a triangle. Close the ring like every other
region.
A direct CutOff.Regenerate(plate, settings) call with no perimeter
cache argument trusted Part.BoundingBox plus a sub-epsilon roundoff
allowance. A legitimately larger arc refit (ConvertProgram fitting
an imported arc's center to its chord) can protrude past that cached
box by more than the roundoff, so an uncached manual cutoff could
emit a segment crossing the part's true converted material.
Every production call site (Plate.RegenerateCutOffs,
AutomaticCutOffPlanner, the desktop cutoff tools) already builds and
passes a Plate.BuildPerimeterCache dictionary, so this only affects a
direct/uncached caller. Extract the per-part conversion out of
BuildPerimeterCache into Plate.BuildPartPerimeter and use it as a
lazy, on-the-fly fallback in CutOff.ComputeSegments whenever no cache
dictionary is supplied at all, so the uncached path gets the same
conservative envelope as the cached one instead of silently trusting
the bare bounding box. Cached behavior (including a cache's own
explicit null entries from a failed conversion) is unchanged.
OpenNest PM 13f7a685-0564-4cbf-8f46-a7805a0288a0.
A cut-off line passing exactly through a vertex of the clearance outline
got duplicate intersection hits and fell back to the part's bounding box.
Automatic strip lines sit on a 0.05 lattice that often lands on such
vertices, and the strip search preferred them because the box fallback
measured as less cutting, so planned cut-offs looked box-trimmed while a
slightly moved manual line was trimmed correctly.
Slice the polygonal clearance outline exactly with half-open edges:
crossing vertices count once, touching vertices zero or twice, edges on
the line are excluded whole. Zero clearance and non-polygon outlines keep
the existing path and its conservative fallback.
PEP-converted nests stored etch marks only in each placed part's locked
cutting program; the drawing program held the contour cuts alone. Because
Remove Lead-ins rebuilds a part from its drawing, clearing lead-ins also
erased the marks from every part.
Recover the marks while reading the nest: when a drawing has no scribe
moves and every placement that kept its saved program carries the same
marks, add them to the drawing in its own frame and refresh that drawing's
clean placements. Saved programs are never rewritten, drawings that already
have marks are untouched, and drawings whose saved programs disagree are
left alone rather than guessed at.
Avoid shortest round-trip decimal text for already-integral doubles. The failing tie regression and the independent 9,163-case exact-integer sweep now keep the fewer-pierce result.
Reserve nominal-line slots in the candidate lattice, compare rounded lengths in exact micro-units before pierce ties, and report real fixed-grid cut length. Per-line notices name local increases instead of negative savings. Failing-first regressions cover dense spacing, decimal ties and the legacy summary.
Check Overlaps dismissed a tiny clamp by the part's coarse material bounds
before the fine tolerance-free test ran, but that material is inscribed at
the 0.001 chord tolerance: real material in the sliver beyond a convex arc
(a short arc cap, a rotated crescent) lay outside those bounds and the clamp
read as clear. For clamps below the tolerance the bounds are widened by the
chord tolerance before rejecting.
PM 407f5eb7-329f-41e4-947e-64ad52aa4034 (review s6-delta3-review.md)
Circumscribing every arc holds all material along convex arcs but drops a
sliver along concave ones (a notch), so a tiny clamp 6e-7 into the material
beside a notch read as clear. Tiny-clamp checks now test the part flattened
both ways (all arcs inscribed, all arcs circumscribed): along any arc one of
the two holds the real material, so together they find every real overlap.
PM 407f5eb7-329f-41e4-947e-64ad52aa4034 (delta review s6-delta2-review.md)
Fine inscribed flattening still let a native arc cut 6.2e-7 into a 1e-5
clamp unseen: less than one chord's sag, so the layout check, interactive
fill and Check Overlaps all called it clear. Tiny-clamp checks now
circumscribe perimeter arcs and inscribe cutout arcs (lines stay exact), so
the polygons hold every bit of real material. Any real overlap is reported;
the cost is a possible report within the tolerance of an arc, never a false
clear. The analyzer builds the same polygons instead of a second
OverlapMaterial read.
PM 407f5eb7-329f-41e4-947e-64ad52aa4034 (delta review s6-delta-review.md)
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
Changing the nest defaults (Nest Info, Set as default, Edit Plate on an
empty sheet) left the editor's trailing empty sheet on the old size and
quadrant, so the writer's clamp-preserving empty-sheet rule saved it as a
stale extra sheet. PlateManager.ChangePlateDefaults wraps a defaults edit:
a trailing empty, cutoff-free sheet that exactly matched the old defaults
(and followed their rail with no removed clamps or explicit zones) takes the
new defaults; every other empty sheet keeps its configuration and is still
written.
PM cade6901-5f4a-4503-85bf-0ea681d9ca7f
Plate.RemovedClamps exposed a HashSet (and every untouched plate shared one
static empty HashSet), so a downcast could change one plate's removal state
or every plate's at once without re-resolving zones. It is now an
ImmutableHashSet.
PM 6ded8ac4-84fd-4e8b-9a3b-6f41e494542c item (e)
Plate.ClampRemovalMinutes multiplied every listed removed id by the
removal time, including ids the rail places beyond the sheet after a resize
or does not know at all. Only removed clamps the rail resolves on this sheet
are pulled, so only those add time; the ids themselves stay listed (C13).
PM 6ded8ac4-84fd-4e8b-9a3b-6f41e494542c item (a)
Collision.Check finds no overlap region for a clamp smaller than its
tolerance, so Check Overlaps reported a part covering a 1e-5 clamp as clear.
Clamps below 0.001 are judged with KeepOutOverlap and reported as a finding
whose fragment is the whole clamp rectangle; flush and clear parts stay clear.
PM aa995123-295f-479c-adc4-b0cbcb7cf838
D10 (user decision 2026-10-13, keep the warning): a clamp footprint covering
the modeled program start (0,0) makes the first rapid of every posting attempt
cross it, and that warning has no exemption and no persisted bypass. Lock the
behavior in all four quadrants with a physical-footprint oracle and a mutation
red (skipping rapids that start inside a zone reddens exactly the four
origin-covering rows). Document the design in docs/post-verification.md.
The desktop captures the overlap freshness stamp before its error
boundary, and a null cut-off or clamp zone in the plate's lists threw
NullReferenceException there. Stamp missing entries as a distinct empty
state so the whole-plate capture can report the malformed input as a
failed check instead.
Project Memory: opennest cdbe2629-b562-46ee-9509-2b120a511654
Pre-post clamp checks looked only at nominal cut-off definitions and
rapids. A definition moved without regenerating its placed program
left a cut through an active clamp that posted with no warning, because
cut-off material is excluded from the overlap check.
Also check each cut-off part's placed cutting motions against the
clamps in the placement frame (lines exactly, arcs by their extent).
Report one warning per cut-off part and clamp, so a current definition
does not produce a duplicate of its nominal finding.
Project Memory: opennest cdbe2629-b562-46ee-9509-2b120a511654
Plate.ClampZones are stored relative to the sheet's lower-left corner,
but cut-off positions, motions and part poses use the signed quadrant
frame. The automatic cutoff planner, manual cut-off contacts, clamp
rapid warnings and whole-plate overlap capture compared the two
directly, so in quadrants 2-4 a cut or rapid through a real rail clamp
was neither avoided nor warned about.
Add ClampZoneFrame.ToPlacement, which maps live zones into the
placement frame exactly like the sheet and engine stock, and use it in
every Slice 5 consumer. It rejects missing or malformed zones (bounds
stay publicly mutable); pre-post verification reports those as an
incomplete clamp check instead of a clear result. Overlap freshness now
treats a size or quadrant change as moving the zones.
Tests use real ClampRail zones in all quadrants; the hand-signed
quadrant fixtures that hid the defect are converted to rails.
Project Memory: opennest 274836d5-fb76-4619-b3cc-0c811e767dd6
The automatic cutoff planner tested only the proposed X against active
clamp spans, then certified a tolerance-close existing line at its own
X. A manual line just inside a clamp edge could therefore be reported
as a verified tail separator. Test the proposed X and every equivalent
existing definition before regenerating.
Also keep a same-line limited manual cut blocking the whole plan when
it lies on a clamp, as it did before clamp checks were added.
Project Memory: opennest 51597040-cff6-4304-87dc-3c2a13edf45a
Slice 3b task 3 (D9 + display). OpenNest.Core: TimingInfo gains
ClampRemovalMinutes (included in +, -, * so every aggregation stays
consistent); Timing.GetTimingInfo(Plate) adds plate.ClampRemovalMinutes
once per physical sheet, before the quantity multiply so it scales
with everything else cut on that sheet; CalculateTime adds it to the
estimate as a flat number of minutes regardless of the cut parameters'
unit. A plate with nothing removed is unaffected (zero).
OpenNest.Data: ClampSettings and ClampRailEditor carry RemovalMinutes
through the same three boundaries as clamp size - the rail/constructor,
the settings file (missing key falls back to
ClampRail.DefaultRemovalMinutes; present but negative/non-numeric/null
is ignored and reported Partial, same as width/depth), and
ToMachineDefaults - except it is a time, not a length, so
ConvertedTo never scales it between inches and millimeters.
Desktop: ClampsForm gets a 'Minutes to remove and replace one clamp'
field wired to the editor; PlateRenderer.DrawRemovedClamps draws each
clamp Plate.RemovedClamps lists (resolved from the full rail, since a
removed clamp is absent from Plate.ClampZones) as a dashed grey
outline with a small 'remove' marker, distinct from an active clamp's
solid hatch and still display-only - no hit test, selection or
sequencing; PlateView calls it right after the active-zone pass.
MainForm's Auto Nest completion flow lists every committed sheet with
a removal as 'Sheet N: remove clamp-X (+5 min)', using
Plate.RemovedClamps/ClampRemovalMinutes from Slice 3b tasks 1-2.
Tests: 10 new Data (ClampSettingsTests, ClampRailEditorTests) covering
round-trip/default/invalid-value handling and the never-scaled-by-units
rule; 4 new Core (TimingTests) covering the per-sheet add, the quantity
scaling, CalculateTime, and nest-level aggregation across plates.
Desktop changes (dialog field, renderer, Auto Nest summary) are
compile-verified only (-p:EnableWindowsTargeting=true), matching the
project's existing WinForms testing bar - no automated UI test exists
for the sibling DrawClampZones either.
Gates: Release OpenNest.Tests 3898 passed/31 skipped, OpenNest.IO.Tests
105 passed, OpenNest.Engine.Tests 878 passed/2 skipped; desktop and
OpenNest.WinForms.Tests cross-builds clean; scoped dotnet format
--verify-no-changes clean.
This completes Slice 3b (D8/D9) on top of Slice 3's rail-following
plates. Not published; master and other feature/clamp-zones-* branches
untouched. Windows runtime/visual acceptance not claimed - the plan's
click-through checklist (after slices 3-5) still applies.
Slice 3b task 1 (D8/D9): ClampRail gains RemovalMinutes (D9, default
5 minutes, validated finite >= 0) and Plate gains RemovedClamps (rail
clamp ids taken off this sheet as a last resort) plus the computed
ClampRemovalMinutes. Plate.SetClampRail gains an overload that
attaches a rail with a removed-clamp set; zones become the rail's
resolution for the sheet minus those ids, re-resolved on every
resize/rotate so a removed clamp stays off and reappears removed if a
later resize brings it back onto the sheet. ClampRail.Matches gains an
overload comparing against the reduced resolution.
IO: PlateDto.RemovedClamps and ClampRailDto.RemovalMinutes persist
(missing removalMinutes falls back to the default; missing
removedClamps means none). The writer's empty-plate drop logic no
longer drops a sheet whose only content is a removal state the
defaults can't recreate. The reader re-attaches the nest rail to a
plate only when its saved zones equal the rail's resolution minus its
saved removed ids, so a reduced-clamp sheet keeps following the rail
(and the removal) through later resizes instead of freezing as static
zones.
Engine/desktop wiring (minimal-removal computation, Auto Nest
last-resort removal, cut-time, UI) is Slice 3b tasks 2-3, not yet
done.
Tests: 16 new Core (ClampRailTests) + 5 new IO (ClampRailPersistenceTests)
covering validation, resize/resolve behavior, round-trip, legacy
missing-key defaults, and the empty-plate-with-removal-only retention
case. Mutation check (reverting the RemovedClamps subtraction) turns
3 Core + 2 IO assertions red, confirming real coverage.
Gates: Release OpenNest.Tests 3884 passed/31 skipped, OpenNest.IO.Tests
105 passed, OpenNest.Engine.Tests (Clamp filter) 98 passed; scoped
dotnet format --verify-no-changes clean.
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.
Plan Cutting previews were built without the plate's clamp zones, so
machine clamps disappeared from the canvas while inspecting a proposal.
Copy the plate's immutable zones onto the detached preview; it does not
follow the rail, so it never re-resolves.
Review finding 8, Project Memory 4e5f566d.
Read clamps.json as a raw document so an explicit null is reported as
an ignored value (Partial, which the desktop warns about) instead of
being taken as an absent field. Missing or invalid sizes now fall back
to the physical 1.5 x 0.625 inch clamp expressed in the file's units,
so a millimeter file no longer gets a 1.5 x 0.625 mm clamp. Values that
would overflow when converted between inches and millimeters are
ignored and reported instead of throwing while creating a nest.
Review findings 2, 3 and 6, Project Memory 4e5f566d.
Enabled clamp settings give a new nest its rail; whole-job Auto Nest
through the template stock, validated pipeline and commit keeps every
part clear of the clamps (an independent box oracle grown by the bottom
edge spacing) on every sheet while still using the edge between them,
for Rectangles and Default in quadrant 1 and Rectangles in quadrant 3.
Save and reload keep the rail, each sheet's zones and placements, and
do not write the empty tail sheet.
New and BOM import give a new nest the machine clamp rail from
clamps.json when it is enabled, converted to the nest's units; with the
default (off) settings nests are created exactly as before. An
unreadable or partly invalid settings file warns once per session.
Nest > Clamps... edits the current nest's rail: clamp width and depth
and a sorted list of X centers with Add/Remove. OK applies the rail to
every plate (Cancel discards the edits); Load Machine Defaults only
fills the dialog; Save as Machine Defaults writes clamps.json at once
and is labelled as not undone by Cancel. The dialog refuses to open
while nesting or a plate action runs. Its decisions live in the
Linux-tested ClampRailEditor; the form only renders and forwards input.
PlateView draws clamp zones as translucent hatched amber rectangles
over the sheet and parts and repaints when they change. They take part
in no hit test, selection or sequencing.
Persist the machine clamp rail in %APPDATA%\OpenNest\clamps.json, in
the same shape as the nest defaults store: camelCase JSON written with
retry, and a read that never throws. Clamps are off by default: a
missing or unreadable file keeps them off, and nothing changes until
the settings are enabled with at least one center.
Each value is read independently; an invalid one (including a single
bad clamp center) is ignored and reported as Partial so the desktop
can warn that the rail may lack a physical clamp. Values carry their
units and convert between inches and millimeters for a new nest.
Add ClampRail: identical clamps (width along the edge, depth into the
sheet) at X centers measured from the plate origin on the origin-side
edge. It resolves onto any sheet size and quadrant without re-scaling:
clamps wholly beyond the sheet are skipped, one still reaching onto it
is clipped to it, and ids follow rail position (clamp-N) so a physical
clamp keeps its id on every sheet.
A plate can follow a rail (Plate.SetClampRail); its ClampZones are then
derived and re-resolved whenever Size or Quadrant changes, because the
clamps are fixed to the machine. Plates without a rail keep explicit
zones unchanged. Nest.PlateDefaults.ClampRail is the template new plates
follow, and Nest.SetClampRail applies a nest-wide rail to every plate
(an empty rail removes it).
Introduce ClampZone: an axis-aligned rectangular keep-out in plate
coordinates (id + detached Box), validated finite, positive-size and
identified. Plate.ClampZones is a first-class collection alongside
Parts/CutOffs with a ClampZonesChanged event for view refresh. Zones are
never parts: quantity, utilization, cut sequence and CNC output ignore
them (locked by tests); they carry no z-height reasoning.