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); }