diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
index 7ea880b..30455be 100644
--- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
+++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
@@ -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
};
}
diff --git a/OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs b/OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs
new file mode 100644
index 0000000..933db14
--- /dev/null
+++ b/OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs
@@ -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; }
+ }
+}
diff --git a/OpenNest.Core/Part.cs b/OpenNest.Core/Part.cs
index 56598f4..37b12f0 100644
--- a/OpenNest.Core/Part.cs
+++ b/OpenNest.Core/Part.cs
@@ -51,6 +51,8 @@ namespace OpenNest
public Program Program { get; private set; }
+ public bool HasManualLeadIns { get; set; }
+
///
/// Gets the rotation of the part in radians.
///
diff --git a/OpenNest.Engine/PlateProcessor.cs b/OpenNest.Engine/PlateProcessor.cs
new file mode 100644
index 0000000..9601933
--- /dev/null
+++ b/OpenNest.Engine/PlateProcessor.cs
@@ -0,0 +1,120 @@
+using System.Collections.Generic;
+using System.Linq;
+using OpenNest.CNC;
+using OpenNest.CNC.CuttingStrategy;
+using OpenNest.Engine.RapidPlanning;
+using OpenNest.Engine.Sequencing;
+using OpenNest.Geometry;
+
+namespace OpenNest.Engine
+{
+ public class PlateProcessor
+ {
+ public IPartSequencer Sequencer { get; set; }
+ public ContourCuttingStrategy CuttingStrategy { get; set; }
+ public IRapidPlanner RapidPlanner { get; set; }
+
+ public PlateResult Process(Plate plate)
+ {
+ var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
+ var results = new List(sequenced.Count);
+ var cutAreas = new List();
+ var currentPoint = PlateHelper.GetExitPoint(plate);
+
+ foreach (var sp in sequenced)
+ {
+ var part = sp.Part;
+
+ // Compute approach point in part-local space
+ var localApproach = ToPartLocal(currentPoint, part);
+
+ Program processedProgram;
+ Vector lastCutLocal;
+
+ if (!part.HasManualLeadIns && CuttingStrategy != null)
+ {
+ var cuttingResult = CuttingStrategy.Apply(part.Program, localApproach);
+ processedProgram = cuttingResult.Program;
+ lastCutLocal = cuttingResult.LastCutPoint;
+ }
+ else
+ {
+ processedProgram = part.Program;
+ lastCutLocal = GetProgramEndPoint(part.Program);
+ }
+
+ // Pierce point: program start point in plate space
+ var pierceLocal = GetProgramStartPoint(part.Program);
+ var piercePoint = ToPlateSpace(pierceLocal, part);
+
+ // Plan rapid from currentPoint to pierce point
+ var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas);
+
+ results.Add(new ProcessedPart
+ {
+ Part = part,
+ ProcessedProgram = processedProgram,
+ RapidPath = rapidPath
+ });
+
+ // Update cut areas with part perimeter
+ var perimeter = GetPartPerimeter(part);
+ if (perimeter != null)
+ cutAreas.Add(perimeter);
+
+ // Update current point to last cut point in plate space
+ currentPoint = ToPlateSpace(lastCutLocal, part);
+ }
+
+ return new PlateResult { Parts = results };
+ }
+
+ private static Vector ToPartLocal(Vector platePoint, Part part)
+ {
+ return platePoint - part.Location;
+ }
+
+ private static Vector ToPlateSpace(Vector localPoint, Part part)
+ {
+ return localPoint + part.Location;
+ }
+
+ private static Vector GetProgramStartPoint(Program program)
+ {
+ if (program.Codes.Count == 0)
+ return Vector.Zero;
+
+ var first = program.Codes[0];
+ if (first is Motion motion)
+ return motion.EndPoint;
+
+ return Vector.Zero;
+ }
+
+ private static Vector GetProgramEndPoint(Program program)
+ {
+ for (var i = program.Codes.Count - 1; i >= 0; i--)
+ {
+ if (program.Codes[i] is Motion motion)
+ return motion.EndPoint;
+ }
+
+ return Vector.Zero;
+ }
+
+ private static Shape GetPartPerimeter(Part part)
+ {
+ var entities = part.Program.ToGeometry();
+ if (entities == null || entities.Count == 0)
+ return null;
+
+ var profile = new ShapeProfile(entities);
+ var perimeter = profile.Perimeter;
+ if (perimeter == null || perimeter.Entities.Count == 0)
+ return null;
+
+ perimeter.Offset(part.Location);
+ return perimeter;
+ }
+ }
+}
diff --git a/OpenNest.Engine/PlateResult.cs b/OpenNest.Engine/PlateResult.cs
new file mode 100644
index 0000000..7209be7
--- /dev/null
+++ b/OpenNest.Engine/PlateResult.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using OpenNest.CNC;
+using OpenNest.Engine.RapidPlanning;
+
+namespace OpenNest.Engine
+{
+ public class PlateResult
+ {
+ public List Parts { get; init; }
+ }
+
+ public readonly struct ProcessedPart
+ {
+ public Part Part { get; init; }
+ public Program ProcessedProgram { get; init; }
+ public RapidPath RapidPath { get; init; }
+ }
+}
diff --git a/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs
new file mode 100644
index 0000000..154e525
--- /dev/null
+++ b/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs
@@ -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 cutAreas)
+ {
+ var travelLine = new Line(from, to);
+
+ foreach (var cutArea in cutAreas)
+ {
+ if (TravelLineIntersectsShape(travelLine, cutArea))
+ {
+ return new RapidPath
+ {
+ HeadUp = true,
+ Waypoints = new List()
+ };
+ }
+ }
+
+ return new RapidPath
+ {
+ HeadUp = false,
+ Waypoints = new List()
+ };
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs
new file mode 100644
index 0000000..edae37c
--- /dev/null
+++ b/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs
@@ -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 cutAreas);
+ }
+}
diff --git a/OpenNest.Engine/RapidPlanning/RapidPath.cs b/OpenNest.Engine/RapidPlanning/RapidPath.cs
new file mode 100644
index 0000000..8ff6eb4
--- /dev/null
+++ b/OpenNest.Engine/RapidPlanning/RapidPath.cs
@@ -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 Waypoints { get; init; }
+ }
+}
diff --git a/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs
new file mode 100644
index 0000000..6de4db6
--- /dev/null
+++ b/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs
@@ -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 cutAreas)
+ {
+ return new RapidPath
+ {
+ HeadUp = true,
+ Waypoints = new List()
+ };
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/AdvancedSequencer.cs b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs
new file mode 100644
index 0000000..fa285aa
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs
@@ -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 Sequence(IReadOnlyList parts, Plate plate)
+ {
+ if (parts.Count == 0)
+ return new List();
+
+ 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(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 GroupIntoRows(IReadOnlyList parts, double minDistance)
+ {
+ // Sort parts by Y center
+ var sorted = parts
+ .OrderBy(p => p.BoundingBox.Center.Y)
+ .ToList();
+
+ var rows = new List();
+
+ 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 Parts { get; } = new List();
+
+ public PartRow(double rowY)
+ {
+ RowY = rowY;
+ }
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/BottomSideSequencer.cs b/OpenNest.Engine/Sequencing/BottomSideSequencer.cs
new file mode 100644
index 0000000..de06053
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/BottomSideSequencer.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace OpenNest.Engine.Sequencing
+{
+ public class BottomSideSequencer : IPartSequencer
+ {
+ public List Sequence(IReadOnlyList parts, Plate plate)
+ {
+ return parts
+ .OrderBy(p => p.Location.Y)
+ .ThenBy(p => p.Location.X)
+ .Select(p => new SequencedPart { Part = p })
+ .ToList();
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs b/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs
new file mode 100644
index 0000000..187a599
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace OpenNest.Engine.Sequencing
+{
+ public class EdgeStartSequencer : IPartSequencer
+ {
+ public List Sequence(IReadOnlyList 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));
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/IPartSequencer.cs b/OpenNest.Engine/Sequencing/IPartSequencer.cs
new file mode 100644
index 0000000..79b0c3e
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/IPartSequencer.cs
@@ -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 Sequence(IReadOnlyList parts, Plate plate);
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs b/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs
new file mode 100644
index 0000000..63b0e2a
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs
@@ -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 Sequence(IReadOnlyList parts, Plate plate)
+ {
+ if (parts.Count == 0)
+ return new List();
+
+ var exit = PlateHelper.GetExitPoint(plate);
+ var ordered = NearestNeighbor(parts, exit);
+ TwoOpt(ordered, exit);
+
+ var result = new List(ordered.Count);
+ foreach (var p in ordered)
+ result.Add(new SequencedPart { Part = p });
+ return result;
+ }
+
+ private static List NearestNeighbor(IReadOnlyList parts, OpenNest.Geometry.Vector exit)
+ {
+ var remaining = new List(parts);
+ var ordered = new List(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 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;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ private static double RouteDistance(List 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 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);
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/LeftSideSequencer.cs b/OpenNest.Engine/Sequencing/LeftSideSequencer.cs
new file mode 100644
index 0000000..ca0f20a
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/LeftSideSequencer.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace OpenNest.Engine.Sequencing
+{
+ public class LeftSideSequencer : IPartSequencer
+ {
+ public List Sequence(IReadOnlyList parts, Plate plate)
+ {
+ return parts
+ .OrderBy(p => p.Location.X)
+ .ThenBy(p => p.Location.Y)
+ .Select(p => new SequencedPart { Part = p })
+ .ToList();
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/PartSequencerFactory.cs b/OpenNest.Engine/Sequencing/PartSequencerFactory.cs
new file mode 100644
index 0000000..0e29d1e
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/PartSequencerFactory.cs
@@ -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.")
+ };
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/PlateHelper.cs b/OpenNest.Engine/Sequencing/PlateHelper.cs
new file mode 100644
index 0000000..1a46327
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/PlateHelper.cs
@@ -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)
+ };
+ }
+ }
+}
diff --git a/OpenNest.Engine/Sequencing/RightSideSequencer.cs b/OpenNest.Engine/Sequencing/RightSideSequencer.cs
new file mode 100644
index 0000000..f804a38
--- /dev/null
+++ b/OpenNest.Engine/Sequencing/RightSideSequencer.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace OpenNest.Engine.Sequencing
+{
+ public class RightSideSequencer : IPartSequencer
+ {
+ public List Sequence(IReadOnlyList parts, Plate plate)
+ {
+ return parts
+ .OrderByDescending(p => p.Location.X)
+ .ThenBy(p => p.Location.Y)
+ .Select(p => new SequencedPart { Part = p })
+ .ToList();
+ }
+ }
+}
diff --git a/OpenNest.Tests/CuttingResultTests.cs b/OpenNest.Tests/CuttingResultTests.cs
new file mode 100644
index 0000000..58cbefc
--- /dev/null
+++ b/OpenNest.Tests/CuttingResultTests.cs
@@ -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);
+ }
+}
diff --git a/OpenNest.Tests/OpenNest.Tests.csproj b/OpenNest.Tests/OpenNest.Tests.csproj
new file mode 100644
index 0000000..ec1b229
--- /dev/null
+++ b/OpenNest.Tests/OpenNest.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0-windows
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenNest.Tests/PartFlagTests.cs b/OpenNest.Tests/PartFlagTests.cs
new file mode 100644
index 0000000..f140dd2
--- /dev/null
+++ b/OpenNest.Tests/PartFlagTests.cs
@@ -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);
+ }
+}
diff --git a/OpenNest.Tests/PlateProcessorTests.cs b/OpenNest.Tests/PlateProcessorTests.cs
new file mode 100644
index 0000000..73d5a98
--- /dev/null
+++ b/OpenNest.Tests/PlateProcessorTests.cs
@@ -0,0 +1,132 @@
+using System.Collections.Generic;
+using System.Linq;
+using OpenNest.CNC;
+using OpenNest.CNC.CuttingStrategy;
+using OpenNest.Engine;
+using OpenNest.Engine.RapidPlanning;
+using OpenNest.Engine.Sequencing;
+using OpenNest.Geometry;
+using Xunit;
+
+namespace OpenNest.Tests;
+
+public class PlateProcessorTests
+{
+ private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y, size: 2);
+
+ [Fact]
+ public void Process_ReturnsAllParts()
+ {
+ var plate = new Plate(60, 120);
+ plate.Parts.Add(MakePartAt(10, 10));
+ plate.Parts.Add(MakePartAt(30, 30));
+ plate.Parts.Add(MakePartAt(50, 50));
+
+ var processor = new PlateProcessor
+ {
+ Sequencer = new RightSideSequencer(),
+ RapidPlanner = new SafeHeightRapidPlanner()
+ };
+
+ var result = processor.Process(plate);
+
+ Assert.Equal(3, result.Parts.Count);
+ }
+
+ [Fact]
+ public void Process_PreservesSequenceOrder()
+ {
+ var plate = new Plate(60, 120);
+ var left = MakePartAt(5, 10);
+ var right = MakePartAt(50, 10);
+ plate.Parts.Add(left);
+ plate.Parts.Add(right);
+
+ var processor = new PlateProcessor
+ {
+ Sequencer = new RightSideSequencer(),
+ RapidPlanner = new SafeHeightRapidPlanner()
+ };
+
+ var result = processor.Process(plate);
+
+ Assert.Same(right, result.Parts[0].Part);
+ Assert.Same(left, result.Parts[1].Part);
+ }
+
+ [Fact]
+ public void Process_SkipsCuttingStrategy_WhenManualLeadIns()
+ {
+ var plate = new Plate(60, 120);
+ var part = MakePartAt(10, 10);
+ part.HasManualLeadIns = true;
+ plate.Parts.Add(part);
+
+ var processor = new PlateProcessor
+ {
+ Sequencer = new LeftSideSequencer(),
+ CuttingStrategy = new ContourCuttingStrategy
+ {
+ Parameters = new CuttingParameters()
+ },
+ RapidPlanner = new SafeHeightRapidPlanner()
+ };
+
+ var result = processor.Process(plate);
+
+ Assert.Same(part.Program, result.Parts[0].ProcessedProgram);
+ }
+
+ [Fact]
+ public void Process_DoesNotMutatePart()
+ {
+ var plate = new Plate(60, 120);
+ var part = MakePartAt(10, 10);
+ var originalProgram = part.Program;
+ plate.Parts.Add(part);
+
+ var processor = new PlateProcessor
+ {
+ Sequencer = new LeftSideSequencer(),
+ RapidPlanner = new SafeHeightRapidPlanner()
+ };
+
+ var result = processor.Process(plate);
+
+ Assert.Same(originalProgram, part.Program);
+ }
+
+ [Fact]
+ public void Process_NoCuttingStrategy_PassesProgramThrough()
+ {
+ var plate = new Plate(60, 120);
+ var part = MakePartAt(10, 10);
+ plate.Parts.Add(part);
+
+ var processor = new PlateProcessor
+ {
+ Sequencer = new LeftSideSequencer(),
+ RapidPlanner = new SafeHeightRapidPlanner()
+ };
+
+ var result = processor.Process(plate);
+
+ Assert.Same(part.Program, result.Parts[0].ProcessedProgram);
+ }
+
+ [Fact]
+ public void Process_EmptyPlate_ReturnsEmptyResult()
+ {
+ var plate = new Plate(60, 120);
+
+ var processor = new PlateProcessor
+ {
+ Sequencer = new LeftSideSequencer(),
+ RapidPlanner = new SafeHeightRapidPlanner()
+ };
+
+ var result = processor.Process(plate);
+
+ Assert.Empty(result.Parts);
+ }
+}
diff --git a/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs b/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs
new file mode 100644
index 0000000..ca6a00b
--- /dev/null
+++ b/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs
@@ -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());
+
+ 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 { 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 { cutArea });
+
+ Assert.True(result.HeadUp);
+ Assert.Empty(result.Waypoints);
+ }
+}
diff --git a/OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs b/OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs
new file mode 100644
index 0000000..3db408f
--- /dev/null
+++ b/OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs
@@ -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();
+
+ 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 };
+
+ var result = planner.Plan(from, to, cutAreas);
+
+ Assert.True(result.HeadUp);
+ }
+}
diff --git a/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs b/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs
new file mode 100644
index 0000000..69f540f
--- /dev/null
+++ b/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs
@@ -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);
+ }
+}
diff --git a/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs b/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs
new file mode 100644
index 0000000..0a64a98
--- /dev/null
+++ b/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs
@@ -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);
+ }
+}
diff --git a/OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs b/OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs
new file mode 100644
index 0000000..3f002de
--- /dev/null
+++ b/OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs
@@ -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);
+ }
+}
diff --git a/OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs b/OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs
new file mode 100644
index 0000000..a5929c5
--- /dev/null
+++ b/OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs
@@ -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);
+ }
+}
diff --git a/OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs b/OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs
new file mode 100644
index 0000000..1c6a22e
--- /dev/null
+++ b/OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs
@@ -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(() => PartSequencerFactory.Create(parameters));
+ }
+}
diff --git a/OpenNest.Tests/TestHelpers.cs b/OpenNest.Tests/TestHelpers.cs
new file mode 100644
index 0000000..8d68f0b
--- /dev/null
+++ b/OpenNest.Tests/TestHelpers.cs
@@ -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;
+ }
+}
diff --git a/OpenNest.sln b/OpenNest.sln
index 69cf786..d0a8d89 100644
--- a/OpenNest.sln
+++ b/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