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>
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>
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>
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>
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>
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.
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>
Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.
Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).
Formatting is now enforceable: dotnet csharpier check . passes.
The witness-probe overlap test missed small corner intersections: its
candidate points (crossing-edge midpoints and vertex-centroid midpoints)
can all land on a part boundary or outside the intersection, so two 10x10
parts at (0,0) and (9,9) with zero spacing were accepted despite sharing
a 1x1 unit of material.
Route the overlap decision through Collision, which clips triangulated
polygons and keeps only positive-area regions, catching corner overlaps,
containment, and coincident poses while legal edge/corner contact stays
legal. Collision's hole subtraction was conservative (partially-clipped
triangles were kept whole), so a part inside another part's cutout could
false-positive depending on triangulation alignment; subtract holes
exactly instead: a piece outside a convex hole triangle is the union of
its clips against each edge's outside half-space.
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>
Change net8.0-windows to net8.0 in Core, IO, and Engine projects so
the libraries can be consumed in Docker containers running on Linux.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Duplicate circle entities at the same location inflated pierce counts
and cut pricing (e.g. SULLYS-035 showed 9 pierces instead of 8).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ellipse-to-arc conversion creates tiny floating-point gaps (~0.00002")
between consecutive arc segments. ShapeBuilder chains these with
ChainTolerance (0.0001"), but ConvertGeometry checked gaps with Epsilon
(0.00001"). Gaps between these thresholds generated spurious rapid moves
that broke GraphicsPath figures, causing diagonal fill artifacts from
GDI+'s implicit figure closing.
Root cause fix: align ConvertGeometry's gap check with ShapeBuilder's
ChainTolerance so precision gaps are absorbed instead of generating rapids.
Defense-in-depth: GraphicsHelper no longer breaks figures at near-zero
rapids, protecting against any programs with residual tiny rapids.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Near-zero-sweep arcs with large radius (e.g. from ellipse converter) have
nearly-coincident start/end points. Downstream code (ConvertProgram, Program
BoundingBox) treats coincident start/end as a full 360° circle, inflating the
bounding box and rendering wrong geometry. Emit a LinearMove when sweep is
negligible — geometrically equivalent and avoids the ambiguity. Also fix the
ellipse converter to produce lines instead of degenerate arcs at the source.
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>
Shape library drawings now get descriptive names based on their
parameters (e.g. "Rectangle 12x6", "Circle 8 Dia") instead of generic
type names, preventing silent duplicates in the DrawingCollection
HashSet. Added a Shape Library button to the Drawings tab toolbar
and removed separators between toolbar buttons for a cleaner look.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tabs were being applied to internal cutouts and circle holes, which is
incorrect — only the external perimeter should be tabbed. Restructured
the Tabs panel to use radio buttons ("Tab all parts" vs "Auto-tab by
smallest dimension") so the two modes are clearly mutually exclusive
instead of the confusing implicit override behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
Program.Offset only adjusted Motion codes, so subprogram calls kept
their original offsets after a part was translated. Apply the offset
to SubProgramCall.Offset too so hole subprograms follow the part.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the full Cincinnati material/etch library list as the committed
default config (seeded into Posts/ on build only when no runtime config
exists), plus a Selected Library override in the PropertyGrid backed by
a TypeConverter that populates from MaterialLibraries. MainForm calls
the new IPostProcessorNestAware hook before showing the config so the
dropdown opens preselected to the best match by nest material and
nearest thickness.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the material textbox on EditNestInfoForm with a combobox whose
items are aggregated from every loaded post processor that implements the
new IMaterialProvidingPostProcessor interface. CincinnatiPostProcessor
exposes its configured MaterialLibraries entries. Free-text entry still
works so custom materials remain usable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Open (non-closed) shapes like scribe lines or partial cuts don't have
a meaningful pierce point or closing segment, so applying lead-in/out
would produce invalid toolpaths. Skip the lead-in/out logic and emit
them as raw contours in both Apply and ApplySingle paths.
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>
Merge the near-identical Left/Right and Up/Down pruning loops into a
single loop that selects the perpendicular axis via IsHorizontalDirection().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ShapeProfile.NormalizeEntities called Shape.Reverse() which flipped arc
directions on the original entity objects shared with the CAD view. Switching
to the Program tab and back would leave arcs reversed. Clone entities before
normalizing so the originals stay untouched.
Adds abstract Entity.Clone() with implementations on Line, Arc, Circle,
Polygon, and Shape (deep-clones children). Also adds CloneAll() extension
and replaces manual duplication in PartGeometry.CopyEntitiesAtLocation and
ProgramEditorControl.CloneEntity.
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>