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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user