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/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.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); + } +}