From d57e2ca54ba4ec465dd7279417c3cf0141e82395 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 31 Mar 2026 22:55:07 -0400 Subject: [PATCH] feat: add contour reordering with auto-sequence and move up/down --- OpenNest.Core/Converters/ContourInfo.cs | 5 + .../Controls/ProgramEditorControl.Designer.cs | 34 ++++- OpenNest/Controls/ProgramEditorControl.cs | 141 ++++++++++++++++++ 3 files changed, 175 insertions(+), 5 deletions(-) diff --git a/OpenNest.Core/Converters/ContourInfo.cs b/OpenNest.Core/Converters/ContourInfo.cs index 57d5f24..21ca8bb 100644 --- a/OpenNest.Core/Converters/ContourInfo.cs +++ b/OpenNest.Core/Converters/ContourInfo.cs @@ -56,6 +56,11 @@ namespace OpenNest.Converters Shape.Reverse(); } + public void SetLabel(string label) + { + Label = label; + } + public static List Classify(List shapes) { if (shapes.Count == 0) diff --git a/OpenNest/Controls/ProgramEditorControl.Designer.cs b/OpenNest/Controls/ProgramEditorControl.Designer.cs index 600a08b..9ae6d95 100644 --- a/OpenNest/Controls/ProgramEditorControl.Designer.cs +++ b/OpenNest/Controls/ProgramEditorControl.Designer.cs @@ -21,6 +21,9 @@ namespace OpenNest.Controls contourList = new System.Windows.Forms.ListBox(); contourMenu = new System.Windows.Forms.ContextMenuStrip(components); menuReverse = new System.Windows.Forms.ToolStripMenuItem(); + menuMoveUp = new System.Windows.Forms.ToolStripMenuItem(); + menuMoveDown = new System.Windows.Forms.ToolStripMenuItem(); + menuSequence = new System.Windows.Forms.ToolStripMenuItem(); reverseButton = new System.Windows.Forms.Button(); rightSplit = new System.Windows.Forms.SplitContainer(); preview = new EntityView(); @@ -90,15 +93,33 @@ namespace OpenNest.Controls // // contourMenu // - contourMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { menuReverse }); + contourMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { menuReverse, menuMoveUp, menuMoveDown, new System.Windows.Forms.ToolStripSeparator(), menuSequence }); contourMenu.Name = "contourMenu"; - contourMenu.Size = new System.Drawing.Size(166, 26); - // + contourMenu.Size = new System.Drawing.Size(180, 120); + // // menuReverse - // + // menuReverse.Name = "menuReverse"; - menuReverse.Size = new System.Drawing.Size(165, 22); + menuReverse.Size = new System.Drawing.Size(179, 22); menuReverse.Text = "Reverse Direction"; + // + // menuMoveUp + // + menuMoveUp.Name = "menuMoveUp"; + menuMoveUp.Size = new System.Drawing.Size(179, 22); + menuMoveUp.Text = "Move Up"; + // + // menuMoveDown + // + menuMoveDown.Name = "menuMoveDown"; + menuMoveDown.Size = new System.Drawing.Size(179, 22); + menuMoveDown.Text = "Move Down"; + // + // menuSequence + // + menuSequence.Name = "menuSequence"; + menuSequence.Size = new System.Drawing.Size(179, 22); + menuSequence.Text = "Auto Sequence"; // // reverseButton // @@ -246,5 +267,8 @@ namespace OpenNest.Controls private System.Windows.Forms.TextBox gcodeEditor; private System.Windows.Forms.ContextMenuStrip contourMenu; private System.Windows.Forms.ToolStripMenuItem menuReverse; + private System.Windows.Forms.ToolStripMenuItem menuMoveUp; + private System.Windows.Forms.ToolStripMenuItem menuMoveDown; + private System.Windows.Forms.ToolStripMenuItem menuSequence; } } diff --git a/OpenNest/Controls/ProgramEditorControl.cs b/OpenNest/Controls/ProgramEditorControl.cs index 5834a8a..041f865 100644 --- a/OpenNest/Controls/ProgramEditorControl.cs +++ b/OpenNest/Controls/ProgramEditorControl.cs @@ -25,6 +25,10 @@ namespace OpenNest.Controls contourList.SelectedIndexChanged += OnContourSelectionChanged; reverseButton.Click += OnReverseClicked; menuReverse.Click += OnReverseClicked; + menuMoveUp.Click += OnMoveUpClicked; + menuMoveDown.Click += OnMoveDownClicked; + menuSequence.Click += OnSequenceClicked; + contourMenu.Opening += OnContourMenuOpening; applyButton.Click += OnApplyClicked; preview.PaintOverlay = OnPreviewPaintOverlay; } @@ -258,6 +262,143 @@ namespace OpenNest.Controls ProgramChanged?.Invoke(this, EventArgs.Empty); } + private void OnContourMenuOpening(object sender, System.ComponentModel.CancelEventArgs e) + { + var index = contourList.SelectedIndex; + var perimeterIndex = contours.FindIndex(c => c.Type == ContourClassification.Perimeter); + var isPerimeter = index >= 0 && index == perimeterIndex; + + // Can't move perimeter, can't move past perimeter + menuMoveUp.Enabled = index > 0 && !isPerimeter; + menuMoveDown.Enabled = index >= 0 && index < perimeterIndex - 1; + } + + private void OnMoveUpClicked(object sender, EventArgs e) + { + var index = contourList.SelectedIndex; + if (index <= 0) return; + if (contours[index].Type == ContourClassification.Perimeter) return; + + (contours[index], contours[index - 1]) = (contours[index - 1], contours[index]); + RebuildAfterReorder(index - 1); + } + + private void OnMoveDownClicked(object sender, EventArgs e) + { + var index = contourList.SelectedIndex; + if (index < 0 || index >= contours.Count - 1) return; + if (contours[index].Type == ContourClassification.Perimeter) return; + if (contours[index + 1].Type == ContourClassification.Perimeter) return; + + (contours[index], contours[index + 1]) = (contours[index + 1], contours[index]); + RebuildAfterReorder(index + 1); + } + + private void OnSequenceClicked(object sender, EventArgs e) + { + // Nearest-neighbor sort for non-perimeter contours + var perimeterIndex = contours.FindIndex(c => c.Type == ContourClassification.Perimeter); + if (perimeterIndex < 0) return; + + var nonPerimeter = contours.Where(c => c.Type != ContourClassification.Perimeter).ToList(); + if (nonPerimeter.Count <= 1) return; + + var sorted = new List(); + var remaining = new List(nonPerimeter); + var pos = new Vector(); + + while (remaining.Count > 0) + { + var nearest = 0; + var nearestDist = double.MaxValue; + + for (var i = 0; i < remaining.Count; i++) + { + var startPt = GetContourStartPoint(remaining[i]); + var dist = pos.DistanceTo(startPt); + if (dist < nearestDist) + { + nearestDist = dist; + nearest = i; + } + } + + var next = remaining[nearest]; + sorted.Add(next); + pos = GetContourEndPoint(next); + remaining.RemoveAt(nearest); + } + + // Put perimeter back at the end + sorted.Add(contours[perimeterIndex]); + contours = sorted; + RebuildAfterReorder(-1); + } + + private static Vector GetContourStartPoint(ContourInfo contour) + { + var entity = contour.Shape.Entities[0]; + return entity switch + { + Line line => line.StartPoint, + Arc arc => arc.StartPoint(), + Circle circle => new Vector(circle.Center.X + circle.Radius, circle.Center.Y), + _ => new Vector(), + }; + } + + private static Vector GetContourEndPoint(ContourInfo contour) + { + var entity = contour.Shape.Entities[^1]; + return entity switch + { + Line line => line.EndPoint, + Arc arc => arc.EndPoint(), + Circle circle => new Vector(circle.Center.X + circle.Radius, circle.Center.Y), + _ => new Vector(), + }; + } + + private void RebuildAfterReorder(int selectIndex) + { + RelabelContours(); + Program = BuildProgram(contours); + isDirty = true; + + PopulateContourList(); + if (selectIndex >= 0 && selectIndex < contourList.Items.Count) + contourList.SelectedIndex = selectIndex; + UpdateGcodeText(); + RefreshPreview(); + ProgramChanged?.Invoke(this, EventArgs.Empty); + } + + private void RelabelContours() + { + var holeCount = 0; + var etchCount = 0; + var openCount = 0; + + foreach (var contour in contours) + { + switch (contour.Type) + { + case ContourClassification.Hole: + holeCount++; + contour.SetLabel($"Hole {holeCount}"); + break; + case ContourClassification.Etch: + etchCount++; + contour.SetLabel(etchCount == 1 ? "Etch" : $"Etch {etchCount}"); + break; + case ContourClassification.Open: + openCount++; + contour.SetLabel(openCount == 1 ? "Open" : $"Open {openCount}"); + break; + } + } + } + private void OnPreviewPaintOverlay(Graphics g) { if (contours.Count == 0) return;