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 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 profile = new ShapeProfile(entities);
|
||||
|
||||
@@ -44,9 +44,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
currentPoint = closestPt;
|
||||
}
|
||||
|
||||
var lastCutPoint = exitPoint;
|
||||
|
||||
// Perimeter last
|
||||
{
|
||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
||||
lastCutPoint = perimeterPt;
|
||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
||||
var winding = DetermineWinding(profile.Perimeter);
|
||||
|
||||
@@ -60,21 +63,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Vector GetExitPoint(Plate plate)
|
||||
{
|
||||
var w = plate.Size.Width;
|
||||
var l = plate.Size.Length;
|
||||
|
||||
return plate.Quadrant switch
|
||||
return new CuttingResult
|
||||
{
|
||||
1 => new Vector(w, l), // Q1 origin BottomLeft -> exit TopRight
|
||||
2 => new Vector(0, l), // Q2 origin BottomRight -> exit TopLeft
|
||||
3 => new Vector(0, 0), // Q3 origin TopRight -> exit BottomLeft
|
||||
4 => new Vector(w, 0), // Q4 origin TopLeft -> exit BottomRight
|
||||
_ => new Vector(w, l)
|
||||
Program = result,
|
||||
LastCutPoint = lastCutPoint
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
11
OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs
Normal file
11
OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs
Normal file
@@ -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 bool HasManualLeadIns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rotation of the part in radians.
|
||||
/// </summary>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
OpenNest.Engine/PlateResult.cs
Normal file
18
OpenNest.Engine/PlateResult.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
44
OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs
Normal file
44
OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
OpenNest.Engine/RapidPlanning/IRapidPlanner.cs
Normal file
10
OpenNest.Engine/RapidPlanning/IRapidPlanner.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
11
OpenNest.Engine/RapidPlanning/RapidPath.cs
Normal file
11
OpenNest.Engine/RapidPlanning/RapidPath.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
17
OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs
Normal file
17
OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs
Normal file
@@ -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>()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
96
OpenNest.Engine/Sequencing/AdvancedSequencer.cs
Normal file
96
OpenNest.Engine/Sequencing/AdvancedSequencer.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
OpenNest.Engine/Sequencing/BottomSideSequencer.cs
Normal file
17
OpenNest.Engine/Sequencing/BottomSideSequencer.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
OpenNest.Engine/Sequencing/EdgeStartSequencer.cs
Normal file
36
OpenNest.Engine/Sequencing/EdgeStartSequencer.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
14
OpenNest.Engine/Sequencing/IPartSequencer.cs
Normal file
14
OpenNest.Engine/Sequencing/IPartSequencer.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
139
OpenNest.Engine/Sequencing/LeastCodeSequencer.cs
Normal file
139
OpenNest.Engine/Sequencing/LeastCodeSequencer.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
OpenNest.Engine/Sequencing/LeftSideSequencer.cs
Normal file
17
OpenNest.Engine/Sequencing/LeftSideSequencer.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
OpenNest.Engine/Sequencing/PartSequencerFactory.cs
Normal file
23
OpenNest.Engine/Sequencing/PartSequencerFactory.cs
Normal file
@@ -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.")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
22
OpenNest.Engine/Sequencing/PlateHelper.cs
Normal file
22
OpenNest.Engine/Sequencing/PlateHelper.cs
Normal file
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
17
OpenNest.Engine/Sequencing/RightSideSequencer.cs
Normal file
17
OpenNest.Engine/Sequencing/RightSideSequencer.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
OpenNest.Tests/CuttingResultTests.cs
Normal file
23
OpenNest.Tests/CuttingResultTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
29
OpenNest.Tests/OpenNest.Tests.csproj
Normal file
29
OpenNest.Tests/OpenNest.Tests.csproj
Normal file
@@ -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>
|
||||
32
OpenNest.Tests/PartFlagTests.cs
Normal file
32
OpenNest.Tests/PartFlagTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
56
OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs
Normal file
56
OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
39
OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs
Normal file
39
OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
69
OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs
Normal file
69
OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
75
OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs
Normal file
75
OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
31
OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs
Normal file
31
OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
61
OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs
Normal file
61
OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
30
OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs
Normal file
30
OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
27
OpenNest.Tests/TestHelpers.cs
Normal file
27
OpenNest.Tests/TestHelpers.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
14
OpenNest.sln
14
OpenNest.sln
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNes
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Training", "OpenNest.Training\OpenNest.Training.csproj", "{249BF728-25DD-4863-8266-207ACD26E964}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Tests", "OpenNest.Tests\OpenNest.Tests.csproj", "{03539EB7-9DB2-4634-A6FD-F094B9603596}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user