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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
Task 6 of the whole-job engine API: adapt the public NestRequest/NestRunner/
NestResponse surface to delegate to the whole-job runner instead of a manual
quantity loop.
- NestRequest: optional explicit Plates stock list (null keeps the legacy
unlimited SheetSize fallback; empty list means no available stock),
optional per-part Id (derived as part-{index} when absent), and an explicit
PlacementStrategy that takes precedence over the legacy Strategy.
- NestRequestPlate: one physical-stock type (id, size, quantity, spacing,
quadrant).
- NestRunner: imports each DXF once, propagates priority/rotation constraints,
runs a single NestJobRunner solve, materializes ID/pose placements exactly
once, and reports aggregate utilization as total placed part area over total
physical sheet area.
- NestResponse: exposes status, stop reason, part fulfillment, stock usage,
and plate-to-stock mapping; .nestquote save/load gains a schema version and
reports completion as unknown for old archives lacking fulfillment metadata.
- Tests: extend the Api request/runner/persistence suites for legacy SheetSize,
explicit mixed finite stock, stock exhaustion, weighted utilization, old
archive loading, and new-archive round trips.
Verification: cross-compiles clean on net8.0-windows (Linux). The Api tests
require a Windows runner (net8.0-windows) and are NOT executed here; the
delegated engine logic is covered by the 70-test net8.0 Engine.Tests suite
(committed in Task 5). Windows runtime verification remains outstanding.
GeometrySimplifier/ArcFit now fit arcs that pass exactly through run
endpoints while balancing tangency error between trusted and estimated
directions, fixing arcs that previously bulged or broke tangent
continuity at fillet/compound-curve junctions.
GravographIS post processor gains per-layer (engrave/cut) tool passes
via a new GravographISPostConfig, so ENGRAVE/ETCH-tagged geometry runs
as a separate scribe pass with its own feed/depth and an operator
pause before the cut pass (spring-floated spindle needs a tool swap).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ChrFont, a reader for Gravograph .CHR engraving fonts, plus UI to
convert placed text into engraved geometry in the CAD converter.
The .CHR files are obfuscated with a single-byte XOR. Different
GravoStyle releases use different keys (0x2F in older versions, 0xCF in
the 7000 series, and others across the font library), so the key is
auto-detected from byte 1 of the file: the font name is ASCII stored as
UTF-16LE, so the high byte of its first character is 0x00 in plaintext
and the raw byte equals the key. This reads every font in a GravoStyle
install regardless of version, not just one hardcoded key.
UI: right-clicking a text item in EntityView raises TextConvertRequested;
CadConverterForm renders it via ChrFont with H/V alignment and adds the
result on an ENGRAVE layer.
Tests use Xunit.SkippableFact and a gitignored test-config.json so the
suite points at a local .CHR file without committing proprietary assets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parts that already overlap the moving group are now excluded from the
obstacle list so they don't block the push direction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that filling an L-shaped part produces consistent counts
regardless of the orientation it was imported at, and that all
placed parts stay within the plate work area.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Arcs with sweep angles smaller than Tolerance.Epsilon were treated as
full circles by IsBetweenRad's shortcut check, causing UpdateBounds to
expand the bounding box to Center ± Radius. This made zoom-to-fit zoom
out far beyond the actual part extents.
Skip cardinal angle expansion when sweep is near-zero so the bounding
box uses only the arc's start/end points.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ellipses with extrusion direction Z=-1 had their parametric direction
reversed, causing the curve to appear mirrored. Negate start/end
parameters when Normal.Z < 0 to correct the minor-axis traversal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parameterize side count so users can generate any regular n-gon
(n>=3). Width remains the inscribed-circle diameter, preserving n=8
behavior; circumradius derives as Width / (2*cos(pi/n)).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pulls the rapid-walk logic (sub-program unwrapping, first-pierce lookup,
incremental-vs-absolute handling, first-rapid skipping) out of
PlateRenderer.DrawRapids into a reusable RapidEnumerator in Core so it
can be unit-tested and reused outside the renderer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ArcToLineClosestDistance used geometric closest-point as a proxy for
directional push distance, which are fundamentally different queries.
The heuristic could overestimate the safe push distance when an arc
faces an inclined line, causing the Compactor to over-push parts into
overlapping positions.
Replace with analytical computation: for each arc/line pair, solve
dt/dθ = 0 to find the two critical angles where the directional
distance is stationary, evaluate both (if within the arc's angular
span), and fire a ray to verify the hit is within the line segment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Splits that cross an interior cutout previously merged physically
disconnected strips into one drawing and drew cut lines through the hole.
The region boundary now spans full feature-edge extents (trimmed against
cutout polygons) and line entities are Liang-Barsky clipped, so multi-split
edges work. Arcs are properly clipped at region boundaries via iterative
split-at-intersection so circles that straddle a split contribute to both
sides. AssemblePieces groups a region's entities into connected closed
loops and nests holes by bbox-pre-check + vertex-in-polygon containment,
so one region can emit multiple drawings when a cutout fully spans it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PlateSizes holds standard mill sheet sizes (48x96 through 96x240) and
exposes Recommend() which snaps small layouts to an increment and
rounds larger layouts up to the nearest fitting sheet. Plate.SnapToStandardSize
applies the result while preserving long-axis orientation, and the
existing Ctrl+P "Resize to Fit" menu in EditNestForm now calls it
instead of the simple round-up AutoSize.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces NominalPipeSize (double) with PipeSize (string), PipeClearance (double), and Blind (bool). GetDrawing cuts a center bore at pipeOD + PipeClearance unless Blind is true or PipeSize is unknown/null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>