Commit Graph
36 Commits
Author SHA1 Message Date
aj 817c4482eb test(engine): scale the Default time-limit test's limit from the unlimited solve
DefaultUnderATimeLimitReturnsAValidPlanNoWorseThanUnlimited failed on CI
(runs 41219924107 and 41224809174, Engine.Tests job): the fixed 5 s limit
assumed the unlimited Default solve takes well under a second, but it takes
about 4 s locally and longer on a loaded runner. A limit shorter than the
normal Irregular search correctly returns an earlier, worse plan, so the
assertion only holds when the limit covers that search. Use the larger of
5 s and three times the measured unlimited solve.

Red: the old test fails on one CPU shared with three busy loops; the new
one passes there. Full Engine.Tests: 2780 passed, 2 skipped.
2026-10-31 17:31:46 -04:00
aj 561f5be06c feat(engine): choose which edge strips count as offcut
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).
2026-10-31 13:29:50 -04:00
aj 63bcd89d35 feat(engine): Irregular searches until a time limit
Under a time limit, Irregular often finished in about a second and left
the rest unused: on the converted Nestbolt b-jakobs1 job it used five
sheets where four fit.

NestJobOptions.SearchUntilCancelled asks an engine to keep searching
after its normal search until the token is cancelled.
DeadlineNestingEngine sets it. Irregular then re-runs its six variants
with seeded per-type weights on its placement rule (seeds 1, 2, 3, ...,
each weight within e^+-0.4), reports each plan that beats its best,
then re-plans that plan's tail and reports any gain. It skips this when
the token cannot be cancelled, nobody listens for progress, nothing is
placeable, or the job has one part type, so unlimited solves are
unchanged.

Default now runs Irregular last (Rectangles, Circles, Irregular) so
Circles still runs, and chooses only after every candidate has run, in
tie-break order, so the run order never changes the choice. Unlimited
choices match the old run order {Rectangles, Irregular, Circles} except
when three candidates are near-tied within the 1e-9 comparison
tolerance without being transitively ordered (only on all-round jobs,
where Circles bids).

b-jakobs1 on one CPU, 10 s limit: 5 sheets (4.500) -> 4 (3.751).
2026-10-31 12:29:19 -04:00
aj 7dc8cbb22d fix(engine): fire time limits from a dedicated thread
DeadlineNestingEngine used CancelAfter, a thread-pool timer. A CPU-bound
Parallel.For keeps pool threads on their own local work, so the timer
callback can run seconds late; pinned to one CPU it waited for the
whole best-fit search (m-sign-3 under a 10 s limit returned at 64.5 s).
A dedicated background thread now waits for the limit, or for the
solve to finish, and cancels; the solve joins it before returning.

A test runs a CPU-bound Parallel.For from a pool thread under a 200 ms
limit and requires the solve to end within 3 s; with CancelAfter it
took about 7 s.
2026-10-31 10:38:55 -04:00
aj 9431006edd fix(irregular): stop waiting for the best-fit search on cancellation
PairCatalog waited for BestFitCache's search without the solve's token,
so a cancelled or time-limited Irregular solve kept running until the
search finished: minutes on one CPU for the many-segment gear, sign and
garment parts of the Nestbolt benchmark (m-public-gear-1 ran 312 s under
a 60 s limit, m-garment-teagan-1 45 min). It now uses the cancellable
overload, which stops the wait and lets the search finish in the
background and fill the cache. The drawing is private to the call or to
the caller, as that overload requires.

A test blocks the pair evaluator and checks Build throws within 5 s of
a 200 ms cancellation; with the old call it waited the full 30 s.
2026-10-31 10:38:55 -04:00
aj 23d5362aa1 feat(engine): return the best valid plan at a time limit
DeadlineNestingEngine wraps any engine with a wall-clock limit (any positive time up to
the cancellation timer's ~49.7 days). An engine that finishes in time
keeps its result, as does a result it returns after the limit. When the
limit cancels it, the best whole-job plan it reported (PlanImproved,
ranked like Default ranks candidates, a tie going to the later plan) is
returned if it passes the layout check; with none the solve fails with
TimeoutException. The caller's own cancellation is never turned into a
result. Engine plate commits are dropped and the returned result's
commits reported once it is known; plans are forwarded only when they
beat every plan shown so far.

Default now runs Rectangles, then Irregular, then Circles, so a search
stopped early already holds a valid plan. Ties still break by candidate
order (Irregular, Rectangles, Circles), so for these three the choice is
unchanged, and when every candidate throws, the first in that order is
still the one rethrown. A test seam takes a run order. Desktop, Console
and MCP do not apply a limit yet; Console can solve a job twice (cutout
trial and plain), which needs one shared deadline.

Mutations killed (10/10): no fallback at the limit, invalid plan
accepted, caller cancellation becomes a result, engine commits
forwarded, a worse later plan replaces the best, a tie keeps the earlier
plan, no upper limit bound, failure rethrown in run order, built-in run
order unchanged, run order ignored.
2026-10-31 08:49:27 -04:00
ajandClaude Opus 5.5 4d55e11318 style: use one spelling across the codebase
The repo mixed variants such as center/centre and color/colour, so a
name could be spelled either way and searches missed half the hits.
Standardize on the US spelling, matching .NET's own APIs (Color, Center).

Wire values owned by other systems keep their spelling: the material
ledger's "Cancelled" hold status and GitHub's "cancelled" job status.

No behavior change beyond the spelling of a few status messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:11:30 -04:00
ajandClaude Opus 5.5 33f60d3dcb feat(engine): break equal-cost ties toward the larger free offcut
Without salvage credit (desktop Auto Nest on a single plate size) every
complete one-sheet layout costs the same, so engines kept whichever layout
came first. N26-23S nested 27.86" long when turning two parts fits it in
21.3".

NestJobCost.CompactnessOffcut measures the largest full-width/full-length
edge offcut as salvage does, whatever the salvage rate. Default and Circles
(CandidateScore), Rectangles and Circles sheet choice (SheetByStock) and
Irregular sheet and plan ranking use it only after cost, so no cost changes.
Sheet-level keys round to 1e-6 to keep ties platform-independent.

Rectangles layout digests are re-captured: each changed fixture keeps its
sheets, parts and cost and leaves a larger offcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:18:57 -04:00
ajandClaude Opus 5.5 f901840ed0 fix(flatbar): stop the swap optimization trading identical copies
TryImprove kept a swap when the trial run looked fuller than before, but
the trial charges each type's self-nesting gain again even when the run
already holds that type. Trading a placed copy for an identical unplaced
one therefore looked fuller by the gain and repeated forever: FlatBar ran
into the benchmark's 5-minute timeout on tabbed_cog_layout once the
all-edges MBR fix gave one part new rotations (it finished in 7 s before).

The swap is now judged on the merged run, so every kept swap really
leaves the run fuller and the loop terminates. Swaps the old test kept
that do fill the run are still kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:41:27 -04:00
ajandClaude Opus 5.5 00acaeaaf1 feat(engine): report best whole-job plans from Rectangles, Circles and Default
Desktop Auto Nest's "Best plan so far" strip now fills in for every built-in
whole-job engine except FlatBar, not only Irregular.

- Rectangles makes one sheet-by-sheet run, so it reports the finished job as
  its one plan, then replays its plate commits (token checked as before).
- Circles reports when a stock-preference run or a tail re-plan beats its
  best result so far.
- Default scores each candidate plan as it ranks results (valid, fewest
  unplaced, lowest cost, candidate order) and reports it only when it beats
  every plan already shown, so a later candidate never shows a worse plan.
  The chosen result is always the last plan reported.
- Shared ReportPlan/SameLayout helpers on CandidateProgress; Irregular uses
  them too. Reporting never changes the search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:56:38 -04:00
ajandClaude Opus 5.5 cd2bea054b feat(desktop): show Auto Nest as a sheet strip with a Keep/Discard review
Auto Nest no longer draws parts on the plate view before they are kept:
its result is a set of sheets, possibly of several sizes, not parts on the
current plate. Ghost parts stay with interactive Fill and remnant tools.

- NestJobStage.PlanImproved carries an engine's best whole-job plan so far;
  Irregular reports one whenever a variant or the tail re-plan beats it.
- NestPipeline adds PlanSheets: the plan on detached drawings built from
  the job geometry (with the caller's names and colors), so previews never
  touch caller drawings or quantities.
- SheetStripControl draws every sheet side by side at one scale, repeats
  once with their count, captioned with size, parts and utilization.
- The progress window shows "Best plan so far"; a review window shows the
  final sheets, totals, cost summary and unplaced parts before commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:56:38 -04:00
aj a2fb945669 Revert "feat(engine): live sheet preview while Irregular packs"
This reverts commit 9f0e536eeed85e4417f285b94913e03f594400f7.
2026-10-18 23:56:38 -04:00
aj b11225135e Revert "feat(desktop): draw Auto Nest previews on the trial sheet"
This reverts commit bf42cdc63d5e475fa6921a3ddac06d7a39152bec.
2026-10-18 23:56:38 -04:00
ajandClaude Opus 5.5 d384e63068 feat(desktop): draw Auto Nest previews on the trial sheet
NestProgress gains PreviewSheet, a detached plate for the stock the preview
parts lie on; Irregular builds one per stock. PlateView draws it (size, edge
spacing, clamps) in place of the plate while the preview is shown and drops
it whenever the preview is cleared or the plate changes. Auto Nest zooms out
when a trial sheet falls outside what it has already shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:56:38 -04:00
ajandClaude Opus 5.5 c997b2e588 feat(engine): live sheet preview while Irregular packs
The frontier packer reports its placements after each choice; the engine
turns them into preview parts at most four times a second and sends them as
NestProgress.BestParts with the stock work area, which desktop Auto Nest
already draws on the plate view. Reporting only reads finished placements,
so layouts are unchanged with or without a progress sink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:56:38 -04:00
ajandClaude Fable 5.1 e6c87df372 perf(engine): pool best-fit pair searches by part geometry across jobs
Every Auto Nest run, and every Default-engine run, recomputed each part's
best-fit pairs: job snapshots are fresh objects and BestFitCache keyed
its entries on Drawing reference, so the Irregular engine paid
PairCatalog.Build (about half of a 118-part solve) on every run of the
same drawings.

BestFitCache now keeps a process-wide, bounded (512 sets, least recently
used first) pool of unfiltered candidate sets keyed by the exact content
of a PartGeometrySnapshot (mode and every motion value, bit for bit;
PartGeometrySnapshot.ContentComparer) and the spacing. Every drawing
DrawingJobMapper.CreateDrawing reconstructs from a snapshot declares
that geometry (BestFitCache.ShareByGeometry), so the Irregular pairs and
blocks, the cutout lattice and the Fill/Default job paths all read and
fill the pool. Candidates depend on the program geometry and spacing
alone, so a pooled set is the set the search would produce: benchmark
layouts, costs and placements are unchanged on the synthetic nests (all
engines) and on the 67-drawing, 118-part job. Failed searches are not
pooled, Invalidate leaves the pool alone, Clear empties it, and a
drawing whose Program is replaced stops vouching for its geometry.

Same 118-part job solved twice in one benchmark process (--parallel 1):
the second solve now takes 13.1 s after a 33.0 s first; before, 35.6 s
after 41.8 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 22:17:53 -04:00
ajandClaude Fable 5.1 d317a90f1e perf(engine): cut Auto Nest time on many-part jobs
A 67-drawing, 118-part job took about six minutes through the desktop
Auto Nest path and was cancelled before any layout appeared. Almost all
of it was the cutout pre-pass, the rest the Irregular engine.

Cutout pre-pass (317 s -> 8 s on that job, same bundles and poses):
- The lattice and NFP searches prepare the frame's and the occupants'
  validation outlines once per search (NestLayoutCheck.Prepare) instead
  of re-outlining and re-triangulating the frame at every candidate, and
  test candidates against the small occupants before the frame. Answers
  match the pairwise check exactly, including its leftmost-inflated rule.
- A hole's proposal is replayed for the same frame, hole and offered
  inserts: automatic-rotation inserts ask the same question at every
  frame angle.

Irregular engine (40 s -> 15 s, benchmark 42 s -> 19 s, same layouts):
- Concave no-fit polygons are built as the union of pairwise convex sums
  of each footprint's convex pieces (ear clipping plus Hertel-Mehlhorn
  merging on the Clipper lattice) instead of n*m edge-pair quads; a
  footprint that cannot be decomposed keeps the old sweep.
- Pair and block catalogs share one private drawing per part geometry,
  owned by the engine instance, so each part's best-fit search runs once
  per instance and across a pipeline's trial and ordinary solves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 21:13:15 -04:00
ajandClaude Opus 5.5 de845a5818 feat(engine): add Circles engine for round parts
Circles packs every part as its minimum enclosing circle at centers tangent
to two of the work-area walls, placed discs and clamp keep-outs, bottom-left
and left-bottom, with hexagonal and square lattice seeds. It bids only when
every part fills at least 0.8 of its enclosing circle and otherwise returns
everything unplaced at once. Default runs it as a third candidate.

Rectangles' sheet-by-sheet stock loop moves to NestingEngines/SheetByStock
(Rectangles output unchanged) and Default's candidate scoring and commit
forwarding become shared helpers. Circles also runs the whole job preferring
each offered stock and keeps the best result. Contract tests take shapes an
engine can override.

Synthetic round jobs, salvage-credited: Circles wins or ties all six with
valid layouts; Default cost falls about 4% versus Irregular alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:49:48 -04:00
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 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
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 fbca54637b fix(irregular): retain sub-grid clamp obstacles 2026-10-13 18:15:49 -04:00
aj c374e02614 feat(irregular): treat clamp zones as static NFP obstacles 2026-10-13 18:15:49 -04:00
aj de9a087027 feat(rectangles): avoid clamp zones in sheet packing
Block each clamp's keep-out into the max-rects free list before any
packing: same split logic as placements, not a used item (no contact
score, no material). The blocked rectangle accounts for the inflated
item model exactly - material keeps the bottom-edge clearance from the
clamp without paying an extra part-spacing tax; degenerate spans
collapse to the zone centerline (conservative, never permissive).

Zero-clamp sheets skip blocking entirely: three fixture layout hashes
(rectangle grid, mixed shapes, multi-sheet overflow), captured from the
unchanged packer at a3f1124, stay bit-identical (C2). With clamps:
placements never intrude, demand lands between two clamps when it fits,
a full-width front rail makes an otherwise feasible job Incomplete, and
a far-away zone leaves the layout hash equal to no-clamps.

Rectangles lane only; Irregular parity is Slice 2.
2026-10-13 18:15:49 -04:00
aj 522de2761c feat(irregular): reuse completed plate layouts for repeat batches 2026-10-12 12:54:15 -04:00
aj 49017c4a91 feat(rectangles): repeat completed sheets from remaining demand 2026-10-12 12:53:59 -04:00
aj 5af44b251c feat(engine): make Default choose between Irregular and Rectangles per job
Default is the engine every front end uses when none is named. It now runs
Irregular, then Rectangles, checks both layouts with NestLayoutCheck and keeps
the best: valid first, then fewest unplaced parts, then lowest salvage-credited
cost; ties keep Irregular. A candidate that throws or returns nothing is
skipped, cancellation stops the search, and only the chosen layout's plate
commits are reported. Neither engine wins every job in the lane benchmarks,
and Rectangles adds little time while also covering Irregular's invalid
layouts.

The registry lists Default first and no longer maps the name to Fill; fill
strategy callers still read Default as Fill. A future circle/ring engine joins
as another candidate.
2026-10-06 12:26:10 -04:00
ajandClaude Sonnet 5 eda4a846f0 fix(irregular): charge nested block-fill cost against the work budget
BlockCatalog's PrepareBlocks pre-pass ran a full independent NFP-based
pack (PrivatePlateFill.Run) for up to 8 attempts per part type, but that
cost never touched the engine's WorkCounter/WorkBudget. For jobs with
several part types at double-digit quantities, these uncounted nested
solves could burn the entire 5-minute timeout inside the first sheet of
the first strategy variant, so Solve() never returned and nothing was
ever committed.

Charge quantity^2 * outline vertex count against the shared counter
before each nested fill, and stop issuing new block proposals once the
budget is exhausted, so the existing effort governor actually sees and
bounds this cost.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-05 09:29:46 -04:00
aj bd5db59e42 fix(irregular): preserve analytic line bounds after polygon cleanup 2026-10-01 20:45:22 -04:00
aj c7b4ba4457 test(irregular): cover exact-contact rotated rectangle placement 2026-10-01 18:44:51 -04:00
aj c01a348243 feat(irregular): offer certified Fill blocks for repeated parts 2026-10-01 16:17:25 -04:00
aj 94e23e4b7a feat(irregular): place best-fit pairs through member free regions 2026-10-01 12:31:38 -04:00
aj 62fd42346e fix(irregular): prevent overlaps from false NFP gaps 2026-10-01 01:05:07 -04:00
aj 7ce53008f1 fix(rectangles): place parts within the layout check's work-area slack
Parts drawn a few millionths over their sheet's work area (for example a
36.125006 panel on a 36.125 sheet with no edge spacing) pass NestLayoutCheck,
which allows Tolerance.Epsilon of overhang, but the Rectangles engine refused
them and left them unplaced. The engine now allows 90% of that slack: a box
that exceeds the sheet by no more than the allowance packs as exactly the
sheet's size, so it spans the axis and the overhang lands only past the edge.

The check's bounds slack is named NestTolerances.WorkAreaSlack (same value,
no behavior change) so the engine and the test layout assertion share it.

Rectangle-lane benchmark (91 strict + 77 boxable jobs): every layout valid;
4 jobs that left panels unplaced now place them (strict complete 86 -> 88,
boxable complete 74 -> 75, one more boxable job places 2 more parts); no other
job changed.
2026-09-30 22:58:41 -04:00
aj 4b6e131cec style(tests): apply dotnet format to RectanglesNestingEngineTests 2026-09-30 22:58:41 -04:00
aj b688ce896f feat(engine): add built-in Rectangles and Irregular nesting engines
Moves the two production plug-in engines into OpenNest.Engine under names
that describe the jobs they suit:

- Rectangles: plain and near-rectangular plates, maximal-rectangles box
  packing (was the RectanglesNestingEngine plug-in)
- Irregular: irregular profiles, no-fit-polygon frontier packing (was the
  Opus55NestingEngine plug-in)

Their tests and the shared engine contract/layout test kit move into
OpenNest.Engine.Tests/NestingEngines.

The registry maps the old plug-in names to the new engines, so saved desktop
selections, scripts and API requests keep working, and a leftover plug-in DLL
under an old name cannot shadow its replacement. Desktop startup passes the
registry's lookup when restoring the saved Auto Nest engine.
2026-09-29 21:18:37 -04:00