From 6a30828fad171d74be937767ce1dbf5577ae909b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 9 Apr 2026 10:33:55 -0400 Subject: [PATCH] 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) --- .../CuttingStrategy/ContourCuttingStrategy.cs | 76 ++++++++++++++++--- OpenNest.Core/Part.cs | 7 +- OpenNest.Engine/LeadInAssigner.cs | 47 +++++++++++- OpenNest.Engine/PlateProcessor.cs | 50 +++++++++--- 4 files changed, 156 insertions(+), 24 deletions(-) diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs index 34ac3bb..7416b70 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs @@ -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(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); diff --git a/OpenNest.Core/Part.cs b/OpenNest.Core/Part.cs index 01484d0..19024b4 100644 --- a/OpenNest.Core/Part.cs +++ b/OpenNest.Core/Part.cs @@ -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; diff --git a/OpenNest.Engine/LeadInAssigner.cs b/OpenNest.Engine/LeadInAssigner.cs index ae09aa5..4150f1a 100644 --- a/OpenNest.Engine/LeadInAssigner.cs +++ b/OpenNest.Engine/LeadInAssigner.cs @@ -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 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; } } } diff --git a/OpenNest.Engine/PlateProcessor.cs b/OpenNest.Engine/PlateProcessor.cs index adc639c..325968f 100644 --- a/OpenNest.Engine/PlateProcessor.cs +++ b/OpenNest.Engine/PlateProcessor.cs @@ -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(sequenced.Count); var cutAreas = new List(); - 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); }