fix(ui): restore manual part sequencing and close control

This commit is contained in:
aj committed 2026-10-09 17:06:17 -04:00
1 parent 575412a464
commit d3e325f36c
6 files changed
+147 -122

No files matched your search

@@ -0,0 +1,29 @@
using System;
using System.Linq;
namespace OpenNest.Sequencing
{
/// <summary>Moves one existing part to a one-based cutting sequence position.</summary>
public static class ManualPartSequencing
{
public static bool Move(Plate plate, Part part, int sequenceNumber)
{
ArgumentNullException.ThrowIfNull(plate);
ArgumentNullException.ThrowIfNull(part);
var order = plate.Parts.ToList();
if (sequenceNumber < 1 || sequenceNumber > order.Count)
return false;
var oldIndex = order.FindIndex(p => ReferenceEquals(p, part));
if (oldIndex < 0)
return false;
if (oldIndex == sequenceNumber - 1)
return true;
order.RemoveAt(oldIndex);
order.Insert(sequenceNumber - 1, part);
plate.Parts.Reorder(order);
return true;
}
}
}
@@ -0,0 +1,43 @@
using OpenNest.Geometry;
using OpenNest.Sequencing;
namespace OpenNest.Tests.Sequencing;
public class ManualPartSequencingTests
{
[Fact]
public void Move_SetsExactOneBasedSequenceWithoutChangingQuantity()
{
var drawing = TestHelpers.MakeSquareDrawing();
var first = new Part(drawing, new Vector(0, 0));
var second = new Part(drawing, new Vector(20, 0));
var third = new Part(drawing, new Vector(40, 0));
var plate = TestHelpers.MakePlate(60, 120, first, second, third);
var nested = drawing.Quantity.Nested;
var reorders = 0;
plate.PartsReordered += (_, _) => reorders++;
Assert.True(ManualPartSequencing.Move(plate, third, 1));
Assert.Equal(new[] { third, first, second }, plate.Parts);
Assert.True(ManualPartSequencing.Move(plate, first, 3));
Assert.Equal(new[] { third, second, first }, plate.Parts);
Assert.Equal(nested, drawing.Quantity.Nested);
Assert.Equal(2, reorders);
}
[Fact]
public void Move_RejectsAbsentPartAndOutOfRangeWithoutChangingPlate()
{
var drawing = TestHelpers.MakeSquareDrawing();
var part = new Part(drawing, new Vector(0, 0));
var plate = TestHelpers.MakePlate(60, 120, part);
var reorders = 0;
plate.PartsReordered += (_, _) => reorders++;
Assert.False(ManualPartSequencing.Move(plate, part, 0));
Assert.False(ManualPartSequencing.Move(plate, part, 2));
Assert.False(ManualPartSequencing.Move(plate, new Part(drawing, new Vector(0, 0)), 1));
Assert.Equal(new[] { part }, plate.Parts);
Assert.Equal(0, reorders);
}
}
+48 -116
View File
@@ -1,169 +1,101 @@
using System.Collections.Generic;
using System;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Converters;
using OpenNest.Forms;
using OpenNest.Geometry;
using OpenNest.Sequencing;
namespace OpenNest.Actions
{
[DisplayName("Set Sequence")]
public class ActionSetSequence : Action
{
private readonly SequenceForm SequenceForm;
private readonly List<Pair> ShapePartPairs;
private readonly Pen pen;
private readonly Forms.SequenceForm sequenceForm;
private readonly Pen pen = new Pen(Color.Blue, 2.0f);
private LayoutPart hoveredPart;
private bool disconnecting;
private Shape ClosestShape;
private Part ClosestPart;
private int sequenceNumber = 1;
public int SequenceNumber
{
get { return sequenceNumber; }
set
{
if (
value <= SequenceForm.numericUpDown1.Maximum
&& value >= SequenceForm.numericUpDown1.Minimum
)
{
sequenceNumber = value;
SequenceForm.numericUpDown1.Value = sequenceNumber;
}
}
}
// Escape ends this tool; it must not resurrect its closed window.
public override bool ResumeOnEscape => false;
public ActionSetSequence(PlateView plateView)
: base(plateView)
{
SequenceForm = new Forms.SequenceForm();
SequenceForm.numericUpDown1.DataBindings.Add(
"Value",
this,
"SequenceNumber",
false,
DataSourceUpdateMode.OnPropertyChanged
);
SequenceForm.numericUpDown1.Maximum = plateView.Plate.Parts.Count;
SequenceForm.Owner = Application.OpenForms[0];
SequenceForm.Show();
plateView.Invalidate();
pen = new Pen(Color.Blue, 2.0f);
ShapePartPairs = new List<Pair>();
foreach (var part in plateView.Plate.Parts)
sequenceForm = new Forms.SequenceForm();
sequenceForm.numericUpDown1.Maximum = System.Math.Max(1, plateView.Plate.Parts.Count);
sequenceForm.DoneClicked += (_, _) => plateView.EndAction();
sequenceForm.FormClosed += (_, _) =>
{
var entities = ConvertProgram
.ToGeometry(part.Program)
.Where(e => e.Layer == SpecialLayers.Cut)
.ToList();
entities.ForEach(entity => entity.Offset(part.Location));
var shapes = ShapeBuilder.GetShapes(entities);
var shape = new Shape();
shape.Entities.AddRange(shapes);
ShapePartPairs.Add(new Pair() { Part = part, Shape = shape });
}
if (!disconnecting)
plateView.EndAction();
};
sequenceForm.Show(plateView.FindForm());
plateView.MouseMove += plateView_MouseMove;
plateView.MouseLeave += plateView_MouseLeave;
plateView.MouseClick += plateView_MouseClick;
plateView.Paint += plateView_Paint;
SequenceNumber = 1;
}
private void plateView_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
CancelAction();
{
plateView.EndAction();
return;
}
if (e.Button != MouseButtons.Left)
return;
if (SequenceNumber > plateView.Plate.Parts.Count)
SequenceNumber = plateView.Plate.Parts.Count;
if (ClosestPart == null)
// Hit-test at the click, not at the previous mouse-move location.
var hit = plateView.GetPartAtControlPoint(e.Location);
var part = hit?.BasePart;
if (part == null || !ManualPartSequencing.Move(plateView.Plate, part, (int)sequenceForm.numericUpDown1.Value))
return;
plateView.Plate.Parts.Remove(ClosestPart);
plateView.Plate.Parts.Insert(SequenceNumber - 1, ClosestPart);
SequenceNumber++;
if (sequenceForm.numericUpDown1.Value < sequenceForm.numericUpDown1.Maximum)
sequenceForm.numericUpDown1.Value++;
plateView.Invalidate();
}
private void plateView_MouseMove(object sender, MouseEventArgs e)
{
var pair = GetClosestShape();
ClosestShape = pair.HasValue ? pair.Value.Shape : null;
ClosestPart = pair.HasValue ? pair.Value.Part : null;
var hit = plateView.GetPartAtControlPoint(e.Location);
if (ReferenceEquals(hit, hoveredPart))
return;
hoveredPart = hit;
plateView.Invalidate();
}
private void plateView_MouseLeave(object sender, EventArgs e)
{
hoveredPart = null;
plateView.Invalidate();
}
private void plateView_Paint(object sender, PaintEventArgs e)
{
if (ClosestShape == null)
return;
var path = ClosestShape.GetGraphicsPath();
path.Transform(plateView.Matrix);
e.Graphics.DrawPath(pen, path);
path.Dispose();
}
private Pair? GetClosestShape()
{
Pair? closestShape = null;
double distance = double.MaxValue;
for (int i = 0; i < ShapePartPairs.Count; i++)
{
var shape = ShapePartPairs[i];
var closestPt = shape.Shape.ClosestPointTo(plateView.CurrentPoint);
var distance2 = closestPt.DistanceTo(plateView.CurrentPoint);
if (distance2 < distance)
{
closestShape = shape;
distance = distance2;
}
}
return closestShape;
var path = hoveredPart?.SelectionPath;
if (path != null)
e.Graphics.DrawPath(pen, path);
}
public override void DisconnectEvents()
{
disconnecting = true;
plateView.Paint -= plateView_Paint;
plateView.MouseMove -= plateView_MouseMove;
plateView.MouseLeave -= plateView_MouseLeave;
plateView.MouseClick -= plateView_MouseClick;
hoveredPart = null;
sequenceForm.Close();
sequenceForm.Dispose();
pen.Dispose();
plateView.Invalidate();
SequenceForm.Close();
}
public override void CancelAction()
{
SequenceNumber = 1;
SequenceForm.numericUpDown1.Value = SequenceNumber + 1;
}
public override void CancelAction() { }
public override bool IsBusy()
{
return false;
}
private struct Pair
{
public Part Part;
public Shape Shape;
}
public override bool IsBusy() => false;
}
}
+17 -5
View File
@@ -31,6 +31,7 @@
tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
label1 = new System.Windows.Forms.Label();
numericUpDown1 = new OpenNest.Controls.NumericUpDown();
doneButton = new System.Windows.Forms.Button();
tableLayoutPanel1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)numericUpDown1).BeginInit();
SuspendLayout();
@@ -42,13 +43,15 @@
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(label1, 0, 0);
tableLayoutPanel1.Controls.Add(numericUpDown1, 1, 0);
tableLayoutPanel1.Controls.Add(doneButton, 1, 1);
tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 1;
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 35F));
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
tableLayoutPanel1.Size = new System.Drawing.Size(266, 35);
tableLayoutPanel1.Size = new System.Drawing.Size(266, 74);
tableLayoutPanel1.TabIndex = 0;
//
// label1
@@ -74,20 +77,28 @@
numericUpDown1.TabIndex = 1;
numericUpDown1.Value = new decimal(new int[] { 1, 0, 0, 0 });
numericUpDown1.Leave += numericUpDown1_Leave;
// doneButton
//
doneButton.Anchor = System.Windows.Forms.AnchorStyles.Right;
doneButton.Location = new System.Drawing.Point(182, 40);
doneButton.Name = "doneButton";
doneButton.Size = new System.Drawing.Size(80, 28);
doneButton.TabIndex = 2;
doneButton.Text = "Done";
doneButton.UseVisualStyleBackColor = true;
doneButton.Click += doneButton_Click;
// SequenceForm
//
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(266, 35);
ControlBox = false;
ClientSize = new System.Drawing.Size(266, 74);
Controls.Add(tableLayoutPanel1);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
Location = new System.Drawing.Point(100, 100);
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
MinimumSize = new System.Drawing.Size(268, 74);
MinimumSize = new System.Drawing.Size(268, 113);
Name = "SequenceForm";
ShowIcon = false;
ShowInTaskbar = false;
@@ -105,5 +116,6 @@
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.Label label1;
public Controls.NumericUpDown numericUpDown1;
private System.Windows.Forms.Button doneButton;
}
}
+8 -1
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Windows.Forms;
namespace OpenNest.Forms
@@ -10,6 +10,13 @@ namespace OpenNest.Forms
InitializeComponent();
}
public event EventHandler DoneClicked;
private void doneButton_Click(object sender, EventArgs e)
{
DoneClicked?.Invoke(this, EventArgs.Empty);
}
private void numericUpDown1_Leave(object sender, EventArgs e)
{
numericUpDown1.Validate();
+2
View File
@@ -36,6 +36,8 @@ For a 120-by-81-inch sheet whose parts extend through 80 inches, the nominal ske
## Part sequencing
For **Plate > Sequence Parts > Manual Sequence**, choose the one-based sequence number and hover a part on the sheet; only the part under the pointer is outlined. Click it to place it at that exact position, then the number advances up to the last part. Clicking empty sheet space changes nothing. Use **Done**, the window close button, right-click on the sheet, or Escape to leave the tool. Manual reordering changes neither geometry nor quantities.
After adding cutoffs, apply **Part Sequencing** to the current plate or all plates. Each cutoff is moved earlier as needed so it is cut before every part its nominal line passes through. Ordinary parts retain their relative order from the chosen sequencing route; a tail separator that crosses no parts keeps its normal route position rather than being forced to the front. Cutoff geometry, clearance, part programs, placements, and quantities are unchanged.
The dependency check uses the nominal horizontal/vertical line and its start/end limits, not the trimmed cutting segments, which intentionally skip the parts. It conservatively checks placed part bounds (including edge contacts), so a cutoff through a concave recess can also move ahead of that part. Definitions are matched by drawing identity, not their displayed names. A cutoff part with no matching definition is conservatively ordered before all ordinary parts.