Layouts placed exactly at the part spacing can land ~1e-4 short once
rotated, rounded (e.g. PEP's 4-decimal exports) and snapped to the
Clipper grid, so both validators rejected layouts that were correct in
practice. NestTolerances.SpacingSlack (0.0005, far below anything a
cutting machine resolves) is now subtracted from the spacing by
NestLayoutCheck's inflation and NestJobPlacementValidator's edge-distance
check. The frozen LegacyNestValidator takes the same rule so the
equivalence tests keep comparing like with like.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job runner already checked sheet bounds on material contours only,
but the benchmark validator and salvage scoring used Part.BoundingBox,
which includes scribe/etch moves. A PEP bend tick that ends a hair past
the part's edge passed the runner yet failed the benchmark when placed
flush to the sheet edge, and it could shrink the credited offcut. Marks
only mark the surface, so bounds and salvage now use material only.
Benchmark before/after (all five built-in engines, local fixtures,
salvage 0.5): no job changed validity or cost. Regression tests pin the
new rule: a protruding tick flush to the sheet edge is valid in all four
quadrants, and a tick past the parts envelope no longer shrinks salvage
(targeted fixture cost 130 -> 120).
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engines had to reverse-engineer the benchmark validator: Opus55 assumed a
0.01 arc tolerance (the validator uses 0.001), Gpt6Astra added hand-tuned
paddings and copied the validator's check order, Qwen picked its chord
tolerance to stay under a constant it could not reference.
NestTolerances publishes the validator's arc tolerance, the Clipper grid
and SafeClearanceMargin (with its derivation). NestLayoutCheck moves the
benchmark NestValidator's checks into OpenNest.Engine as a public API
(Clears for a part pair, Violations for a whole result); NestValidator is
now a thin wrapper. Verdicts are unchanged: tests compare ordered
violation lists against a frozen copy of the old validator, and a
tangent-disc stress test covers 432 pairs at the safe margin.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All three plugin engines turned a RotationPolicy into trial angles by hand
(fixed angle, stepped sweep, or right angles plus the minimum-bounding-
rectangle angle for Automatic), each with its own normalization, dedup and
sweep caps. EnumerateAngles gives one deterministic, Allows-checked list;
RotationCandidates.ForShape adds the MBR-aligning angles via the existing
Polygon.FindBestRotation, and DistinctOutlines drops angles where the part
looks identical. A cap of one returns the sweep start rather than throwing,
since engines request a single sample for small orientation budgets.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engines optimized guesses at the benchmark cost: Opus55 re-implemented
salvage credit, Qwen used plate area per part area, Gpt6Astra ignored
salvage. NestJobCost moves StockLadder's EstimateNetArea into a public
home (net sheet area, unplaced-part penalty, whole-result Evaluate) and
the benchmark and StockLadder now call it. Scores are unchanged: tests pin
it against a frozen copy of the old computation and real benchmark runs.
Bounds still include marks, as before, so scores do not move.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every plugin engine rebuilt part material from a snapshot by hand and
filtered only rapids, so all three kept counting scribe/etch marks as
material after 1b5e1b1 fixed it in the host. JobPartGeometry is the
validator's own reader made public: SpecialLayers.IsMaterial, validated
closed contours, material area, and TryRead returning null for unreadable
parts. The job validators now use it, so engines and validation agree.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seed came from string.GetHashCode, which .NET randomizes per process,
so each run drew different samples and the "ring" case occasionally drew
fewer than six rejections and failed its coverage assertion (1 in 6 runs),
even though every validator decision matched the brute-force reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engines assembled NestJobResult by hand - instance indices, fulfillment,
stock usage, status and PlateCommitted progress - and Qwen38FlashNext got
PlateIndex wrong (stock index instead of sheet order). The builder assigns
plate and instance indices itself and rejects overproduction and exhausted
stock, so engines only decide placements.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every plugin engine re-derived the quadrant/edge-spacing work area by hand
(Gpt6Astra, Opus55 and Qwen each had a copy, as did the placement
validator). One definition on the stock removes that duplication and the
chance of an engine disagreeing with the validator's bounds.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The candidate validator flattened every arc into 1000 segments and rebuilt
both parts' polygons and edge lists for every pair it compared, so spacing
checks on filleted parts cost millions of edge pairs each. Validation, not
the fill pipeline, was nearly all of a solve's wall time.
Placed contours are now flattened once with ToPolygonWithTolerance(0.001),
the tolerance the benchmark NestValidator and Part.Intersects already use,
and each part's shape is built once per candidate. Arcs stay inscribed, so
a layout placed exactly at the spacing still passes.
12-nest PEP corpus, Default + StockLadder, --parallel 1: 2820 s -> 227 s.
Every run that finished before gives the same validity, count, plates and
cost. Three StockLadder runs that used to hit the 5-minute timeout now
finish.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Console --engine now names a jobs engine for --autonest (solved once
through NestingEngineRegistry.Create and committed onto the plate) or a
built-in fill strategy for single-plate fill through the public
PlateFillService; unknown names exit with the valid choices instead of
consulting the process-global legacy registry. MCP nesting tools take an
explicit engine argument per call with the session default, never read
process-global active-engine state, and reject whole-job engine names on
single-plate fill tools. NestingEngineRegistry gains an explicit Create
(name) resolution; PlateFillService gains a public ResolveStrategy and a
plate-number Nest overload used by interactive callers.
Pin committed fulfillment and exact placement poses for Default, Strip,
Vertical Remnant, and Horizontal Remnant through the production
PlateNesterFactory + NestJobRunner path, plus deterministic poses for
OrderedPlateNester via its StockLadder wiring (no legacy counterpart) and
authoritative progress-stage sequences for StockLadder and
FixedStrategyNestingEngine. Captured at 42bbde7 (post ShrinkFiller axis
fix); these fixtures are the regression net for the legacy-engine removal.
Note: Strip is pinned on a rectangle-variety job. On dense mixed-shape
jobs the iterative shrink path intermittently proposes overlapping
candidates (pre-existing scheduling nondeterminism), so its mixed-geometry
layout is deliberately not pinned.
ShrinkAxis.Length must shrink the Y extent and ShrinkAxis.Width the X
extent to agree with MeasureDimension/TrimToCount; Box's constructor
takes the X extent first. The estimate previously shrank the wrong
dimension and returned a mis-ordered box. Add a parameterized regression
test over both axes and both translated-remnant orientations.
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.
The witness-probe overlap test missed small corner intersections: its
candidate points (crossing-edge midpoints and vertex-centroid midpoints)
can all land on a part boundary or outside the intersection, so two 10x10
parts at (0,0) and (9,9) with zero spacing were accepted despite sharing
a 1x1 unit of material.
Route the overlap decision through Collision, which clips triangulated
polygons and keeps only positive-area regions, catching corner overlaps,
containment, and coincident poses while legal edge/corner contact stays
legal. Collision's hole subtraction was conservative (partially-clipped
triangles were kept whole), so a part inside another part's cutout could
false-positive depending on triangulation alignment; subtract holes
exactly instead: a piece outside a convex hole triangle is the union of
its clips against each edge's outside half-space.
SortStrips measured the gap between only the first two strips in original
placement order and replayed that single value between every strip after
reordering by height/width. Real (non-uniform) geometry produces varying
inter-strip gaps, so resequencing could expand the total footprint beyond
the plate's already-fitted work area, crashing StripPlateNester with
"Candidate placement falls outside the usable stock area." Using the
actual required spacing guarantees the resequenced span never exceeds
the original, since real gaps are always >= spacing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements a sealed adapter class that forces a fixed IPlateNester strategy onto
any NestJob, overriding the job's own PlacementStrategy while preserving MaxPlates.
Delegates all multi-plate allocation and stock selection to NestJobRunner.
This allows single-plate nesting strategies to compete as full whole-job
INestingEngine solvers in benchmarks, enabling comparative performance testing
of placement algorithms across various job configurations.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Task 5 of the whole-job engine API: add a geometry safety gate that
validates every candidate trial before the runner commits accounting.
- NestJobPlacementValidator: closed-contour validity, rotation-policy
compliance, work-area containment per quadrant, hole-aware material
overlap, and required part spacing. Overlap is interior-only, so
zero-clearance edge/corner contact remains a valid placement.
- NestJobValidator: route candidate validation through the geometry
gate; reject unusable/unclosed/degenerate contours up front.
- NestJobRunner: wrap candidate evaluation in a progress bridge that
tags legacy engine detail with the current candidate context.
- LegacyPlateNesterAdapter: forward IProgress to the legacy engine so
its progress surfaces under the active candidate.
- Tests: geometry (quadrants, rotations, touching, containment, holes,
empty stock, real Default/Strip smoke), validation, and cancellation
suites; repaired test fakes that emitted out-of-bounds or overlapping
placements the gate now correctly rejects.
Engine.Tests: 70 passed, 0 failed, 0 skipped in Debug and Release.
Windows-only OpenNest.Tests not run on Linux.
Replace name-based quantity deduction with reference-based drawing
identity in the engine paths the whole-job runner reaches
(NestEngineBase fill/pack, StripNestEngine deduction, RemnantFiller
ledger, IterativeShrinkFiller leftovers). Add instance-scoped
PlateNesterFactory that resolves built-in strategies without touching
the global NestEngineRegistry. Add identity and engine-selection tests.
44 net8.0 tests pass in Debug and Release; no new warnings.