Equal-cost plans break ties by the largest edge offcut, from any of the
four edges. A caller that wants the leftover off one way, such as a
shop that keeps only end drops, or a benchmark scoring the used sheet
height, had no way to say so. On the converted Nestbolt jobs the
extended Irregular search then found plans whose larger offcut is a
strip down the side, which Nestbolt scores as worse: b-blaz1-strip
3.601 -> 3.867, b-shirts-strip 2.674 -> 2.995.
NestJobOptions.OffcutCut (a CutOffAxis, default null) counts only the
strips one cut along that axis frees, for salvage credit and the
compactness tie-break alike: Horizontal the full-width bottom and top
strips, Vertical the full-length sides. PlateRemnant.LargestOffcut takes
the axis; NestJobCost, the time limit and clamp-removal recovery pass
it on. Unset, nothing changes.
Benchmark manifests accept "offcutCut": "horizontal" or "vertical".
With horizontal, one CPU, 20 s limit, Default:
b-blaz1-strip 3.934 -> 3.333
b-shirts-strip 2.995 -> 2.610
m-box-deep 1.918 -> 1.276 (Nestbolt 1.251)
Manifests accept only the words horizontal or vertical (any case); a
list such as "horizontal,vertical" is rejected. PlateRemnant rejects an
undefined axis. Fixed strategies keep their own no-salvage tie-break
and ignore the option; desktop, Console and MCP do not offer it yet.
Mutation reds: 14/14 (offcut-mutations.py).
The benchmark built stock from spacing and quadrant only, so engines were
scored on clamp-free sheets and baselines were never checked against the
clamps they were made with. Stock now comes from NestStockBuilder.FromTemplate,
as in desktop Auto Nest: the source plate's rail is resolved for every
candidate size (explicit zones are offered unchanged), and baseline plates
keep their rail and removed clamps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder) is
no longer meant to be the engine used by default, so it gets a name for what
it does. The registry lists it as Fill and maps the old name Default to it;
PlateFillService, PlateNesterFactory and NestJobOptions use Fill, and every
fill-strategy caller still accepts Default. Console --autonest now validates
engine names through NestingEngineRegistry.ResolveName so renamed names work.
Layouts are unchanged: Default and Fill resolve to the same fillers and the
golden layouts pass under the new name.
The benchmark is about to be used as the objective for LLM-designed
engines, and several gaps would have rewarded the wrong behavior:
- Ranking was utilization-first, so dropping awkward parts raised the
score. Rank valid > fully placed > cost > plates, where cost is
salvage-credited sheet area plus a largest-sheet penalty per unplaced
part; placing a part is never scored worse than omitting it.
- Salvage rate was ignored in scoring; cost now uses EstimateNetArea,
recomputed from job geometry rather than trusted from the engine.
- Rotation constraints were never validated. Add RotationPolicy.Allows
(shared with NestJobPlacementValidator) and check every placement.
- Returned sheets were trusted, so an engine could loosen spacing or
invent a size. Sheets must now match offered stock.
- Part-in-part placements were flagged as overlaps; spacing now accounts
for cutouts, with an X-sorted sweep to prune distant pairs.
- Summary averaged per-job percentages; it now sums areas and cost.
- --spacing and sheet sizes parsed with the current culture.
- Warn when .nest jobs offer only their original sheet sizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Previously each job fixed one plate size and ran a single Nest() call,
which doesn't reflect the actual problem: a real job is fulfilled
across however many plates are needed, drawn from a pool of standard
sheet sizes, not forced onto one fixed sheet.
NestEngineBase.Nest() has no way to pick its own plate's size - it
fills whatever Plate it's given - so size selection now lives in the
harness itself, applied identically to every engine:
- BenchmarkJob carries the full candidate size pool (CandidateSizes)
instead of one fixed PlateSize; one job per file, not one per size.
- BenchmarkRunner drives a loop: while items remain, pick the smallest
candidate size that fits the largest still-unplaced drawing (reusing
the codebase's own MultiPlateNester.CreatePlate/FitsBounds), build a
fresh plate of that size, and run one Nest() call to fill it. Repeat
until everything is placed, no candidate size fits what's left, or a
safety cap (40 plates) is hit.
- NestValidator now validates bounds/spacing per plate but the
quantity cap once globally across all plates, since that limit
belongs to the whole order, not any one sheet.
- JobResult/Report report PlatesUsed and a per-size breakdown instead
of a single-plate bounding-box compactness metric; utilization is
now aggregated across every plate the engine used. Ranking keeps the
same rule (utilization first), with fewer plates as the tie-break
when both are fully placed and tied - the natural multi-plate
analogue of the old single-plate compactness tie-break.
Smoke-tested against the synthetic sample across 5 candidate sizes:
correctly builds one job, picks the smallest fitting size, uses
however many plates each engine needs (1-2 here), and still catches
StripNestEngine's pre-existing out-of-bounds bug.
Loads any .nest file (or folder of them) via NestReader and nests every
drawing with quantity > 0 using each registered NestEngineBase, so it
works sight-unseen against arbitrary real jobs without any hardcoded
geometry. Optionally sweeps a fixed --sheet-sizes list instead of each
file's own plate size.
- BenchmarkJob/JobLoader build immutable job specs; a fresh Plate and
NestItem list is created per (job, engine) run so state never leaks
between engines or jobs.
- NestValidator rejects a layout if any part falls outside the work
area, any two parts are closer than PartSpacing (checked via each
part's own world-space polygon inflated by the spacing, so it holds
for arbitrary concave/holed geometry, not just bounding boxes), or a
drawing gets more parts than requested.
- Scoring matches Plate.Utilization() (placed area / full sheet area);
ties among fully-placed layouts break on the smaller used bounding
box (more usable remnant).
- Report prints a per-job ranked breakdown plus a per-engine summary
(wins, avg utilization, time), and can write a flat CSV.
Verified end-to-end against a synthetic .nest file (not committed)
against the four built-in engines; caught a genuine out-of-work-area
bug in StripNestEngine in the process.