feat: optimize external lead-in placement using next-part pierce points

External lead-ins now sit on the line between the last internal cutout
and the next part's first pierce point, minimizing rapid travel. Cutout
sequencing starts from the bounding box corner opposite the origin and
iterates 3 times to converge the perimeter lead-in and internal sequence.
LeadInAssigner and PlateProcessor both use a two-pass approach: first
pass collects pierce points, second pass refines with next-part knowledge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 10:33:55 -04:00
parent 786b6e2e88
commit 6a30828fad
4 changed files with 156 additions and 24 deletions

View File

@@ -11,6 +11,11 @@ namespace OpenNest.CNC.CuttingStrategy
private record ContourEntry(Shape Shape, Vector Point, Entity Entity);
public CuttingResult Apply(Program partProgram, Vector approachPoint)
{
return Apply(partProgram, approachPoint, Vector.Invalid);
}
public CuttingResult Apply(Program partProgram, Vector approachPoint, Vector nextPartStart)
{
var entities = partProgram.ToGeometry();
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
@@ -20,14 +25,43 @@ namespace OpenNest.CNC.CuttingStrategy
var profile = new ShapeProfile(entities);
// Forward pass: sequence cutouts nearest-neighbor from perimeter
var perimeterPoint = profile.Perimeter.ClosestPointTo(approachPoint, out _);
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
// Start from the bounding box corner opposite the origin (max X, max Y)
var bbox = entities.GetBoundingBox();
var startCorner = new Vector(bbox.Right, bbox.Top);
// Initial pass: sequence cutouts from bbox corner
var seedPoint = startCorner;
var orderedCutouts = SequenceCutouts(profile.Cutouts, seedPoint);
orderedCutouts.Reverse();
// 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 perimeterSeed = profile.Perimeter.ClosestPointTo(seedPoint, out _);
var cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterSeed);
Vector perimeterPt;
Entity perimeterEntity;
if (!double.IsNaN(nextPartStart.X) && cutoutEntries.Count > 0)
{
// Iterate: each pass refines the perimeter lead-in which changes
// the internal sequence which changes the last cutout position
for (var iter = 0; iter < 3; iter++)
{
var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point;
perimeterSeed = FindPerimeterIntersection(profile.Perimeter, lastCutoutPt, nextPartStart, out _);
orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed);
orderedCutouts.Reverse();
cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterSeed);
}
var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point;
perimeterPt = FindPerimeterIntersection(profile.Perimeter, finalLastCutout, nextPartStart, out perimeterEntity);
}
else
{
var perimeterRef = cutoutEntries.Count > 0 ? cutoutEntries[0].Point : approachPoint;
perimeterPt = profile.Perimeter.ClosestPointTo(perimeterRef, out perimeterEntity);
}
var result = new Program(Mode.Absolute);
@@ -36,9 +70,6 @@ namespace OpenNest.CNC.CuttingStrategy
foreach (var entry in cutoutEntries)
EmitContour(result, entry.Shape, entry.Point, entry.Entity);
// Perimeter last
var lastRefPoint = cutoutEntries.Count > 0 ? cutoutEntries[cutoutEntries.Count - 1].Point : approachPoint;
var perimeterPt = profile.Perimeter.ClosestPointTo(lastRefPoint, out var perimeterEntity);
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
result.Mode = Mode.Incremental;
@@ -187,6 +218,33 @@ namespace OpenNest.CNC.CuttingStrategy
return new List<ContourEntry>(entries);
}
private static Vector FindPerimeterIntersection(Shape perimeter, Vector lastCutout, Vector nextPartStart, out Entity entity)
{
var ray = new Line(lastCutout, nextPartStart);
if (perimeter.Intersects(ray, out var pts) && pts.Count > 0)
{
// Pick the intersection closest to the last cutout
var best = pts[0];
var bestDist = best.DistanceTo(lastCutout);
for (var i = 1; i < pts.Count; i++)
{
var dist = pts[i].DistanceTo(lastCutout);
if (dist < bestDist)
{
best = pts[i];
bestDist = dist;
}
}
return perimeter.ClosestPointTo(best, out entity);
}
// Fallback: closest point on perimeter to the last cutout
return perimeter.ClosestPointTo(lastCutout, out entity);
}
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
{
var contourType = forceType ?? DetectContourType(shape);

View File

@@ -62,10 +62,15 @@ namespace OpenNest
public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint)
{
ApplyLeadIns(parameters, approachPoint, Geometry.Vector.Invalid);
}
public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint, Vector nextPartStart)
{
preLeadInRotation = Rotation;
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
var result = strategy.Apply(Program, approachPoint);
var result = strategy.Apply(Program, approachPoint, nextPartStart);
Program = result.Program;
CuttingParameters = parameters;
HasManualLeadIns = true;

View File

@@ -1,5 +1,7 @@
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Engine.Sequencing;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine
@@ -15,14 +17,28 @@ namespace OpenNest.Engine
return;
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
var currentPoint = PlateHelper.GetExitPoint(plate);
var exitPoint = PlateHelper.GetExitPoint(plate);
foreach (var sp in sequenced)
// Pass 1: assign lead-ins to establish pierce points
var piercePoints = AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: null);
// Pass 2: re-assign with knowledge of next part's start point
AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: piercePoints);
}
private Vector[] AssignPass(List<SequencedPart> sequenced, CuttingParameters parameters,
Vector exitPoint, Vector[] nextPiercePoints)
{
var piercePoints = new Vector[sequenced.Count];
var currentPoint = exitPoint;
for (var i = 0; i < sequenced.Count; i++)
{
var part = sp.Part;
var part = sequenced[i].Part;
if (part.LeadInsLocked)
{
piercePoints[i] = GetPiercePoint(part);
currentPoint = part.Location;
continue;
}
@@ -31,10 +47,33 @@ namespace OpenNest.Engine
part.RemoveLeadIns();
var localApproach = currentPoint - part.Location;
part.ApplyLeadIns(parameters, localApproach);
if (nextPiercePoints != null && i + 1 < sequenced.Count)
{
var nextStart = nextPiercePoints[i + 1] - part.Location;
part.ApplyLeadIns(parameters, localApproach, nextStart);
}
else
{
part.ApplyLeadIns(parameters, localApproach);
}
piercePoints[i] = GetPiercePoint(part);
currentPoint = part.Location;
}
return piercePoints;
}
private static Vector GetPiercePoint(Part part)
{
foreach (var code in part.Program.Codes)
{
if (code is CNC.Motion motion)
return motion.EndPoint + part.Location;
}
return part.Location;
}
}
}

View File

@@ -17,15 +17,38 @@ namespace OpenNest.Engine
public PlateProcessingResult Process(Plate plate)
{
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
var exitPoint = PlateHelper.GetExitPoint(plate);
// Pass 1: process each part to collect pierce points
var piercePoints = new Vector[sequenced.Count];
var currentPoint = exitPoint;
for (var i = 0; i < sequenced.Count; i++)
{
var part = sequenced[i].Part;
if (!part.HasManualLeadIns && CuttingStrategy != null)
{
var localApproach = ToPartLocal(currentPoint, part);
var result = CuttingStrategy.Apply(part.Program, localApproach);
piercePoints[i] = ToPlateSpace(GetProgramStartPoint(result.Program), part);
currentPoint = ToPlateSpace(result.LastCutPoint, part);
}
else
{
piercePoints[i] = ToPlateSpace(GetProgramStartPoint(part.Program), part);
currentPoint = ToPlateSpace(GetProgramEndPoint(part.Program), part);
}
}
// Pass 2: re-process with next part's start point for perimeter lead-in refinement
var results = new List<ProcessedPart>(sequenced.Count);
var cutAreas = new List<Shape>();
var currentPoint = PlateHelper.GetExitPoint(plate);
currentPoint = exitPoint;
foreach (var sp in sequenced)
for (var i = 0; i < sequenced.Count; i++)
{
var part = sp.Part;
// Compute approach point in part-local space
var part = sequenced[i].Part;
var localApproach = ToPartLocal(currentPoint, part);
Program processedProgram;
@@ -33,7 +56,18 @@ namespace OpenNest.Engine
if (!part.HasManualLeadIns && CuttingStrategy != null)
{
var cuttingResult = CuttingStrategy.Apply(part.Program, localApproach);
CuttingResult cuttingResult;
if (i + 1 < sequenced.Count)
{
var nextStart = ToPartLocal(piercePoints[i + 1], part);
cuttingResult = CuttingStrategy.Apply(part.Program, localApproach, nextStart);
}
else
{
cuttingResult = CuttingStrategy.Apply(part.Program, localApproach);
}
processedProgram = cuttingResult.Program;
lastCutLocal = cuttingResult.LastCutPoint;
}
@@ -43,11 +77,9 @@ namespace OpenNest.Engine
lastCutLocal = GetProgramEndPoint(part.Program);
}
// Pierce point: program start point in plate space
var pierceLocal = GetProgramStartPoint(processedProgram);
var piercePoint = ToPlateSpace(pierceLocal, part);
// Plan rapid from currentPoint to pierce point
var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas);
results.Add(new ProcessedPart
@@ -57,12 +89,10 @@ namespace OpenNest.Engine
RapidPath = rapidPath
});
// Update cut areas with part perimeter
var perimeter = GetPartPerimeter(part);
if (perimeter != null)
cutAreas.Add(perimeter);
// Update current point to last cut point in plate space
currentPoint = ToPlateSpace(lastCutLocal, part);
}