Commit Graph
100 Commits
Author SHA1 Message Date
aj b318950a54 perf(fill): short-circuit default comparisons by count 2026-09-25 16:43:09 -04:00
ajandClaude Opus 5.5 1b862dc1a8 fix(engine): allow 0.0005 spacing slack in layout validation
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>
2026-09-25 14:19:04 -04:00
aj d1f3907505 Merge branch 'fix/mcp-config-path' 2026-09-25 14:19:01 -04:00
ajandClaude Opus 5.5 4fb924b03a chore: restore LF line endings in CLAUDE.md
39db4dc committed CLAUDE.md with CRLF endings while the repo stores it
as LF (text=auto), turning a one-line doc addition into a whole-file
diff. Renormalized; the content is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:12:40 -04:00
ajandClaude Opus 5.5 39db4dc10f feat(ui): route Auto Nest through whole-job engines
StockLadder and Engines/ plug-ins only implement INestingEngine.Solve, so
selecting them in Auto Nest had no path to run. MainForm now solves the
whole job through JobEngineNest when the selected engine is not a
built-in fill strategy, feeding NestJobProgress into NestProgressForm and
binding the result poses back onto the nest's own drawings. Whole-job
engines throw on cancel rather than returning a partial layout, so the
progress form hides Accept for these runs. Built-in strategies keep the
existing per-plate fill path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:03:10 -04:00
ajandClaude Opus 5.5 23dd99fa2f feat(benchmark): add --progress logging for engine solves
Long whole-job solves ran silently, so there was no way to tell a slow
engine from a hung one until the timeout fired. --progress hands each
solve a JobProgressLog that prints [job/engine] lines for start, finish
(or failure/timeout), every plate commit, and candidate evaluations
throttled to one line per 2 s so parallel runs stay readable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:03:10 -04:00
695ccc0a3b perf(engine): use cached triangulations in the layout check
The benchmark validator and every engine test re-triangulated both parts
for each nearby pair. NestLayoutCheck now uses TriangulatedRegion, with
Collision.HasOverlap as the fallback when it cannot decide. Verdicts are
unchanged (the frozen-validator equivalence tests still pass); validating
100 discs went from 1,254 ms to 94 ms.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:08:36 -04:00
98d0124172 perf(core): add cached-triangulation collision and an edge-grid prefilter
Collision.HasOverlap re-triangulates both polygons on every call; Qwen
measured that as its dominant cost (over 400 s -> ~110 s on a 219-part
job once cached). TriangulatedRegion (from Qwen's TriSet) triangulates a
part once and takes translation as a parameter; it returns null when it
cannot decide so callers fall back to Collision, which stays the
reference. EdgeGridPolygon (from Qwen's FastPoly) certifies clearly
disjoint shells and never reports Clear for an overlap. A seeded harness
of 100,000 decisions (concave shapes, arcs, holes, touching contacts)
finds 0 mismatches against Collision.HasOverlap.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:08:36 -04:00
4161e6d1c7 feat(core): add a concave no-fit polygon to NoFitPolygon
Core only had a convex NFP, so Opus55 and Gpt6Astra each built concave
NFPs from Clipper's Minkowski sum, and only Opus55 added the terms that
cover one part lying inside or swallowing the other - Gpt6Astra instead
filled every positive path and lost real interlocks. NoFitPolygon.Compute
ports Opus55's construction (boundary sweep united with A + p0 and
-B + a0; convex pairs use the linear edge merge). It works on filled
perimeters only; hole-aware clearance stays with collision testing.
Tests port Opus55's NFP tests and add a notch fit and a seeded property
check against Collision.HasOverlap.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:08:36 -04:00
896ed2026a fix(engine): leave scribe marks out of layout bounds and salvage
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>
2026-09-25 08:56:49 -04:00
7f63c725e6 refactor(engine): expose the layout validation contract to engines
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>
2026-09-25 08:54:54 -04:00
ec5f57171f feat(engine): add RotationPolicy.EnumerateAngles and RotationCandidates
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>
2026-09-25 08:33:52 -04:00
dd1a958f5c feat(engine): expose NestJobCost, the benchmark's scoring
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>
2026-09-25 08:12:07 -04:00
ae2b0beb45 feat(engine): expose JobPartGeometry for reading snapshot material
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>
2026-09-25 08:12:07 -04:00
ajandClaude Opus 5.5 6ab45e6de7 test(engine): seed spacing brute-force sampling deterministically
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>
2026-09-25 08:12:07 -04:00
029299ccf8 feat(engine): add NestJobResultBuilder for engine result assembly
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>
2026-09-25 07:55:10 -04:00
4cdb39870b feat(engine): expose NestPlateStock.WorkArea, Area and Fits
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>
2026-09-25 07:55:10 -04:00
ajandCodex 95833236cf docs(engine): clarify priority so scarce stock favors lower numbers
Document constructor and property semantics and cover priority zero versus nine on one sheet.

Co-Authored-By: Codex <noreply@openai.com>
2026-09-25 07:53:16 -04:00
ajandClaude Opus 5.5 1b5e1b14a6 fix: leave etch/scribe marks out of nesting geometry
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>
2026-09-25 06:44:29 -04:00
ajandClaude Opus 5.5 e3d10e95ae chore: move plugin engines to the OpenNest-Engines repo
Engines now live in https://git.thecozycat.net/aj/OpenNest-Engines (history
carried over) so they can be published independently, and so a copy of
OpenNest handed to a model for an engine-building run contains no
competing engines. Engines still load at runtime from an Engines/ folder
next to the app/benchmark output; nothing in the solution referenced them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 10:02:29 -04:00
aj 22d21924b8 feat(astra): add independent nesting engine and fix hole validation
Replace the Terra scaffold with an independent configuration-space contact placer and bounded stock-plan search. Include plugin tests, synthetic and DXF benchmark drivers, results, and deployment documentation.

Correct shared collision clipping and hole subtraction so curved-hole inserts validate consistently. Cover translated layouts, spacing violations, operand order, winding, and independent boolean-area comparisons.

Validation: 1,293 tests passed with 12 fixture skips; all 34 synthetic/generated and four DXF cases are valid and complete.
2026-09-24 00:14:36 -04:00
ajandClaude Sonnet 5 87f7ddad1a docs(engines): allow fill/pattern components in Terra and Qwen scaffolds
Reusable Fill/, BestFit, RectanglePacking and CirclePacking components are
fair game; whole-engine delegation and run-all-pick-best stay banned.
Improvements to shared components go in the engine's own project and are
reported, not applied to OpenNest.Core/OpenNest.Engine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:15:19 -04:00
aj 23fcec4b6c docs: compress README to a skimmable overview
Drop deep engine/validator/benchmark internals (covered by CLAUDE.md and
code comments) and per-option reference tables in favor of a quick-start
level guide.
2026-09-23 17:37:07 -04:00
aj d6d819bfed chore: remove tools directory
One-off helper utilities don't belong in the repo. PepNestExport moved to
/home/aj/src/PepNestExport as a standalone companion project; NestDxfJob and
StreamGravographJob are removed (recoverable from history).
2026-09-23 15:15:45 -04:00
aj c45909e397 Merge branch 'perf/fill-pipeline' 2026-09-23 14:30:46 -04:00
ajandClaude Opus 5.5 1c363504f5 perf(engine): key fill caches by source drawing so they hit across trials
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>
2026-09-23 14:29:43 -04:00
ajandClaude Opus 5.5 dc9e83ef17 perf(jobs): flatten validator contours once, at a 0.001 chord tolerance
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>
2026-09-23 14:29:43 -04:00
aj 1a7ed3c759 Merge branch 'refactor/engines-subfolder' 2026-09-23 11:42:39 -04:00
ajandClaude Opus 5.5 b122298f57 refactor(engines): move plugin engines into an Engines/ subfolder
Plugin engines (Opus55, Qwen, Terra) each add two projects at the repo
root, and more are coming; at a dozen they would outnumber the core
projects. They are also a different kind of thing: out-of-solution,
runtime-loaded plugins. Grouping them under Engines/ keeps the root
readable.

Engines/Directory.Build.props now holds the shared TFM, nullable and
implicit-usings settings and the OpenNest.Engine reference, so a new
engine's csproj is nearly empty. The tests/ compile exclusion lives in
Directory.Build.targets because a removal in .props runs before the SDK
adds its default Compile glob and has no effect.

Build-Engines.ps1 replaces the per-README manual build-and-copy steps
for deploying engines into the benchmark's runtime Engines/ folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 11:34:18 -04:00
ajandClaude Opus 5.5 2ecd94c705 Merge branch 'feat/pep-nest-export'
Opus55 NFP nesting engine and the PepNestExport tool for benchmarking
against PEP layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 11:09:17 -04:00
ajandClaude Opus 5.5 26c17a386b Merge branch 'refactor/clipper-geometry'
Spacing offsets move onto Clipper (ClipperBridge) for CPU preparation,
fixing spikes and inverted loops where features are narrower than the
spacing; Collision stays hand-rolled for the GPU path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 11:09:17 -04:00
ajandClaude Opus 5.5 4dd00c14c4 feat(tools): PepNestExport converts PEP nests into benchmark .nest files
Converts a PepApi year of PEP nests into .nest files that keep PEP's own
placements, so OpenNest.Benchmark can score PEP as its Baseline row.

Micro-joint tabs: PEP leaves tabs uncut by jumping them with a rapid, at a
contour's seam or partway along it (a cutout cut as two halves 0.02 apart).
The part still occupies that material, so open cut runs are chained end to
start across gaps up to 0.25 and bridged with a cut line, but only where
they close into a loop, so separate contours that lie close together (two
circles 0.25 apart) are never merged. Previously only seam tabs were closed,
leaving mid-contour tabbed cutouts open and the validator reading them as
garbage regions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 11:09:06 -04:00
ajandClaude Opus 5.5 5064340eb3 fix(benchmark): validate spacing at a 0.001 flattening tolerance
Conservative flattening circumscribes arcs, so at 0.01 the validator was up
to 0.01 too strict along curves: PEP's P260626-03 layout, exactly 0.25 apart
along an arc, failed with a 0.004 sliver. At 0.001 the worst error on either
side is 0.001. Validating the 26-job benchmark set with Opus55 went from
about 2 s to about 5 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 11:08:58 -04:00
ajandClaude Opus 5.5 062c7fa08f fix(geometry): tighten circumscribed flattening; stop padding the validator
The Clipper validator flagged valid Opus55 and PEP layouts (P260805-03,
P260626-03). Two causes:

- Arc.ToPoints(circumscribe) scales every vertex out by 1/cos(step/2),
  endpoints included, so a 0.03125 corner fillet flattened at 0.01 poked
  0.013 past the straight edges it meets. ClipperBridge now flattens itself:
  circumscribed arcs keep their endpoints on the arc and put interior
  vertices on tangent intersections, with the segment count chosen so the
  outward error stays within the tolerance. Arc.ToPoints is unchanged for
  its other callers.
- The conservative padding made a layout exactly at the spacing fail.
  NestValidator now uses OffsetForValidation: the same conservative
  flattening, round joins at a tenth of the tolerance, no padding. Its only
  leniency is that join chord error at convex corners.

With both, Opus55 is valid on all 26 benchmark jobs (25 before the
Clipper migration; the old failure was a spike artifact).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:40:59 -04:00
ajandClaude Opus 5.5 9af97c70b0 docs: document ClipperBridge, Clipper2 dependency and GPU-portable Collision
Spacing offsets now go through Clipper for CPU preparation while the
per-pair Collision test stays hand-rolled for a future GPU port; record
that split, which offset path each caller uses, and the missing Clipper2
entry in the NuGet list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:43:42 -04:00
ajandClaude Opus 5.5 01789c5929 fix(geometry): harden the arc-preserving per-entity offset
GetOffsetPerimeterEntities/GetOffsetPartEntities feed directional-distance
loops (FillLinear, Compactor, RotationSlideStrategy) that handle arcs
natively. Switching them to Clipper line output (plan option B) made
OpenNest.Tests run 48s -> 8m19s, Fill tests ~3x slower, and broke 20
exact-fit tests through tessellation and conservative padding, so they keep
the per-entity offset (option A), hardened:

- Arc, Circle and Line offsets are now side-symmetric. Right on a CCW arc
  shrank instead of growing, Right on a CW circle grew, and Right on a line
  offset to the left and reversed it. Only Left was used on hot paths, so
  this was latent (SimplifierViewer drew both tolerance bands on one side).
- Shape.OffsetEntity closes every gap between consecutive offset pieces:
  convex non-tangent line/arc corners get a round join about the original
  corner, lines across a collapsed fillet are mitered, and any other gap
  (concave arc corner, collapsed entity) is bridged with a line. Before,
  only line-line corners were joined, so a vertex could slip through.
- Zero-area spikes are left in place and documented: they lie inside the
  offset envelope, which is harmless for directional distance.
- OffsetOutward/OffsetInward become internal; PartGeometry is their only
  caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:43:02 -04:00
ajandClaude Opus 5.5 dceb5f7d18 test(geometry): cover Collision with ClipperBridge inputs; document GPU contract
Collision stays hand-rolled because it is the reference for a future GPU
kernel, but its inputs now come from Clipper region offsets. Pin down that
lines-only, round-join, 1e-4-precision polygons keep the contact and
part-in-part semantics: a neighbor inside a collapsed slot, a part inside
a hole that shrank by the spacing, and zero-spacing edge contact.

Document which steps are per-polygon preparation to cache and upload once,
and which are per-pair kernel-shaped work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:25:26 -04:00
ajandClaude Opus 5.5 10fe00d8ab refactor(geometry): delete RemoveSelfIntersections
Its callers now take Clipper region offsets, which never produce the
self-intersections it patched over (and it only caught proper crossings,
so spikes survived it anyway). Polygon.OffsetEntity was its last caller;
the override is required by Entity but has no callers, so it becomes a
Clipper miter offset that keeps the Left/Right semantics and the input
winding. FindCrossing, SplitAtCrossing, SegmentsIntersect and the static
CalculateArea helper go with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:24:37 -04:00
ajandClaude Opus 5.5 9b9386b029 refactor(geometry): move polygon offset callers onto ClipperBridge
Per-entity offsetting left spikes and inverted loops wherever a feature is
narrower than the spacing (1.nest), and RemoveSelfIntersections only
caught proper crossings. The callers that already flatten to polygons now
take a single Clipper region offset instead:

- PolygonHelper (BestFit) and PartBoundary use the conservative mode, which
  keeps their never-under-estimate guarantee. PartBoundary also keeps holes
  that appear when a perimeter curls back on itself.
- NestValidator offsets perimeter and cutouts in one region; Clipper drops
  collapsed cutouts, so the collapsed-or-flipped heuristic goes away.
- CutOff.IntersectPerimeter offsets through the bridge. The old
  OffsetEntity(Left) grew CW perimeters but shrank CCW ones, so with the
  plate's perimeter cache a cut-off ran through the part; slots narrower
  than twice the clearance now close up instead of leaving a gap.
- GetOffsetPartLines (3 overloads) and the AddOffset* helpers had no
  callers and are removed, as is EntityView's never-defined DRAW_OFFSET
  block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:21:48 -04:00
ajandClaude Opus 5.5 12f97474b7 refactor(geometry): remove dead concave NFP path
NoFitPolygon.Compute, its triangulate-and-union MinkowskiSum branch and
UnionPolygons had no callers; only ComputeConvex (NfpSlideStrategy) is used.
The Clipper path helpers they relied on now live in ClipperBridge.
ConvexDecomposition.Triangulate stays because Collision uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:13:59 -04:00
ajandClaude Opus 5.5 a6bc9d8be6 feat(geometry): add ClipperBridge for region offsetting
Offsetting entity by entity leaves spikes and inverted loops wherever a
feature is narrower than the spacing, and RemoveSelfIntersections only
catches proper crossings. ClipperBridge flattens a ShapeProfile into one
region (perimeter positive, cutouts negative) and inflates it in a single
Clipper pass with round joins, so narrow features collapse and holes that
close up disappear.

Conservative mode circumscribes perimeter arcs, inscribes cutout arcs and
pads the inflation by the chord tolerance, so the result never
under-estimates the spacing. It replaces the circumscribed-polygon
guarantee the BestFit/PartBoundary callers rely on.

Clipper stays confined to CPU preparation whose output is cached; the
per-pair Collision path remains hand-rolled for GPU portability.
LayoutPart's display offset now goes through the bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:12:34 -04:00
ajandClaude Opus 5.5 7964c87eb9 fix(geometry): fit arc centers to endpoints; Clipper offset for spacing display
PEP-exported programs carry arc centers that are not equidistant from the
start and end points (e.g. I0.03 on a 0.0598 chord). Building the arc from
the end radius left its start off the previous move's end, so contours
failed to chain. Project the center onto the chord's perpendicular bisector.

The Draw Offset display offset each entity separately, which left spikes and
inverted loops wherever a feature is narrower than the spacing (1.nest,
P260417-06). Inflate the flattened region with Clipper instead, which
collapses narrow features and drops holes that close up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:56:15 -04:00
ajandClaude Opus 5.5 45a59c6b02 feat(engine): Opus55 frontier-advance NFP nesting engine
Rename the OpenNest.Engine.Sonnet5 scaffold to OpenNest.Engine.Opus55 and
implement an independent whole-job INestingEngine (no built-in engine,
registry, or best-fit internals are called or copied).

- PartCatalog: snapshot perimeter -> polygon per allowed orientation, with
  adaptive chord tolerance and MBR-aligned rotations for Automatic parts.
- NoFitCache: spacing footprints and cached Clipper2 Minkowski NFPs (convex
  fast path; concave sweep plus both containment terms).
- FrontierPacker: per-(type, orientation) free regions (inner-fit rectangle
  minus NFPs), updated incrementally; gap-fill-largest, else least front
  advance per area^beta.
- Engine: look-ahead stock choice by estimated whole-job net area, six
  deterministic variants, tail re-plan of the last 1-3 sheets.
- Tests judged by OpenNest.Benchmark's NestValidator, including an NFP
  containment regression guard.

P260805-10.nest (219 parts), all 9 stock sizes: 219/219 valid, 27 sheets,
91.7% utilization, ~7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 05:10:50 -04:00
ajandClaude Sonnet 5 1c8305e8a1 feat(engine): scaffold independent nesting engine plugins for Sonnet5, Terra, Qwen
Three standalone INestingEngine plugin projects (Solve() unimplemented,
throws NotImplementedException), each outside OpenNest.sln and discovered
at runtime via NestingEngineRegistry.LoadPlugins, same pattern as
OpenNest.Engine.Aurora. Each README spells out that Solve() must be an
independently designed placement algorithm and must never call/wrap/select
over the built-in engines (StockLadderNestingEngine, FixedStrategyNestingEngine,
PlateNesterFactory, NestingEngineRegistry).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 22:06:47 -04:00
aj 828500f984 chore: replace CSharpier with dotnet format 2026-09-22 16:45:54 -04:00
aj 8e4735d25d [verified] add benchmark baseline and rotation fixes 2026-09-22 16:35:25 -04:00
aj 5bbb7b7461 Merge branch 'fix/benchmark-scoring' 2026-09-22 15:06:02 -04:00
ajandClaude Opus 5.5 57e9f625b6 fix(benchmark): rank by sheet cost so engines can't game the score
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>
2026-09-22 15:05:43 -04:00
aj e65f849c7a refactor(engine): remove legacy nesting engine surface 2026-09-22 14:25:46 -04:00
ajandClaude Opus 5.5 1be0904506 fix(mcp): make opennest server launchable on any machine
.mcp.json pointed at C:/Users/AJ/.claude/mcp/OpenNest.Mcp/run.cmd, a
profile path and wrapper script that only existed on one machine, so the
server failed with CONNECTION_CLOSED everywhere else. Launch the published
exe via ${USERPROFILE} instead.

Also route console logging to stderr: the host's default console logger
wrote to stdout, interleaving log lines with the JSON-RPC stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-22 13:02:28 -04:00
ajandClaude Sonnet 5 98c4929d14 Merge branch 'fix/pairs-overlap'
Fixes Part.Clone() double-counting baked drawing rotation, which caused
overlapping placements when tiling interlocking pairs for drawings needing
canonical-frame axis correction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:46:17 -04:00
ajandClaude Sonnet 5 014c071716 fix(core): stop Part.Clone from double-counting baked drawing rotation
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>
2026-09-22 10:46:11 -04:00
ajandClaude Sonnet 5 da1f8120f1 Merge branch 'fix/bestfit-overlap-test'
Fixes BestFit overlap check to agree with the geometry BuildParts
actually places, resolving BestFitOverlapTests.KeptPairs_NoOverlap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:58:46 -04:00
ajandClaude Sonnet 5 0512f3f84a fix(engine): check BestFit overlap in the same frame BuildParts places
PairEvaluator checked overlap on the raw candidate geometry before
applying the pair's OptimalRotation, but BestFitResult.BuildParts (and
everything downstream) rotates both parts by -OptimalRotation before
placing them. Re-tessellating a rotated arc at the overlap chord
tolerance samples different chord points than rotating an
already-tessellated polygon, so a few tangent-corner candidates came
out overlap-free in the raw frame but overlapping once actually
placed.

Move the landscape-normalization step before the overlap check and
rotate part1/part2 the same way BuildParts does before tessellating
and running Collision.HasOverlap, so Keep agrees with the geometry
that's actually placed. Fixes
OpenNest.Tests.BestFit.BestFitOverlapTests.KeptPairs_NoOverlap (was
failing 3/1082 candidates).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:57:40 -04:00
aj b14cb9d10a style(io): apply consistent formatting and remove unused usings
Whitespace/indentation cleanup (formatter pass) plus unused using
directives. No behavior change.
2026-09-22 09:51:18 -04:00
aj a0417a6077 fix(ui): avoid double-counting baseline rotation in part label placement
_labelPoint is computed from BaseDrawing.Program's current geometry,
which already carries BaseDrawing.Program.Rotation (non-zero for
canonical-frame drawings). BasePart.Rotation is cumulative from that
same baseline, so it must be re-applied net of the baseline already
baked into _labelPoint, mirroring CanonicalFrame.RebindToOriginal.
2026-09-22 09:51:15 -04:00
aj 56f39556d1 feat(engine): rebind live fill previews to the drawing's original frame
DefaultPlateFiller runs its search in a canonical (MBR-axis-aligned)
copy of the drawing. Intermediate progress reports — the Nesting
Progress dialog, PlateView's active-parts overlay — were showing that
transient canonical orientation instead of the drawing's real one.

FillContext.OriginalDrawing carries the pre-canonicalization drawing
through the pipeline; ReportProgress rebinds reported parts to it via
CanonicalFrame.RebindToOriginal before they reach the UI. Uses a
shallow list copy rather than per-part Part.Clone() — Clone() re-derives
its target rotation from BaseDrawing.Program.Rotation + Rotation, which
would double-count the canonical drawing's baked source angle.
2026-09-22 09:51:11 -04:00
aj 86f6c9efa1 refactor(engine): extract PairFiller candidate selection and remnant filling
Splits PairCandidateSelector (strip-mode candidate ranking) and
PairRemnantFiller (leftover L-shaped area fill) out of PairFiller into
their own classes. Pure extraction — logic is unchanged, just relocated
and given dedicated unit boundaries so each piece can be tested and
reasoned about on its own.
2026-09-22 09:51:06 -04:00
aj 6f250b5730 fix(geometry): correct overlap detection for real-world CNC shapes
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.
2026-09-22 09:51:02 -04:00
aj c28bc0da21 refactor(engine): select desktop nesting through app-scoped jobs engine
Replace the MainForm/AutoNestForm engine combo bindings and every
desktop NestEngineRegistry call: selection now lives in app-scoped
EngineSelection addressing jobs engines in NestingEngineRegistry, the
combo lists the four built-in strategies (StockLadder stays out per the
frozen UI decision), and Engines/ plug-ins load through
NestingEngineRegistry.LoadPlugins. Whole-job fill routes through the
public PlateFillService with the selected strategy and plate number;
interactive group fill (PlateView) and area fill (ActionFillArea) go
through PlateFillService with identical accept/cancel preview behavior.
Multi-plate and size-search orchestrators receive the explicit strategy.
No desktop code reads or writes process-global engine state.
2026-09-22 00:30:40 -04:00
aj 88966d118c refactor(engine): route console and MCP nesting through named job engines
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.
2026-09-22 00:30:03 -04:00
aj f36e124039 refactor(engine): thread explicit placement strategy through multi-plate orchestrators 2026-09-21 20:22:45 -04:00
aj 856dbfd8af refactor(engine): add public plate fill service 2026-09-21 19:51:35 -04:00
aj eb8fbec1aa refactor(engine): make jobs plate nesters filler-backed 2026-09-21 18:08:25 -04:00
aj 073ead9b79 refactor(engine): share jobs placement identity and progress mechanics 2026-09-21 16:36:24 -04:00
aj bb671cb176 refactor(engine): retarget ML runner to plate filler 2026-09-21 15:52:45 -04:00
aj 6bcea7919a refactor(engine): remove unused stripe engine hook 2026-09-21 15:43:15 -04:00
aj ed908b9706 refactor(engine): extract strip plate filler 2026-09-21 15:40:13 -04:00
aj e69ec07830 refactor(engine): extract default and remnant plate fillers 2026-09-21 15:27:03 -04:00
aj eafa0fab01 refactor(engine): extract plate fill orchestration 2026-09-21 15:01:00 -04:00
aj 9b9e510510 refactor(engine): extract progress reporting seam 2026-09-21 14:43:46 -04:00
aj 451841401a refactor(engine): align namespaces with directory layout under OpenNest.Engine
All 132 OpenNest.Engine source files now declare namespaces matching
their nested directories: Jobs/, Jobs/Placement/, Jobs/Adapters/,
Fill/, RectanglePacking/, CirclePacking/, and engine-root types moved
from 'OpenNest' to 'OpenNest.Engine'. RootNamespace updated accordingly.
Consumers (Api, Console, Mcp, Benchmark, Training, desktop app, tests)
gained the explicit usings the move requires; CLAUDE.md updated.
2026-09-21 13:18:34 -04:00
aj fd3375cde6 test(engine): golden-layout parity fixtures for all four strategies
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.
2026-09-21 12:17:24 -04:00
aj 42bbde7433 Merge branch 'fix/failing-tests-after-master-pull' 2026-09-21 11:07:49 -04:00
ajandClaude Sonnet 5 64d38c452c fix(engine): route restricted-rotation requirements to OrderedPlateNester
The legacy engine reads RotationStart == RotationEnd == 0 as
unconstrained and its Pairs/RectBestFit strategies rotate freely, so it
returned poses a Fixed/BoundedSweep RotationPolicy forbids and the job
validator threw. DefaultPlateNester now delegates such requests to the
policy-aware OrderedPlateNester.

Fixes RunAsync_FiniteStockExhaustion_PreservesUnplacedRequirementAndLockedRotation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:07:45 -04:00
ajandClaude Sonnet 5 630d514b0e fix(engine): don't drop the topmost part when no other drawing is waiting
RemnantFiller removes the topmost placed part to keep a clean rectangular
obstacle for the next drawing, but the envelope then walls that slot off,
so the part was lost for nothing (4 squares on a 9x9 plate became 3).
Only remove it while another drawing still has demand.

Fixes the mixed-stock NestRunner tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:07:45 -04:00
ajandClaude Sonnet 5 a764a70e52 fix(engine): make canonical-frame fills orientation-invariant
Part.Rotation is cumulative, so rebinding canonical parts with
CreateAtOrigin(original, p.Rotation) double-counted the drawing's own
rotation, and FromCanonical rotated each part about its Location, which
moved it off its slot and out of the work area. Add
CanonicalFrame.RebindToOriginal (rotation = part - original program
rotation, footprint aligned to the canonical part) and use it in the
three places that duplicated the old logic.

The MBR only fixes the frame modulo 90 degrees and nest results are not
90-degree symmetric (an L gave 56/43/42/42 parts by orientation).
CanonicalAngle.Compute now picks one of the four orientations from the
centroid offset; symmetric shapes keep the MBR orientation.

Fixes the three NestInvarianceTests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:07:44 -04:00
ajandClaude Sonnet 5 2a855139d2 fix(core): stop Program.BoundingBox including the origin
Min/max were seeded at 0, so any geometry not touching the origin got an
inflated box, and the first move only updated max (else-if). Rotated
canonical drawings are the common trigger: their origin ends up outside
the shape, which skewed Part bounds and bbox-based alignment.

Track the real extents and keep returning a zero box for empty programs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:07:44 -04:00
aj f23f0fa566 feat(tools): headless DXF+workbook job verification tool
NestDxfJob imports a DXF folder plus a quantity workbook, runs a
registered whole-job engine, and only publishes a nest that passes
quantity, bounds, overlap/spacing, and cut-only checks both before and
after save/reload. Writes import and validation reports; partial or
invalid results exit nonzero. Standalone project (not in the solution);
documented in the README.
2026-09-21 10:05:55 -04:00
aj 6f38c11383 feat(io): workbook part-quantity reader and cut-only import coverage
PartQuantityReader parses a Parts worksheet (Part Name / Qty Required)
with exact-name matching and strict rejection of invalid, fractional,
negative, or duplicate quantities. Cut-only import drops case-insensitive
ETCH/SCRIBE mark layers before geometry so bend detection cannot
regenerate them. Tests cover both.
2026-09-21 10:05:39 -04:00
aj f34b3c4449 perf(engine): bounds-based short-circuit in placement validation
Bounding-box distance is a conservative lower bound on true contour
clearance, so pairs far apart can skip the polygon Overlaps/Distance
checks without letting an overlap or spacing violation through. Also
gate the per-contour-pair BoundaryDistance work on a running minimum.
Validation is on the hot path for every candidate placement.
2026-09-21 10:05:39 -04:00
aj 365825bc5a fix(engine): shrink the requested axis in ShrinkFiller estimates
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.
2026-09-21 10:05:18 -04:00
ajandClaude Sonnet 5 02fc0ea3db chore: remove unused NFP nesting code
Delete OpenNest.Engine/Nfp (AutoNester, BottomLeftFill, NfpCache,
SimulatedAnnealing, INestOptimizer, PlacedPart, SequenceEntry), the Core
InnerFitPolygon, and the NestPhase.Nfp member. None had callers outside the
folder: console --autonest and MCP autonest_plate call engine.Nest(), not
AutoNester.

Drop the Nfp cases from NestPhaseExtensionsTests, fix the --autonest help
text, and update CLAUDE.md. NoFitPolygon stays; BestFit pair evaluation
still uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:20:30 -04:00
ajandClaude Sonnet 5 451876c02f fix(engine): stop PairFiller toggling the global fill-strategy registry
PairFiller.EvaluateCandidates wrapped its candidate loop in
FillStrategyRegistry.SetEnabled(...) and reset it with SetEnabled(null)
afterwards. Nothing inside that window re-enters the strategy pipeline
(EvaluateCandidate and FillRemnantBox only use FillLinear), so the
restriction had no effect on the running solve. It did mutate process-global
state: concurrent solves (e.g. benchmark --parallel) could observe the
narrowed strategy list, and the reset cleared any filter set elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:20:30 -04:00
ajandClaude Sonnet 5 5061b41a5d feat(benchmark): build jobs from DXF manifests and run solves in parallel
Benchmark jobs could only come from .nest files. A JSON manifest now lists
DXF files with quantities (plus sheet sizes, spacing, edge spacing, quadrant
and per-part allowRotation), imported through CadImporter. DXF paths resolve
relative to the manifest; sheet sizes are required from the manifest or
--sheet-sizes and are read in the DXFs' own units. Folder scans pick up
*.nest and *.manifest.json, and invalid manifests fail loudly.

BenchmarkRunner now runs (job x engine) solves concurrently, capped by
--parallel N (CLI default 3; --parallel 1 is sequential). Results are written
by index so report order is unchanged. Concurrent solves compete for cores,
so Time(ms) is only clean at --parallel 1; the run prints a note when N > 1.

Also fixes --output for manifest jobs, which tried to read the manifest as a
.nest to copy metadata from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 21:43:41 -04:00
aj e7cbd99db6 Separate WinForms tests so main test suite runs on Linux 2026-09-20 17:40:06 -04:00
aj 27684c3782 style: apply CSharpier formatting to files merged from arc-tangency branch 2026-09-20 16:54:16 -04:00
aj 54694f9b17 Merge branch 'chore/csharpier-sweep'
# Conflicts:
#	OpenNest.Core/Geometry/ArcFit.cs
#	OpenNest.Core/Geometry/GeometrySimplifier.cs
#	OpenNest.Posts.GravographIS/GravographISWriter.cs
#	OpenNest.Posts.GravographIS/NestPolylineExtractor.cs
2026-09-20 16:53:52 -04:00
aj de1248589a Merge branch 'feat/outer-profile-only'
# Conflicts:
#	CLAUDE.md
2026-09-20 16:52:18 -04:00
aj a9ebd8bb55 chore: add .git-blame-ignore-revs for the CSharpier sweep
Skip the formatting commit in git blame.
Enable with: git config blame.ignoreRevsFile .git-blame-ignore-revs
2026-09-20 16:42:45 -04:00
aj aec0523062 style: apply CSharpier formatting to all C# sources
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.
2026-09-20 16:41:50 -04:00
aj 8e6fa677fb chore: add .editorconfig and pinned CSharpier tool manifest
Mirrors CSharpier conventions (4-space indent, Allman braces,
System-first usings, 100-col wraps) so IDE auto-format and
'dotnet format' agree with the canonical formatter.
Usage: dotnet tool restore && dotnet csharpier format .
2026-09-20 16:04:04 -04:00
aj 589d341455 feat(io): conservative opt-in bend repair with tests and console CLI
Add OpenNest.IO/Bending/BendRepair: opt-in repair of unambiguous paired
ETCH/SCRIBE bend ticks, bounded to <=3.175 mm endpoint movement with
explicit source units. Cut geometry is never modified.

- CadImportOptions.BendRepair configures it; CadImportResult exposes
  per-bend BendRepairReports; CadImporter/Dxf wire it into import.
- Console: --repair-bends-mm <limit> --cad-units inches|mm prints
  per-bend reports for newly imported DXFs.
- New OpenNest.IO.Tests project (net8.0, synthetic DXFs, 30 tests)
  covering bend detection and repair, added to the solution.
- Update README.md and CLAUDE.md for the new pipeline and build/test
  instructions.
2026-09-20 15:22:46 -04:00
aj 1a05391d94 fix(engine): reject small corner overlaps in placement validation
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.
2026-09-20 13:40:45 -04:00
aj f5d27652f4 Merge branch 'fix/simplifier-arc-tangency'
Arc-tangency fitting fix in GeometrySimplifier/ArcFit plus layered
engrave/cut passes for the GravographIS post processor.
2026-09-19 12:02:08 -04:00
aj ea4bd836cd Add tested caller-stock StockLadder baseline with strict geometry validation 2026-09-19 11:24:36 -04:00
ajandClaude Sonnet 5 9b69c67572 fix(engine): use required spacing, not a sampled gap, when resequencing shrink-fill strips
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>
2026-09-19 10:04:33 -04:00
ajandClaude Sonnet 5 aa88eee484 fix(benchmark): use reference-based drawing identity in NestValidator, fix duplicate-sheet-size crash, document Engines/ plugin contract
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 08:44:09 -04:00
ajandClaude Sonnet 5 e0e3b96bed fix(benchmark): match drawing identity across materialization boundary in NestValidator
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 08:29:48 -04:00
aj 424ff15ebc docs: describe INestingEngine-based benchmark comparison 2026-09-19 08:25:57 -04:00