- Drop Circle.ToArcFrom (zero-sweep problem), keep Circle in shape and handle in ConvertShapeToMoves with full-circle ArcMove - Use point-distance tolerance for Arc.SplitAt instead of angle comparison to avoid wrap-around issues at 0/2pi - Simplify SplitAt return types to non-nullable tuple - Add ArgumentException guard in ReindexAt - Add throw for unexpected entity types in ConvertShapeToMoves - Document absolute coordinate convention and shared references - Clarify variable names for both replacement sites
6.4 KiB
Contour Re-Indexing Design
Overview
Add entity-splitting primitives and a Shape.ReindexAt method so that a closed contour can be reordered to start (and end) at an arbitrary point. Then wire this into ContourCuttingStrategy.Apply() to replace the NotImplementedException stubs.
All geometry additions live on existing classes in OpenNest.Geometry. The strategy wiring is a change to the existing ContourCuttingStrategy in OpenNest.CNC.CuttingStrategy.
Entity Splitting Primitives
Line.SplitAt(Vector point)
public (Line first, Line second) SplitAt(Vector point)
- Returns two lines:
StartPoint → pointandpoint → EndPoint. - If the point is at
StartPoint(withinTolerance.Epsilondistance),firstis null. - If the point is at
EndPoint(withinTolerance.Epsilondistance),secondis null. - The point is assumed to lie on the line (caller is responsible — it comes from
ClosestPointTo).
Arc.SplitAt(Vector point)
public (Arc first, Arc second) SplitAt(Vector point)
- Computes
splitAngle = Center.AngleTo(point), normalized viaAngle.NormalizeRad. - First arc: same center, radius, direction —
StartAngle → splitAngle. - Second arc: same center, radius, direction —
splitAngle → EndAngle. - Endpoint tolerance: compare
point.DistanceTo(arc.StartPoint())andpoint.DistanceTo(arc.EndPoint())rather than comparing angles directly. This avoids wrap-around issues at the 0/2π boundary. - If the point is at
StartPoint()(withinTolerance.Epsilondistance),firstis null. - If the point is at
EndPoint()(withinTolerance.Epsilondistance),secondis null.
Circle — no conversion needed
Circles are kept as-is in ReindexAt. The ConvertShapeToMoves method handles circles directly by emitting an ArcMove from the start point back to itself (a full circle), matching the existing ConvertGeometry.AddCircle pattern. This avoids the problem of constructing a "full-sweep arc" where StartAngle == EndAngle would produce zero sweep.
Shape.ReindexAt
public Shape ReindexAt(Vector point, Entity entity)
point: the start/end point for the reindexed contour (fromClosestPointTo).entity: the entity containingpoint(fromClosestPointTo'soutparameter).- Returns a new Shape (does not modify the original). The new shape shares entity references with the original for unsplit entities — callers must not mutate either.
- Throws
ArgumentExceptionifentityis not found inEntities.
Algorithm
-
If
entityis aCircle:- Return a new Shape with that single
Circleentity andpointstored forConvertShapeToMovesto use as the start point.
- Return a new Shape with that single
-
Find the index
iofentityinEntities. ThrowArgumentExceptionif not found. -
Split the entity at
point:Line→line.SplitAt(point)→(firstHalf, secondHalf)Arc→arc.SplitAt(point)→(firstHalf, secondHalf)
-
Build the new entity list (skip null entries):
secondHalf(if not null)Entities[i+1],Entities[i+2], ...,Entities[count-1](after the split)Entities[0],Entities[1], ...,Entities[i-1](before the split, wrapping around)firstHalf(if not null)
-
Return a new Shape with this entity list.
Edge Cases
- Point lands on entity boundary (start/end of an entity): one half of the split is null. The reordering still works — it just starts from the next full entity.
- Single-entity shape that is an Arc: split produces two arcs, reorder is just
[secondHalf, firstHalf]. - Single-entity Circle: handled by step 1 — kept as Circle, converted to a full-circle ArcMove in
ConvertShapeToMoves.
Wiring into ContourCuttingStrategy
Entity-to-ICode Conversion
Add a private method to ContourCuttingStrategy:
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
The startPoint parameter is needed for the Circle case (to know where the full-circle ArcMove starts).
Iterates shape.Entities and converts each to cutting moves using absolute coordinates (consistent with ConvertGeometry):
Line→LinearMove(line.EndPoint)Arc→ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW)Circle→ArcMove(startPoint, circle.Center, circle.Rotation)— full circle from start point back to itself, matchingConvertGeometry.AddCircle- Any other entity type → throw
InvalidOperationException
No RapidMove between entities — they are contiguous in a reindexed shape. The lead-in already positions the head at the shape's start point.
Replace NotImplementedException
In ContourCuttingStrategy.Apply(), replace the two throw new NotImplementedException(...) blocks:
Cutout loop (uses cutout shape variable):
var reindexed = cutout.ReindexAt(closestPt, entity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
Perimeter block (uses profile.Perimeter):
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
The full sequence for each contour becomes:
- Lead-in codes (rapid to pierce point, cutting moves to contour start)
- Contour body (reindexed entity moves from
ConvertShapeToMoves) - Lead-out codes (overcut moves away from contour)
MicrotabLeadOut Handling
When the lead-out is MicrotabLeadOut, the last cutting move must be trimmed by GapSize. This is a separate concern from re-indexing — stub it with a TODO comment for now. The trimming logic will shorten the last LinearMove or ArcMove in the contour body.
Files Modified
| File | Change |
|---|---|
OpenNest.Core/Geometry/Line.cs |
Add SplitAt(Vector) method |
OpenNest.Core/Geometry/Arc.cs |
Add SplitAt(Vector) method |
OpenNest.Core/Geometry/Shape.cs |
Add ReindexAt(Vector, Entity) method |
OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs |
Add ConvertShapeToMoves, replace NotImplementedException blocks |
Out of Scope
- MicrotabLeadOut trimming (trim last move by gap size — stubbed with TODO)
- Tab insertion (inserting tab codes mid-contour — already stubbed)
- Lead-in editor UI (interactive start point selection — separate feature)
- Contour re-indexing for open shapes (only closed contours supported)