diff --git a/OpenNest.Engine/PlateProcessor.cs b/OpenNest.Engine/PlateProcessor.cs new file mode 100644 index 0000000..9601933 --- /dev/null +++ b/OpenNest.Engine/PlateProcessor.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Engine.RapidPlanning; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; + +namespace OpenNest.Engine +{ + public class PlateProcessor + { + public IPartSequencer Sequencer { get; set; } + public ContourCuttingStrategy CuttingStrategy { get; set; } + public IRapidPlanner RapidPlanner { get; set; } + + public PlateResult Process(Plate plate) + { + var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate); + var results = new List(sequenced.Count); + var cutAreas = new List(); + var currentPoint = PlateHelper.GetExitPoint(plate); + + foreach (var sp in sequenced) + { + var part = sp.Part; + + // Compute approach point in part-local space + var localApproach = ToPartLocal(currentPoint, part); + + Program processedProgram; + Vector lastCutLocal; + + if (!part.HasManualLeadIns && CuttingStrategy != null) + { + var cuttingResult = CuttingStrategy.Apply(part.Program, localApproach); + processedProgram = cuttingResult.Program; + lastCutLocal = cuttingResult.LastCutPoint; + } + else + { + processedProgram = part.Program; + lastCutLocal = GetProgramEndPoint(part.Program); + } + + // Pierce point: program start point in plate space + var pierceLocal = GetProgramStartPoint(part.Program); + var piercePoint = ToPlateSpace(pierceLocal, part); + + // Plan rapid from currentPoint to pierce point + var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas); + + results.Add(new ProcessedPart + { + Part = part, + ProcessedProgram = processedProgram, + 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); + } + + return new PlateResult { Parts = results }; + } + + private static Vector ToPartLocal(Vector platePoint, Part part) + { + return platePoint - part.Location; + } + + private static Vector ToPlateSpace(Vector localPoint, Part part) + { + return localPoint + part.Location; + } + + private static Vector GetProgramStartPoint(Program program) + { + if (program.Codes.Count == 0) + return Vector.Zero; + + var first = program.Codes[0]; + if (first is Motion motion) + return motion.EndPoint; + + return Vector.Zero; + } + + private static Vector GetProgramEndPoint(Program program) + { + for (var i = program.Codes.Count - 1; i >= 0; i--) + { + if (program.Codes[i] is Motion motion) + return motion.EndPoint; + } + + return Vector.Zero; + } + + private static Shape GetPartPerimeter(Part part) + { + var entities = part.Program.ToGeometry(); + if (entities == null || entities.Count == 0) + return null; + + var profile = new ShapeProfile(entities); + var perimeter = profile.Perimeter; + if (perimeter == null || perimeter.Entities.Count == 0) + return null; + + perimeter.Offset(part.Location); + return perimeter; + } + } +} diff --git a/OpenNest.Tests/PlateProcessorTests.cs b/OpenNest.Tests/PlateProcessorTests.cs new file mode 100644 index 0000000..73d5a98 --- /dev/null +++ b/OpenNest.Tests/PlateProcessorTests.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using System.Linq; +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Engine; +using OpenNest.Engine.RapidPlanning; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Tests; + +public class PlateProcessorTests +{ + private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y, size: 2); + + [Fact] + public void Process_ReturnsAllParts() + { + var plate = new Plate(60, 120); + plate.Parts.Add(MakePartAt(10, 10)); + plate.Parts.Add(MakePartAt(30, 30)); + plate.Parts.Add(MakePartAt(50, 50)); + + var processor = new PlateProcessor + { + Sequencer = new RightSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Equal(3, result.Parts.Count); + } + + [Fact] + public void Process_PreservesSequenceOrder() + { + var plate = new Plate(60, 120); + var left = MakePartAt(5, 10); + var right = MakePartAt(50, 10); + plate.Parts.Add(left); + plate.Parts.Add(right); + + var processor = new PlateProcessor + { + Sequencer = new RightSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Same(right, result.Parts[0].Part); + Assert.Same(left, result.Parts[1].Part); + } + + [Fact] + public void Process_SkipsCuttingStrategy_WhenManualLeadIns() + { + var plate = new Plate(60, 120); + var part = MakePartAt(10, 10); + part.HasManualLeadIns = true; + plate.Parts.Add(part); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + CuttingStrategy = new ContourCuttingStrategy + { + Parameters = new CuttingParameters() + }, + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Same(part.Program, result.Parts[0].ProcessedProgram); + } + + [Fact] + public void Process_DoesNotMutatePart() + { + var plate = new Plate(60, 120); + var part = MakePartAt(10, 10); + var originalProgram = part.Program; + plate.Parts.Add(part); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Same(originalProgram, part.Program); + } + + [Fact] + public void Process_NoCuttingStrategy_PassesProgramThrough() + { + var plate = new Plate(60, 120); + var part = MakePartAt(10, 10); + plate.Parts.Add(part); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Same(part.Program, result.Parts[0].ProcessedProgram); + } + + [Fact] + public void Process_EmptyPlate_ReturnsEmptyResult() + { + var plate = new Plate(60, 120); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Empty(result.Parts); + } +}