feat: add PlateProcessor orchestrator
This commit is contained in:
120
OpenNest.Engine/PlateProcessor.cs
Normal file
120
OpenNest.Engine/PlateProcessor.cs
Normal file
@@ -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<ProcessedPart>(sequenced.Count);
|
||||
var cutAreas = new List<Shape>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
132
OpenNest.Tests/PlateProcessorTests.cs
Normal file
132
OpenNest.Tests/PlateProcessorTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user