From edc81ae45ec71b7627b97d1b1209ef6deb108194 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 16 Mar 2026 00:36:07 -0400 Subject: [PATCH] feat: add AdvancedSequencer with row grouping and serpentine Co-Authored-By: Claude Sonnet 4.6 --- .../Sequencing/AdvancedSequencer.cs | 96 +++++++++++++++++++ .../Sequencing/AdvancedSequencerTests.cs | 69 +++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 OpenNest.Engine/Sequencing/AdvancedSequencer.cs create mode 100644 OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs 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.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); + } +}