refactor: two-pass lead-in placement in ContourCuttingStrategy
Resolve lead-in points by walking backward through cutting order (from perimeter outward) so each lead-in faces the next cutout to be cut rather than pointing back at the previous lead-out. Extract EmitContour and EmitScribeContours to eliminate duplicated cutout/perimeter logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,113 +8,101 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
public CuttingParameters Parameters { get; set; }
|
public CuttingParameters Parameters { get; set; }
|
||||||
|
|
||||||
|
private record ContourEntry(Shape Shape, Vector Point, Entity Entity);
|
||||||
|
|
||||||
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
||||||
{
|
{
|
||||||
var exitPoint = approachPoint;
|
|
||||||
var entities = partProgram.ToGeometry();
|
var entities = partProgram.ToGeometry();
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||||
|
|
||||||
// Separate scribe/etch entities — they don't get lead-ins or kerf
|
|
||||||
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
|
|
||||||
var profile = new ShapeProfile(entities);
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
// Find closest point on perimeter from exit point
|
// Forward pass: sequence cutouts nearest-neighbor from perimeter
|
||||||
var perimeterPoint = profile.Perimeter.ClosestPointTo(exitPoint, out var perimeterEntity);
|
var perimeterPoint = profile.Perimeter.ClosestPointTo(approachPoint, out _);
|
||||||
|
|
||||||
// Chain cutouts by nearest-neighbor from perimeter point, then reverse
|
|
||||||
// so farthest cutouts are cut first, nearest-to-perimeter cut last
|
|
||||||
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
||||||
orderedCutouts.Reverse();
|
orderedCutouts.Reverse();
|
||||||
|
|
||||||
// Build output program: scribe first, cutouts second, perimeter last
|
// Backward pass: walk from perimeter back through cutting order
|
||||||
|
// so each lead-in faces the next cutout to be cut, not the previous
|
||||||
|
var cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterPoint);
|
||||||
|
|
||||||
var result = new Program(Mode.Absolute);
|
var result = new Program(Mode.Absolute);
|
||||||
var currentPoint = exitPoint;
|
|
||||||
|
|
||||||
// Emit scribe/etch contours first (no lead-ins, no kerf)
|
EmitScribeContours(result, scribeEntities);
|
||||||
if (scribeEntities.Count > 0)
|
|
||||||
{
|
|
||||||
var scribeShapes = ShapeBuilder.GetShapes(scribeEntities);
|
|
||||||
foreach (var scribe in scribeShapes)
|
|
||||||
{
|
|
||||||
var startPt = GetShapeStartPoint(scribe);
|
|
||||||
result.Codes.Add(new RapidMove(startPt));
|
|
||||||
result.Codes.AddRange(ConvertShapeToMoves(scribe, startPt, LayerType.Scribe));
|
|
||||||
currentPoint = startPt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var cutout in orderedCutouts)
|
foreach (var entry in cutoutEntries)
|
||||||
{
|
EmitContour(result, entry.Shape, entry.Point, entry.Entity);
|
||||||
var contourType = DetectContourType(cutout);
|
|
||||||
var closestPt = cutout.ClosestPointTo(currentPoint, out var entity);
|
|
||||||
var normal = ComputeNormal(closestPt, entity, contourType);
|
|
||||||
var winding = DetermineWinding(cutout);
|
|
||||||
|
|
||||||
var leadIn = SelectLeadIn(contourType);
|
|
||||||
var leadOut = SelectLeadOut(contourType);
|
|
||||||
|
|
||||||
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
|
||||||
leadIn = ClampLeadInForCircle(leadIn, circle, closestPt, normal);
|
|
||||||
|
|
||||||
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
|
|
||||||
var reindexed = cutout.ReindexAt(closestPt, entity);
|
|
||||||
|
|
||||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
|
||||||
{
|
|
||||||
var trimmed = TrimShapeForTab(reindexed, closestPt, Parameters.TabConfig.Size);
|
|
||||||
result.Codes.AddRange(ConvertShapeToMoves(trimmed, closestPt));
|
|
||||||
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
|
|
||||||
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPoint = closestPt;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastCutPoint = exitPoint;
|
|
||||||
|
|
||||||
// Perimeter last
|
// Perimeter last
|
||||||
{
|
var lastRefPoint = cutoutEntries.Count > 0 ? cutoutEntries[cutoutEntries.Count - 1].Point : approachPoint;
|
||||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
var perimeterPt = profile.Perimeter.ClosestPointTo(lastRefPoint, out var perimeterEntity);
|
||||||
lastCutPoint = perimeterPt;
|
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
|
||||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
|
||||||
var winding = DetermineWinding(profile.Perimeter);
|
|
||||||
|
|
||||||
var leadIn = SelectLeadIn(ContourType.External);
|
|
||||||
var leadOut = SelectLeadOut(ContourType.External);
|
|
||||||
|
|
||||||
result.Codes.AddRange(leadIn.Generate(perimeterPt, normal, winding));
|
|
||||||
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
|
|
||||||
|
|
||||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
|
||||||
{
|
|
||||||
var trimmed = TrimShapeForTab(reindexed, perimeterPt, Parameters.TabConfig.Size);
|
|
||||||
result.Codes.AddRange(ConvertShapeToMoves(trimmed, perimeterPt));
|
|
||||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
|
|
||||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to incremental mode to match the convention used by
|
|
||||||
// the rest of the system (rendering, bounding box, drag, etc.).
|
|
||||||
result.Mode = Mode.Incremental;
|
result.Mode = Mode.Incremental;
|
||||||
|
|
||||||
return new CuttingResult
|
return new CuttingResult
|
||||||
{
|
{
|
||||||
Program = result,
|
Program = result,
|
||||||
LastCutPoint = lastCutPoint
|
LastCutPoint = perimeterPt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
|
||||||
|
{
|
||||||
|
var entries = new ContourEntry[cutouts.Count];
|
||||||
|
var currentPoint = startPoint;
|
||||||
|
|
||||||
|
// Walk backward through cutting order (from perimeter outward)
|
||||||
|
// so each cutout's lead-in point faces the next cutout to be cut
|
||||||
|
for (var i = cutouts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var closestPt = cutouts[i].ClosestPointTo(currentPoint, out var entity);
|
||||||
|
entries[i] = new ContourEntry(cutouts[i], closestPt, entity);
|
||||||
|
currentPoint = closestPt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<ContourEntry>(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
||||||
|
{
|
||||||
|
var contourType = forceType ?? DetectContourType(shape);
|
||||||
|
var normal = ComputeNormal(point, entity, contourType);
|
||||||
|
var winding = DetermineWinding(shape);
|
||||||
|
|
||||||
|
var leadIn = SelectLeadIn(contourType);
|
||||||
|
var leadOut = SelectLeadOut(contourType);
|
||||||
|
|
||||||
|
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
||||||
|
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
|
||||||
|
|
||||||
|
program.Codes.AddRange(leadIn.Generate(point, normal, winding));
|
||||||
|
|
||||||
|
var reindexed = shape.ReindexAt(point, entity);
|
||||||
|
|
||||||
|
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||||
|
reindexed = TrimShapeForTab(reindexed, point, Parameters.TabConfig.Size);
|
||||||
|
|
||||||
|
program.Codes.AddRange(ConvertShapeToMoves(reindexed, point));
|
||||||
|
program.Codes.AddRange(leadOut.Generate(point, normal, winding));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitScribeContours(Program program, List<Entity> scribeEntities)
|
||||||
|
{
|
||||||
|
if (scribeEntities.Count == 0) return;
|
||||||
|
|
||||||
|
var shapes = ShapeBuilder.GetShapes(scribeEntities);
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var startPt = GetShapeStartPoint(shape);
|
||||||
|
program.Codes.Add(new RapidMove(startPt));
|
||||||
|
program.Codes.AddRange(ConvertShapeToMoves(shape, startPt, LayerType.Scribe));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
||||||
{
|
{
|
||||||
var remaining = new List<Shape>(cutouts);
|
var remaining = new List<Shape>(cutouts);
|
||||||
|
|||||||
Reference in New Issue
Block a user