feat: add PlateProcessor for per-part lead-in assignment and cut sequencing
Three-stage pipeline: IPartSequencer → ContourCuttingStrategy → IRapidPlanner wired by PlateProcessor. 6 sequencing strategies, 2 rapid planners, 31 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,9 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
public CuttingParameters Parameters { get; set; }
|
public CuttingParameters Parameters { get; set; }
|
||||||
|
|
||||||
public Program Apply(Program partProgram, Plate plate)
|
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
||||||
{
|
{
|
||||||
var exitPoint = GetExitPoint(plate);
|
var exitPoint = approachPoint;
|
||||||
var entities = partProgram.ToGeometry();
|
var entities = partProgram.ToGeometry();
|
||||||
var profile = new ShapeProfile(entities);
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
@@ -44,9 +44,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
currentPoint = closestPt;
|
currentPoint = closestPt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastCutPoint = exitPoint;
|
||||||
|
|
||||||
// Perimeter last
|
// Perimeter last
|
||||||
{
|
{
|
||||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
||||||
|
lastCutPoint = perimeterPt;
|
||||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
||||||
var winding = DetermineWinding(profile.Perimeter);
|
var winding = DetermineWinding(profile.Perimeter);
|
||||||
|
|
||||||
@@ -60,21 +63,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return new CuttingResult
|
||||||
}
|
|
||||||
|
|
||||||
private Vector GetExitPoint(Plate plate)
|
|
||||||
{
|
|
||||||
var w = plate.Size.Width;
|
|
||||||
var l = plate.Size.Length;
|
|
||||||
|
|
||||||
return plate.Quadrant switch
|
|
||||||
{
|
{
|
||||||
1 => new Vector(w, l), // Q1 origin BottomLeft -> exit TopRight
|
Program = result,
|
||||||
2 => new Vector(0, l), // Q2 origin BottomRight -> exit TopLeft
|
LastCutPoint = lastCutPoint
|
||||||
3 => new Vector(0, 0), // Q3 origin TopRight -> exit BottomLeft
|
|
||||||
4 => new Vector(w, 0), // Q4 origin TopLeft -> exit BottomRight
|
|
||||||
_ => new Vector(w, l)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public readonly struct CuttingResult
|
||||||
|
{
|
||||||
|
public Program Program { get; init; }
|
||||||
|
public Vector LastCutPoint { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,8 @@ namespace OpenNest
|
|||||||
|
|
||||||
public Program Program { get; private set; }
|
public Program Program { get; private set; }
|
||||||
|
|
||||||
|
public bool HasManualLeadIns { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the rotation of the part in radians.
|
/// Gets the rotation of the part in radians.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine
|
||||||
|
{
|
||||||
|
public class PlateResult
|
||||||
|
{
|
||||||
|
public List<ProcessedPart> Parts { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ProcessedPart
|
||||||
|
{
|
||||||
|
public Part Part { get; init; }
|
||||||
|
public Program ProcessedProgram { get; init; }
|
||||||
|
public RapidPath RapidPath { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public class DirectRapidPlanner : IRapidPlanner
|
||||||
|
{
|
||||||
|
public RapidPath Plan(Vector from, Vector to, IReadOnlyList<Shape> cutAreas)
|
||||||
|
{
|
||||||
|
var travelLine = new Line(from, to);
|
||||||
|
|
||||||
|
foreach (var cutArea in cutAreas)
|
||||||
|
{
|
||||||
|
if (TravelLineIntersectsShape(travelLine, cutArea))
|
||||||
|
{
|
||||||
|
return new RapidPath
|
||||||
|
{
|
||||||
|
HeadUp = true,
|
||||||
|
Waypoints = new List<Vector>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RapidPath
|
||||||
|
{
|
||||||
|
HeadUp = false,
|
||||||
|
Waypoints = new List<Vector>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TravelLineIntersectsShape(Line travelLine, Shape shape)
|
||||||
|
{
|
||||||
|
foreach (var entity in shape.Entities)
|
||||||
|
{
|
||||||
|
if (entity is Line edge)
|
||||||
|
{
|
||||||
|
if (travelLine.Intersects(edge, out _))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public interface IRapidPlanner
|
||||||
|
{
|
||||||
|
RapidPath Plan(Vector from, Vector to, IReadOnlyList<Shape> cutAreas);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public readonly struct RapidPath
|
||||||
|
{
|
||||||
|
public bool HeadUp { get; init; }
|
||||||
|
public List<Vector> Waypoints { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public class SafeHeightRapidPlanner : IRapidPlanner
|
||||||
|
{
|
||||||
|
public RapidPath Plan(Vector from, Vector to, IReadOnlyList<Shape> cutAreas)
|
||||||
|
{
|
||||||
|
return new RapidPath
|
||||||
|
{
|
||||||
|
HeadUp = true,
|
||||||
|
Waypoints = new List<Vector>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class AdvancedSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
private readonly SequenceParameters _parameters;
|
||||||
|
|
||||||
|
public AdvancedSequencer(SequenceParameters parameters)
|
||||||
|
{
|
||||||
|
_parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
if (parts.Count == 0)
|
||||||
|
return new List<SequencedPart>();
|
||||||
|
|
||||||
|
var exit = PlateHelper.GetExitPoint(plate);
|
||||||
|
|
||||||
|
// Group parts into rows by Y proximity
|
||||||
|
var rows = GroupIntoRows(parts, _parameters.MinDistanceBetweenRowsColumns);
|
||||||
|
|
||||||
|
// Sort rows bottom-to-top (ascending Y)
|
||||||
|
rows.Sort((a, b) => a.RowY.CompareTo(b.RowY));
|
||||||
|
|
||||||
|
// Determine initial direction based on exit point
|
||||||
|
var leftToRight = exit.X > plate.Size.Width * 0.5;
|
||||||
|
|
||||||
|
var result = new List<SequencedPart>(parts.Count);
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var sorted = leftToRight
|
||||||
|
? row.Parts.OrderBy(p => p.BoundingBox.Center.X).ToList()
|
||||||
|
: row.Parts.OrderByDescending(p => p.BoundingBox.Center.X).ToList();
|
||||||
|
|
||||||
|
foreach (var p in sorted)
|
||||||
|
result.Add(new SequencedPart { Part = p });
|
||||||
|
|
||||||
|
if (_parameters.AlternateRowsColumns)
|
||||||
|
leftToRight = !leftToRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PartRow> GroupIntoRows(IReadOnlyList<Part> parts, double minDistance)
|
||||||
|
{
|
||||||
|
// Sort parts by Y center
|
||||||
|
var sorted = parts
|
||||||
|
.OrderBy(p => p.BoundingBox.Center.Y)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var rows = new List<PartRow>();
|
||||||
|
|
||||||
|
foreach (var part in sorted)
|
||||||
|
{
|
||||||
|
var y = part.BoundingBox.Center.Y;
|
||||||
|
var placed = false;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
if (System.Math.Abs(y - row.RowY) <= minDistance + Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
row.Parts.Add(part);
|
||||||
|
placed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!placed)
|
||||||
|
{
|
||||||
|
var row = new PartRow(y);
|
||||||
|
row.Parts.Add(part);
|
||||||
|
rows.Add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PartRow
|
||||||
|
{
|
||||||
|
public double RowY { get; }
|
||||||
|
public List<Part> Parts { get; } = new List<Part>();
|
||||||
|
|
||||||
|
public PartRow(double rowY)
|
||||||
|
{
|
||||||
|
RowY = rowY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class BottomSideSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
return parts
|
||||||
|
.OrderBy(p => p.Location.Y)
|
||||||
|
.ThenBy(p => p.Location.X)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class EdgeStartSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
// Plate(width, length) stores Size with Width/Length swapped internally.
|
||||||
|
// Reconstruct the logical plate box using the BoundingBox origin and the
|
||||||
|
// corrected extents: Size.Length = X-extent, Size.Width = Y-extent.
|
||||||
|
var origin = plate.BoundingBox(false);
|
||||||
|
var plateBox = new OpenNest.Geometry.Box(
|
||||||
|
origin.X, origin.Y,
|
||||||
|
plate.Size.Length,
|
||||||
|
plate.Size.Width);
|
||||||
|
|
||||||
|
return parts
|
||||||
|
.OrderBy(p => MinEdgeDistance(p.BoundingBox.Center, plateBox))
|
||||||
|
.ThenBy(p => p.Location.X)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double MinEdgeDistance(OpenNest.Geometry.Vector center, OpenNest.Geometry.Box plateBox)
|
||||||
|
{
|
||||||
|
var distLeft = center.X - plateBox.Left;
|
||||||
|
var distRight = plateBox.Right - center.X;
|
||||||
|
var distBottom = center.Y - plateBox.Bottom;
|
||||||
|
var distTop = plateBox.Top - center.Y;
|
||||||
|
|
||||||
|
return System.Math.Min(System.Math.Min(distLeft, distRight), System.Math.Min(distBottom, distTop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public readonly struct SequencedPart
|
||||||
|
{
|
||||||
|
public Part Part { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPartSequencer
|
||||||
|
{
|
||||||
|
List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class LeastCodeSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
private readonly int _maxIterations;
|
||||||
|
|
||||||
|
public LeastCodeSequencer(int maxIterations = 100)
|
||||||
|
{
|
||||||
|
_maxIterations = maxIterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
if (parts.Count == 0)
|
||||||
|
return new List<SequencedPart>();
|
||||||
|
|
||||||
|
var exit = PlateHelper.GetExitPoint(plate);
|
||||||
|
var ordered = NearestNeighbor(parts, exit);
|
||||||
|
TwoOpt(ordered, exit);
|
||||||
|
|
||||||
|
var result = new List<SequencedPart>(ordered.Count);
|
||||||
|
foreach (var p in ordered)
|
||||||
|
result.Add(new SequencedPart { Part = p });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Part> NearestNeighbor(IReadOnlyList<Part> parts, OpenNest.Geometry.Vector exit)
|
||||||
|
{
|
||||||
|
var remaining = new List<Part>(parts);
|
||||||
|
var ordered = new List<Part>(parts.Count);
|
||||||
|
|
||||||
|
var current = exit;
|
||||||
|
while (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
var bestIdx = 0;
|
||||||
|
var bestDist = Distance(current, Center(remaining[0]));
|
||||||
|
|
||||||
|
for (var i = 1; i < remaining.Count; i++)
|
||||||
|
{
|
||||||
|
var d = Distance(current, Center(remaining[i]));
|
||||||
|
if (d < bestDist - Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
bestDist = d;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = remaining[bestIdx];
|
||||||
|
ordered.Add(next);
|
||||||
|
remaining.RemoveAt(bestIdx);
|
||||||
|
current = Center(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TwoOpt(List<Part> ordered, OpenNest.Geometry.Vector exit)
|
||||||
|
{
|
||||||
|
var n = ordered.Count;
|
||||||
|
if (n < 3)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var iter = 0; iter < _maxIterations; iter++)
|
||||||
|
{
|
||||||
|
var improved = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < n - 1; i++)
|
||||||
|
{
|
||||||
|
for (var j = i + 1; j < n; j++)
|
||||||
|
{
|
||||||
|
var before = RouteDistance(ordered, exit, i, j);
|
||||||
|
Reverse(ordered, i, j);
|
||||||
|
var after = RouteDistance(ordered, exit, i, j);
|
||||||
|
|
||||||
|
if (after < before - Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
improved = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Revert
|
||||||
|
Reverse(ordered, i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!improved)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the total distance of the route starting from exit through all parts.
|
||||||
|
/// Only the segment around the reversed segment [i..j] needs to be checked,
|
||||||
|
/// but here we compute the full route cost for correctness.
|
||||||
|
/// </summary>
|
||||||
|
private static double RouteDistance(List<Part> ordered, OpenNest.Geometry.Vector exit, int i, int j)
|
||||||
|
{
|
||||||
|
// Full route distance: exit -> ordered[0] -> ... -> ordered[n-1]
|
||||||
|
var total = 0.0;
|
||||||
|
var prev = exit;
|
||||||
|
foreach (var p in ordered)
|
||||||
|
{
|
||||||
|
var c = Center(p);
|
||||||
|
total += Distance(prev, c);
|
||||||
|
prev = c;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Reverse(List<Part> list, int i, int j)
|
||||||
|
{
|
||||||
|
while (i < j)
|
||||||
|
{
|
||||||
|
var tmp = list[i];
|
||||||
|
list[i] = list[j];
|
||||||
|
list[j] = tmp;
|
||||||
|
i++;
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenNest.Geometry.Vector Center(Part part)
|
||||||
|
{
|
||||||
|
return part.BoundingBox.Center;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double Distance(OpenNest.Geometry.Vector a, OpenNest.Geometry.Vector b)
|
||||||
|
{
|
||||||
|
var dx = b.X - a.X;
|
||||||
|
var dy = b.Y - a.Y;
|
||||||
|
return System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class LeftSideSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
return parts
|
||||||
|
.OrderBy(p => p.Location.X)
|
||||||
|
.ThenBy(p => p.Location.Y)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public static class PartSequencerFactory
|
||||||
|
{
|
||||||
|
public static IPartSequencer Create(SequenceParameters parameters)
|
||||||
|
{
|
||||||
|
return parameters.Method switch
|
||||||
|
{
|
||||||
|
SequenceMethod.RightSide => new RightSideSequencer(),
|
||||||
|
SequenceMethod.LeftSide => new LeftSideSequencer(),
|
||||||
|
SequenceMethod.BottomSide => new BottomSideSequencer(),
|
||||||
|
SequenceMethod.EdgeStart => new EdgeStartSequencer(),
|
||||||
|
SequenceMethod.LeastCode => new LeastCodeSequencer(),
|
||||||
|
SequenceMethod.Advanced => new AdvancedSequencer(parameters),
|
||||||
|
_ => throw new NotSupportedException(
|
||||||
|
$"Sequence method '{parameters.Method}' is not supported.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
internal static class PlateHelper
|
||||||
|
{
|
||||||
|
public static Vector GetExitPoint(Plate plate)
|
||||||
|
{
|
||||||
|
var w = plate.Size.Width;
|
||||||
|
var l = plate.Size.Length;
|
||||||
|
|
||||||
|
return plate.Quadrant switch
|
||||||
|
{
|
||||||
|
1 => new Vector(w, l),
|
||||||
|
2 => new Vector(0, l),
|
||||||
|
3 => new Vector(0, 0),
|
||||||
|
4 => new Vector(w, 0),
|
||||||
|
_ => new Vector(w, l)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class RightSideSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
return parts
|
||||||
|
.OrderByDescending(p => p.Location.X)
|
||||||
|
.ThenBy(p => p.Location.Y)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class CuttingResultTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CuttingResult_StoresValues()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(1, 2)));
|
||||||
|
var point = new Vector(3, 4);
|
||||||
|
|
||||||
|
var result = new CuttingResult { Program = pgm, LastCutPoint = point };
|
||||||
|
|
||||||
|
Assert.Same(pgm, result.Program);
|
||||||
|
Assert.Equal(3, result.LastCutPoint.X);
|
||||||
|
Assert.Equal(4, result.LastCutPoint.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.5.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class PartFlagTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HasManualLeadIns_DefaultsFalse()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
var drawing = new Drawing("test", pgm);
|
||||||
|
var part = new Part(drawing);
|
||||||
|
|
||||||
|
Assert.False(part.HasManualLeadIns);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HasManualLeadIns_CanBeSet()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
var drawing = new Drawing("test", pgm);
|
||||||
|
var part = new Part(drawing);
|
||||||
|
|
||||||
|
part.HasManualLeadIns = true;
|
||||||
|
|
||||||
|
Assert.True(part.HasManualLeadIns);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.RapidPlanning;
|
||||||
|
|
||||||
|
public class DirectRapidPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NoCutAreas_ReturnsHeadDown()
|
||||||
|
{
|
||||||
|
var planner = new DirectRapidPlanner();
|
||||||
|
var result = planner.Plan(new Vector(0, 0), new Vector(10, 10), new List<Shape>());
|
||||||
|
|
||||||
|
Assert.False(result.HeadUp);
|
||||||
|
Assert.Empty(result.Waypoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearPath_ReturnsHeadDown()
|
||||||
|
{
|
||||||
|
var planner = new DirectRapidPlanner();
|
||||||
|
|
||||||
|
var cutArea = new Shape();
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(50, 0), new Vector(50, 10)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(50, 10), new Vector(60, 10)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(60, 10), new Vector(60, 0)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(60, 0), new Vector(50, 0)));
|
||||||
|
|
||||||
|
var result = planner.Plan(
|
||||||
|
new Vector(0, 0), new Vector(10, 10),
|
||||||
|
new List<Shape> { cutArea });
|
||||||
|
|
||||||
|
Assert.False(result.HeadUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BlockedPath_ReturnsHeadUp()
|
||||||
|
{
|
||||||
|
var planner = new DirectRapidPlanner();
|
||||||
|
|
||||||
|
var cutArea = new Shape();
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(5, 0), new Vector(5, 20)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(5, 20), new Vector(6, 20)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(6, 20), new Vector(6, 0)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(6, 0), new Vector(5, 0)));
|
||||||
|
|
||||||
|
var result = planner.Plan(
|
||||||
|
new Vector(0, 10), new Vector(10, 10),
|
||||||
|
new List<Shape> { cutArea });
|
||||||
|
|
||||||
|
Assert.True(result.HeadUp);
|
||||||
|
Assert.Empty(result.Waypoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.RapidPlanning;
|
||||||
|
|
||||||
|
public class SafeHeightRapidPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AlwaysReturnsHeadUp()
|
||||||
|
{
|
||||||
|
var planner = new SafeHeightRapidPlanner();
|
||||||
|
var from = new Vector(10, 10);
|
||||||
|
var to = new Vector(50, 50);
|
||||||
|
var cutAreas = new List<Shape>();
|
||||||
|
|
||||||
|
var result = planner.Plan(from, to, cutAreas);
|
||||||
|
|
||||||
|
Assert.True(result.HeadUp);
|
||||||
|
Assert.Empty(result.Waypoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnsHeadUp_EvenWithCutAreas()
|
||||||
|
{
|
||||||
|
var planner = new SafeHeightRapidPlanner();
|
||||||
|
var from = new Vector(0, 0);
|
||||||
|
var to = new Vector(10, 10);
|
||||||
|
|
||||||
|
var shape = new Shape();
|
||||||
|
shape.Entities.Add(new Line(new Vector(5, 0), new Vector(5, 20)));
|
||||||
|
var cutAreas = new List<Shape> { shape };
|
||||||
|
|
||||||
|
var result = planner.Plan(from, to, cutAreas);
|
||||||
|
|
||||||
|
Assert.True(result.HeadUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class AdvancedSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GroupsIntoRows_NoAlternate()
|
||||||
|
{
|
||||||
|
var plate = new Plate(100, 100);
|
||||||
|
var row1a = MakePartAt(10, 10);
|
||||||
|
var row1b = MakePartAt(30, 10);
|
||||||
|
var row2a = MakePartAt(10, 50);
|
||||||
|
var row2b = MakePartAt(30, 50);
|
||||||
|
plate.Parts.Add(row1a);
|
||||||
|
plate.Parts.Add(row1b);
|
||||||
|
plate.Parts.Add(row2a);
|
||||||
|
plate.Parts.Add(row2b);
|
||||||
|
|
||||||
|
var parameters = new SequenceParameters
|
||||||
|
{
|
||||||
|
Method = SequenceMethod.Advanced,
|
||||||
|
MinDistanceBetweenRowsColumns = 5.0,
|
||||||
|
AlternateRowsColumns = false
|
||||||
|
};
|
||||||
|
var sequencer = new AdvancedSequencer(parameters);
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(row1a, result[0].Part);
|
||||||
|
Assert.Same(row1b, result[1].Part);
|
||||||
|
Assert.Same(row2a, result[2].Part);
|
||||||
|
Assert.Same(row2b, result[3].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SerpentineAlternatesDirection()
|
||||||
|
{
|
||||||
|
var plate = new Plate(100, 100);
|
||||||
|
var r1Left = MakePartAt(10, 10);
|
||||||
|
var r1Right = MakePartAt(30, 10);
|
||||||
|
var r2Left = MakePartAt(10, 50);
|
||||||
|
var r2Right = MakePartAt(30, 50);
|
||||||
|
plate.Parts.Add(r1Left);
|
||||||
|
plate.Parts.Add(r1Right);
|
||||||
|
plate.Parts.Add(r2Left);
|
||||||
|
plate.Parts.Add(r2Right);
|
||||||
|
|
||||||
|
var parameters = new SequenceParameters
|
||||||
|
{
|
||||||
|
Method = SequenceMethod.Advanced,
|
||||||
|
MinDistanceBetweenRowsColumns = 5.0,
|
||||||
|
AlternateRowsColumns = true
|
||||||
|
};
|
||||||
|
var sequencer = new AdvancedSequencer(parameters);
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(r1Left, result[0].Part);
|
||||||
|
Assert.Same(r1Right, result[1].Part);
|
||||||
|
Assert.Same(r2Right, result[2].Part);
|
||||||
|
Assert.Same(r2Left, result[3].Part);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class DirectionalSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
private static Plate MakePlate(params Part[] parts) => TestHelpers.MakePlate(60, 120, parts);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RightSide_SortsXDescending()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(10, 5);
|
||||||
|
var b = MakePartAt(30, 5);
|
||||||
|
var c = MakePartAt(20, 5);
|
||||||
|
var plate = MakePlate(a, b, c);
|
||||||
|
|
||||||
|
var sequencer = new RightSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(b, result[0].Part);
|
||||||
|
Assert.Same(c, result[1].Part);
|
||||||
|
Assert.Same(a, result[2].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LeftSide_SortsXAscending()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(10, 5);
|
||||||
|
var b = MakePartAt(30, 5);
|
||||||
|
var c = MakePartAt(20, 5);
|
||||||
|
var plate = MakePlate(a, b, c);
|
||||||
|
|
||||||
|
var sequencer = new LeftSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(a, result[0].Part);
|
||||||
|
Assert.Same(c, result[1].Part);
|
||||||
|
Assert.Same(b, result[2].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BottomSide_SortsYAscending()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(5, 20);
|
||||||
|
var b = MakePartAt(5, 5);
|
||||||
|
var c = MakePartAt(5, 10);
|
||||||
|
var plate = MakePlate(a, b, c);
|
||||||
|
|
||||||
|
var sequencer = new BottomSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(b, result[0].Part);
|
||||||
|
Assert.Same(c, result[1].Part);
|
||||||
|
Assert.Same(a, result[2].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RightSide_TiesBrokenByPerpendicularAxis()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(10, 20);
|
||||||
|
var b = MakePartAt(10, 5);
|
||||||
|
var plate = MakePlate(a, b);
|
||||||
|
|
||||||
|
var sequencer = new RightSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(b, result[0].Part);
|
||||||
|
Assert.Same(a, result[1].Part);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class EdgeStartSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SortsByDistanceFromNearestEdge()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var edgePart = MakePartAt(1, 1);
|
||||||
|
var centerPart = MakePartAt(25, 55);
|
||||||
|
var midPart = MakePartAt(10, 10);
|
||||||
|
plate.Parts.Add(edgePart);
|
||||||
|
plate.Parts.Add(centerPart);
|
||||||
|
plate.Parts.Add(midPart);
|
||||||
|
|
||||||
|
var sequencer = new EdgeStartSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(edgePart, result[0].Part);
|
||||||
|
Assert.Same(midPart, result[1].Part);
|
||||||
|
Assert.Same(centerPart, result[2].Part);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class LeastCodeSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NearestNeighbor_FromExitPoint()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var farPart = MakePartAt(5, 5);
|
||||||
|
var nearPart = MakePartAt(55, 115);
|
||||||
|
plate.Parts.Add(farPart);
|
||||||
|
plate.Parts.Add(nearPart);
|
||||||
|
|
||||||
|
var sequencer = new LeastCodeSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
// nearPart is closer to exit point, should come first
|
||||||
|
Assert.Same(nearPart, result[0].Part);
|
||||||
|
Assert.Same(farPart, result[1].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreservesAllParts()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
plate.Parts.Add(MakePartAt(i * 5, i * 10));
|
||||||
|
|
||||||
|
var sequencer = new LeastCodeSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Equal(10, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoOpt_ImprovesSolution()
|
||||||
|
{
|
||||||
|
var plate = new Plate(100, 100);
|
||||||
|
var a = MakePartAt(90, 90);
|
||||||
|
var b = MakePartAt(10, 80);
|
||||||
|
var c = MakePartAt(80, 10);
|
||||||
|
var d = MakePartAt(5, 5);
|
||||||
|
plate.Parts.Add(a);
|
||||||
|
plate.Parts.Add(b);
|
||||||
|
plate.Parts.Add(c);
|
||||||
|
plate.Parts.Add(d);
|
||||||
|
|
||||||
|
var sequencer = new LeastCodeSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Equal(4, result.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class PartSequencerFactoryTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(SequenceMethod.RightSide, typeof(RightSideSequencer))]
|
||||||
|
[InlineData(SequenceMethod.LeftSide, typeof(LeftSideSequencer))]
|
||||||
|
[InlineData(SequenceMethod.BottomSide, typeof(BottomSideSequencer))]
|
||||||
|
[InlineData(SequenceMethod.EdgeStart, typeof(EdgeStartSequencer))]
|
||||||
|
[InlineData(SequenceMethod.LeastCode, typeof(LeastCodeSequencer))]
|
||||||
|
[InlineData(SequenceMethod.Advanced, typeof(AdvancedSequencer))]
|
||||||
|
public void Create_ReturnsCorrectType(SequenceMethod method, Type expectedType)
|
||||||
|
{
|
||||||
|
var parameters = new SequenceParameters { Method = method };
|
||||||
|
var sequencer = PartSequencerFactory.Create(parameters);
|
||||||
|
Assert.IsType(expectedType, sequencer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_RightSideAlt_Throws()
|
||||||
|
{
|
||||||
|
var parameters = new SequenceParameters { Method = SequenceMethod.RightSideAlt };
|
||||||
|
Assert.Throws<NotSupportedException>(() => PartSequencerFactory.Create(parameters));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
internal static class TestHelpers
|
||||||
|
{
|
||||||
|
public static Part MakePartAt(double x, double y, double size = 1)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
var drawing = new Drawing("test", pgm);
|
||||||
|
return new Part(drawing, new Vector(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Plate MakePlate(double width = 60, double length = 120, params Part[] parts)
|
||||||
|
{
|
||||||
|
var plate = new Plate(width, length);
|
||||||
|
foreach (var p in parts)
|
||||||
|
plate.Parts.Add(p);
|
||||||
|
return plate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNes
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Training", "OpenNest.Training\OpenNest.Training.csproj", "{249BF728-25DD-4863-8266-207ACD26E964}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Training", "OpenNest.Training\OpenNest.Training.csproj", "{249BF728-25DD-4863-8266-207ACD26E964}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Tests", "OpenNest.Tests\OpenNest.Tests.csproj", "{03539EB7-9DB2-4634-A6FD-F094B9603596}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -125,6 +127,18 @@ Global
|
|||||||
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x64.Build.0 = Release|Any CPU
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.ActiveCfg = Release|Any CPU
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.Build.0 = Release|Any CPU
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user