Stock can arrive up to 1.5 in wider than nominal, so only the origin-side
edge is certain. A cut-off with a head-locked overcut cuts past the nominal
far edge with the head height locked, covering the oversize. With that lock,
Plan Cutting may start the far-edge segment at the far edge: overcut outward,
rapid back still locked, unlock, cut toward the origin. Without it the
direction stays fixed, as decided in Project Memory a20ea455.
- CutOff.HeadLockOvercut / FarEdgeFirst, persisted in .nest; HEAD HEIGHT
LOCK/UNLOCK comment markers; far edge is quadrant-aware.
- Planner offers the reversed far-edge segment in the joint search, the
safest route and segment insertion; Apply records the direction.
- Cincinnati CL and CI Fiber posts gain lock/unlock code settings and refuse
head-locked cut-offs while either is empty.
- Automatic cut-off dialog setting (off by default, 1.5 in), kept between
sessions and used for new cut-offs.
Lock codes and machine behaviour with the head locked are unconfirmed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plates with staged automatic scrap cut-offs could not be planned: P260810-01
plate 1 ran out of budget even at 10x effort, while the same plate plans
Ready in 3.6k expansions without the lines. A cut-off was one fixed program
cut whole before every part its line passes through, which dragged the tour
to the middle of the sheet and left the head at the far edge of the line;
and cutting every line first (the rejected alternative) weakens the sheet
and breaks the containment operators rely on when they fast-forward a
changed program.
A cut-off stays one definition, but Plate.RegenerateCutOffs now makes one
part per trimmed segment, each with its own place in the cut sequence.
CutOffSegments gives each segment the span of the nominal line it stands
for (end of the segment before it to start of the one after), and the
dependency rules of the planner and Part Sequencing use that span, so a
segment precedes only the parts beside it. The free-order search plans the
parts as if the segments were not there, so adding lines no longer changes
a plate's route, then CutOffInsertion slides each segment into the latest
gap before its first part where the rapid into its pierce and the rapid
from its end cross nothing cut before; only when one fits nowhere is the
plate searched with the segments among the parts. The search looks through
segments wherever they sit (entries face the next part, hole routes rank
from the pre-segment position, rapids are still checked from the segment's
end), so an applied plan replanned with the order kept replays Ready.
Segments saved out of consecutive order are recorded per segment in the
.nest file (`segments`); older readers load them consecutively.
P260810-01 plate 1 now plans Ready in 3639 expansions with every check
passing; rapid travel 1836 against 1547 without the lines. Plates without
cut-offs keep their PlanningCharacterizationTests digests, and the
problem-nest corpus has no plate below its baseline.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
Two remaining pin rules from the drawing library plan:
- D3: a library drawing added to a nest whose material, grade or
thickness differ from the library record now really arrives without
its cut pins. The insert already warned and reported KeepPins = false;
it now clears the copy's pins, because they were authored for the
library material and thickness. The library link still records the
revision the copy came from.
- A copy of a placement (Clone, CloneAtOffset) keeps its drawing pins
off, so copying or patterning an overridden part does not silently
bring the drawing's pins back. Clearing lead-ins keeps it too: pins-off
is a placement choice, not lead-in state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drawing cut pins and the per-placement pins-off flag now survive
save/reload and travel in drawing packages.
- `DrawingDto.cutPins` reuses the plate cutting-parameter lead and tab
DTOs. Pins are read strictly per entry: an unknown lead or tab type is
never replaced by a default, because a pin that cut differently from
what was saved is worse than no pin. A bad entry is skipped with a
reader warning naming the drawing and pin, and the rest load; a value
that is not a list loads no pins, with a warning. A nest never fails to
open over pins.
- `PartDto.drawingPinsOff` is written only when true. Anything other than
a boolean loads as pins on (the drawing wins, D1) with a warning naming
the plate and part, so an operator's override is never lost silently.
- Packages carry pins as drawing content, so pinning a drawing changes
its package hash and the newer-revision notice can tell geometry/pins
changes from detail-only ones.
- `Part.DrawingPinsOff` holds the flag; nothing plans with it yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plate-scoped cutting plan requests now record the plate's exact state at
capture, and CuttingPlanService.Apply installs Ready, replayed proposals
for a whole scope at once:
- Any change after capture (order, pose bits, program reference or
in-place content, drawing program, lock/lead-in flags, settings,
quantity, size, quadrant or cutoff definitions) returns Stale with
nothing changed.
- Order changes without PartAdded/PartRemoved, so drawing quantities and
sentinel plates are untouched; ObservableList.Reorder exposes the same
operation and Plate.PartsReordered is raised once per changed plate.
- Regenerated parts receive owned copies of the replayed program and of
the settings captured with the request; fixed programs stay in place.
- An install failure restores every plate exactly; an observer failure
after publication is reported as a refresh error, not a rollback.
Part.Clone and CloneAtOffset copied the lead-in program but not
HasManualLeadIns, LeadInsLocked, CuttingParameters or the tracked
pre-lead-in rotation. A copy of a rotated lead-in part reported
rotation 0 (the rebuilt program's), so saving it wrote the wrong
rotation and Remove Lead-ins restored an unrotated part.
Part.Rotate on a lead-in part set the tracked rotation from the
lead-in program's own Rotation, which starts at zero when the cutting
strategy rebuilds it, so a further quarter turn left Rotation unchanged.
Lead-in parts now accumulate the applied angle instead.
Both HasOverlappingParts loops rebuilt each part's polygon from its
Program on every pair. PartOverlapChecker prepares each distinct Program
(reference identity) once and each part's world polygon once per call,
then uses the overlap-only Collision.HasOverlap. Loop order, bounding-box
prefilter, early exit and returned indices are unchanged; Part.Intersects
shares the material/polygon recipe and still returns crossing points.
Verification:
- Frozen LegacyPartOverlap differential (original Intersects and both
loops): verdicts, indices and world polygons bit-identical across fill
grids, patterns, touching/epsilon gaps, scribe/rapid/empty programs.
- Debug OverlapPolygonPreparations: 246 -> 1 and 64 -> 2 per check.
- Corpus job (169 parts, --engines Default --parallel 1, with 1a):
median 18,885 -> 13,464 ms over 4+4 alternating runs, identical
outcomes; serialized layout byte-identical to the base.
Every nesting-geometry consumer filtered only rapids, so scribe/etch
moves counted as part material. An etch tick that ends a hair outside
the outline (PEP bend ticks start on the notch edge) made the part
"open geometry leaving the material region": the job validator threw
and every built-in engine plus Gpt6Astra crashed on real PEP jobs
(PT75, drawing 4980 A01 PT77). Marks are only on the surface, so they
should never affect placement, collision, area, or validation.
- SpecialLayers.IsMaterial excludes Rapid and Scribe; used by drawing
area, canonical angle, part collision, PartGeometry, plate perimeter,
best-fit/pair evaluation, rotation analysis, GPU evaluators, and both
validators. Timing, display, splitting and posts still see marks.
- ConvertGeometry also maps the saved SCRIBE layer name to Scribe, so
programs rebuilt from stored entities keep their marks.
- NestReader repairs older files (e.g. PepNestExport output) whose
programs saved etch as cut moves while source entities kept SCRIBE.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every DefaultPlateFiller.Fill makes a fresh canonical copy of the drawing,
and BestFitCache/FillResultCache keyed by drawing reference, so fills never
shared results and the static caches grew without bound.
CanonicalFrame now records which drawing each canonical copy came from.
Both caches key weakly on that source drawing, so every canonical copy
shares one entry and released drawings can be collected. An entry is
dropped when the drawing's Program instance or canonical angle changes.
Best-fit candidates are computed once per (drawing, spacing) through
BestFitFinder.FindCandidates and filtered per plate size with the same
filter FindBestFits uses. FillResultCache keeps canonical and
non-canonical callers apart.
Adds Debug-only PerfCounters for best-fit runs, offset perimeter builds
and Part.Intersects calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part.Clone() rebuilt the clone from BaseDrawing and then re-applied the
part's absolute Rotation on top of it. Since BaseDrawing.Program.Rotation
is itself absolute (baked in), this double-counted it whenever the base
drawing already carried a nonzero rotation, silently corrupting the
clone's orientation while its Location stayed unchanged.
This only manifests for drawings needing canonical-frame axis correction
(nonzero Source.Angle), since DefaultPlateFiller wraps every drawing in a
rotated canonical copy before running any fill strategy. FillHelpers.
BuildRotatedPattern clones parts before tiling, so any strategy that
tiles interlocking pairs (Pairs, Strip/Remnant, Column/Row) could produce
overlapping placements for such drawings.
Fix: clone the already-composed Program directly instead of re-deriving
rotation from BaseDrawing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Part.Intersects has been silently non-functional everywhere it's used.
Shape.ToPolygon()/ToPolygonWithTolerance() never called UpdateBounds(),
so every freshly-built polygon kept Entity's constructor-default
zero-size bounding box regardless of its actual vertices. Collision.Check's
first step is a bounding-box pre-filter, and a zero-size box can never
overlap anything, so it always short-circuited to "no overlap" no matter
what the real geometry looked like.
That masked a second bug: Part.Intersects built its polygons via the
unconditional 1000-segments-per-arc ToPolygon() default instead of an
adaptive tolerance. For parts with several small fillets/holes this
produced tens of thousands of vertices, making the now-correct bbox
check fall through into a triangulation/clip step too slow to return
in practice. Switched to ToPolygonWithTolerance at a named tolerance
matching PartGeometry's existing convention.
PairEvaluator's own Keep/overlap check had a third, independent bug:
it used Shape.Intersects (edge-crossing detection only) at a coarse
0.01 chord tolerance, which misses containment-style overlaps and can
polygonize rounded corners coarsely enough to hide a genuine sliver
overlap. Switched to Collision.HasOverlap (full polygon clip, handles
containment) at a tighter dedicated tolerance.
Verified against a real nest file: PairFiller was tiling a BestFit
pair that PairEvaluator had incorrectly marked Keep=true, producing
visibly overlapping parts on the plate that no downstream overlap
check ever caught.
Known follow-up: OpenNest.Tests.BestFit.BestFitOverlapTests.KeptPairs_NoOverlap
still fails on 3/1082 synthetic candidates that overlap by a sub-0.001
sliver right at a rounded-corner tangent point — a separate, much
smaller precision edge case in PairEvaluator's raw (pre-transform)
coordinate frame, not a regression from this change.
Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.
Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).
Formatting is now enforceable: dotnet csharpier check . passes.
External lead-ins now sit on the line between the last internal cutout
and the next part's first pierce point, minimizing rapid travel. Cutout
sequencing starts from the bounding box corner opposite the origin and
iterates 3 times to converge the perimeter lead-in and internal sequence.
LeadInAssigner and PlateProcessor both use a two-pass approach: first
pass collects pierce points, second pass refines with next-part knowledge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EditDrawingsInConverter was replacing Drawing objects with new instances,
but Part.BaseDrawing is readonly — parts kept referencing the old drawings
with stale programs (e.g. etch lines that were removed). Now matches by
name and updates existing drawings in-place, then refreshes all parts.
Also fixes Part.Update() which applied rotation backwards and was missing
UpdateBounds() and lead-in state reset.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Box constructor and derived properties (Right, Top, Center, Translate, Offset)
had Width and Length swapped — Length is X axis, Width is Y axis. Corrected
across Core geometry, plate bounding box, rectangle packing, fill algorithms,
tests, and UI renderers.
Added FillSpiral with center remnant detection and recursive FillBest on
the gap between the 4 spiral quadrants. RectFill.FillBest now compares
spiral+center vs full best-fit fairly. BestCombination returns a
CombinationResult record instead of out params.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds ApplySingle to ContourCuttingStrategy that applies lead-in/out to
only the contour containing the clicked entity, emitting other contours
as raw geometry. Also adds ApplySingleLeadIn wrapper to Part.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Track preLeadInRotation when parts are rotated so lead-in removal
can restore the correct rotation. Remove stale HasManualLeadIns and
LeadInsLocked deserialization from NestReader since these flags are
transient state, not persisted data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three issues caused incorrect rendering after lead-in application:
- Rapid move entities from ToGeometry() were included in ShapeProfile
contour detection, turning traversal paths into cutting moves
- Program created with Mode.Incremental default made the absolute-to-
incremental conversion a no-op, leaving coordinates unconverted
- AddProgramSplit didn't call StartFigure() at rapid moves, causing
GraphicsPath to draw implicit connecting lines between contours
- Part.Rotation returned 0 from the new program instead of the actual
rotation, displacing the sequence label on rotated parts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Part.Intersects: filter intersection points at a vertex of either
shape (was both), so edge-touching parts are not flagged as overlapping
- NestEngineBase.HasOverlaps: use epsilon-based bounding box pre-filter
consistent with FillExtents and Plate.HasOverlappingParts
- PartGeometry.GetOffsetPartLines: remove extra chordTolerance added to
spacing offset — was causing 0.002" gap beyond the intended part spacing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Part.Intersects now filters out intersection points that coincide with
vertices of both perimeters (shared corners/endpoints), which are touch
points rather than actual crossings. Plate.HasOverlappingParts adds a
bounding box pre-filter requiring overlap region to exceed Epsilon in
both dimensions before performing expensive shape intersection checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CloneAtOffset shares the Program instance for tiling performance,
but rotating a part on the plate mutated the shared Program, causing
all parts from the same tile template to rotate together.
Added ownsProgram flag with EnsureOwnedProgram() that clones the
Program before first mutation, preserving tiling performance while
making user rotations independent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace shape-list iteration with ShapeProfile.Perimeter in both
Part.Intersects and PartBoundary, simplifying the logic and ensuring
only the outermost contour is used for collision detection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Offset-only copies don't modify program codes, so sharing the instance
avoids expensive cloning during large pattern tiling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
"Length" is more natural than "height" for flat plate materials.
Renames the field on OpenNest.Geometry.Size, Box.Height property,
and all references across 38 files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Part.Clone() re-clones from the drawing's unrotated program, re-rotates,
and walks all CNC codes twice for bounding box — 4 O(c) passes per clone.
CloneAtOffset clones from the already-rotated program and computes the
bounding box arithmetically, reducing to 1 O(c) pass per clone.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fill(NestItem) and Fill(List<Part>) now delegate to their Box overloads
- Add Part.CreateAtOrigin() to replace repeated 4-line build-at-origin pattern
used in NestEngine, RotationSlideStrategy, and PairEvaluator
- Remove dead code: FillArea overloads, Fill(NestItem, int), FillWithPairs(NestItem),
ConvertTileResultToParts, PackBottomLeft.FindPointHorizontal, Pattern.GetLines/GetOffsetLines,
unused count variable in FillNoRotation
- Simplify IsBetterValidFill to delegate to IsBetterFill after overlap check
NestEngine reduced from 717 to 484 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Part.Intersects included rapid move geometry (G00 traversals) when
checking for overlaps, causing false positives. The overlap validation
added in 5bebfcb rejected all FillLinear configs, producing 0 parts.
Every other GetShapes caller already filters SpecialLayers.Rapid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all projects from Source/ to repository root for simpler navigation.
- Remove External/ dependency DLLs (will use NuGet packages)
- Remove Installer/ NSIS script
- Replace PartCollection/PlateCollection with ObservableList
- Add packages.config for NuGet dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>