feat(desktop): preview sequence numbers live while drawing a path

While a Sequence by Path line is being dragged, the plate view shows the
numbers the finished line would give, without reordering the plate, so
reorder listeners (drawing list, overlap overlay, simulator) are not hit
on every mouse move. The plate is reordered once on release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-20 20:26:55 -04:00
1 parent aeb90d8a86
commit 96f02c665c
6 files changed
+72 -12

No files matched your search

@@ -18,22 +18,31 @@ namespace OpenNest.Engine.Sequencing
/// <summary>Reorders the plate's parts; false when no line has length or nothing moves.</summary>
public static bool Apply(Plate plate, IReadOnlyList<IReadOnlyList<Vector>> lines)
{
ArgumentNullException.ThrowIfNull(plate);
ArgumentNullException.ThrowIfNull(lines);
if (plate.Parts.Count < 2 || !lines.Any(HasLength))
return false;
var first = plate.Parts.Where(p => p.BaseDrawing.CutFirst);
var rest = plate.Parts.Where(p => !p.BaseDrawing.CutFirst);
var ordered = Order(first, lines).Concat(Order(rest, lines)).ToList();
var cutOrder = PlateSequencing.OrderCutOffsFirst(ordered, plate);
if (cutOrder.SequenceEqual(plate.Parts, ReferenceEqualityComparer.Instance))
var cutOrder = CutOrder(plate, lines);
if (cutOrder == null || cutOrder.SequenceEqual(plate.Parts, ReferenceEqualityComparer.Instance))
return false;
plate.Parts.Reorder(cutOrder);
return true;
}
/// <summary>
/// The order <see cref="Apply"/> would give the plate, without changing it; null when
/// no line has length or there is nothing to order.
/// </summary>
public static List<Part> CutOrder(Plate plate, IReadOnlyList<IReadOnlyList<Vector>> lines)
{
ArgumentNullException.ThrowIfNull(plate);
ArgumentNullException.ThrowIfNull(lines);
if (plate.Parts.Count < 2 || !lines.Any(HasLength))
return null;
var first = plate.Parts.Where(p => p.BaseDrawing.CutFirst);
var rest = plate.Parts.Where(p => !p.BaseDrawing.CutFirst);
var ordered = Order(first, lines).Concat(Order(rest, lines)).ToList();
return PlateSequencing.OrderCutOffsFirst(ordered, plate);
}
/// <summary>The parts in line order; ties keep their current order.</summary>
public static List<Part> Order(IEnumerable<Part> parts, IReadOnlyList<IReadOnlyList<Vector>> lines)
{
@@ -47,6 +47,29 @@ public class DrawnPathSequencingTests
Assert.Equal(bottom.Concat(top), plate.Parts);
}
[Fact]
public void CutOrder_PreviewsApplyWithoutReorderingThePlate()
{
var drawing = TestHelpers.MakeSquareDrawing();
var a = new Part(drawing, new Vector(0, 0));
var b = new Part(drawing, new Vector(20, 0));
var c = new Part(drawing, new Vector(40, 0));
var plate = TestHelpers.MakePlate(60, 120, c, a, b);
var reorders = 0;
plate.PartsReordered += (_, _) => reorders++;
var lines = Lines(new[] { new Vector(-5, 5), new Vector(55, 5) });
var preview = DrawnPathSequencing.CutOrder(plate, lines);
Assert.Equal(new[] { a, b, c }, preview);
Assert.Equal(new[] { c, a, b }, plate.Parts);
Assert.Equal(0, reorders);
Assert.Null(DrawnPathSequencing.CutOrder(plate, Lines(new[] { new Vector(0, 0) })));
Assert.True(DrawnPathSequencing.Apply(plate, lines));
Assert.Equal(preview, plate.Parts);
}
[Fact]
public void Apply_PartGoesToTheNearestLineEvenWhenDrawnLater()
{
+20
View File
@@ -92,9 +92,26 @@ namespace OpenNest.Actions
if (plateView.LengthWorldToGui(point.DistanceTo(drawing[^1])) < PointSpacingPixels)
return;
drawing.Add(point);
UpdatePreview();
plateView.Invalidate();
}
/// <summary>Numbers the parts as if the line being drawn were finished, without reordering the plate.</summary>
private void UpdatePreview()
{
var order = DrawnPathSequencing.CutOrder(plateView.Plate, lines.Append(drawing).ToList());
if (order == null)
{
plateView.PreviewSequence = null;
return;
}
var numbers = new Dictionary<Part, int>(order.Count, ReferenceEqualityComparer.Instance);
for (var i = 0; i < order.Count; i++)
numbers[order[i]] = i + 1;
plateView.PreviewSequence = numbers;
}
private void plateView_MouseUp(object sender, MouseEventArgs e)
{
if (drawing == null || e.Button != MouseButtons.Left)
@@ -102,6 +119,7 @@ namespace OpenNest.Actions
var line = drawing;
drawing = null;
plateView.PreviewSequence = null;
var point = plateView.PointControlToWorld(e.Location);
if (point.DistanceTo(line[^1]) > 0)
line.Add(point);
@@ -192,6 +210,7 @@ namespace OpenNest.Actions
plateView.MouseUp -= plateView_MouseUp;
plateView.KeyDown -= plateView_KeyDown;
plateView.Paint -= plateView_Paint;
plateView.PreviewSequence = null;
form.Close();
form.Dispose();
linePen.Dispose();
@@ -206,6 +225,7 @@ namespace OpenNest.Actions
public override void CancelAction()
{
drawing = null;
plateView.PreviewSequence = null;
plateView.Invalidate();
}
+3 -1
View File
@@ -124,7 +124,9 @@ namespace OpenNest.Controls
var finding = view.ValidationOverlay.Finding;
var muted = finding != null && finding.PartNumber != i + 1 && finding.OtherPartNumber != i + 1
|| !view.HasProposalDisplay && view.CutSimulation.IsPartPending(i);
part.Draw(g, (i + 1).ToString(), muted);
var number = view.PreviewSequence != null
&& view.PreviewSequence.TryGetValue(part.BasePart, out var preview) ? preview : i + 1;
part.Draw(g, number.ToString(), muted);
DrawBendLines(g, part.BasePart, muted);
DrawEtchMarks(g, part.BasePart, muted);
DrawGrainWarning(g, part.BasePart, muted);
+6
View File
@@ -312,6 +312,12 @@ namespace OpenNest.Controls
internal OverlapOverlayController OverlapOverlay => overlapOverlay;
internal ValidationOverlay ValidationOverlay => validationOverlay;
internal CutSimulationOverlay CutSimulation => cutSimulation;
/// <summary>
/// Sequence numbers a tool previews in place of the parts' current order, without
/// reordering the plate; null shows the plate's own order.
/// </summary>
internal IReadOnlyDictionary<Part, int> PreviewSequence { get; set; }
internal (float Scale, PointF Origin) CaptureViewport() => (ViewScale, origin);
internal void RestoreViewport((float Scale, PointF Origin) viewport)
{
+1 -1
View File
@@ -45,7 +45,7 @@ Existing full-width vertical cut-offs inside the used span become fixed strip bo
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.
For **Plate > Sequence Parts > Sequence by Path**, drag lines over the sheet with the left button in the order you want the parts cut. Lifting the button between lines is a rapid: the dashed jump to the next line's numbered start claims no parts. Each part belongs to the line passing closest to its bounding-box center, and lines are cut in drawing order, each in the order its nearest points fall along it; where a line passes a part more than once, the closest pass decides. The plate renumbers after every line. **Undo Line**, Backspace or Ctrl+Z removes the last line; **Clear** or right-click removes them all and restores the order the tool started with. Cut-first parts stay ahead of the rest, and cutoffs follow the same dependency rules as Part Sequencing below. Escape mid-drag drops only that line; otherwise Escape, **Done** or closing the window leaves the tool.
For **Plate > Sequence Parts > Sequence by Path**, drag lines over the sheet with the left button in the order you want the parts cut. Lifting the button between lines is a rapid: the dashed jump to the next line's numbered start claims no parts. Each part belongs to the line passing closest to its bounding-box center, and lines are cut in drawing order, each in the order its nearest points fall along it; where a line passes a part more than once, the closest pass decides. The numbers update as you drag, and the plate is reordered when you release. **Undo Line**, Backspace or Ctrl+Z removes the last line; **Clear** or right-click removes them all and restores the order the tool started with. Cut-first parts stay ahead of the rest, and cutoffs follow the same dependency rules as Part Sequencing below. Escape mid-drag drops only that line; otherwise Escape, **Done** or closing the window leaves the tool.
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 cutoff that splits the whole sheet — one continuous cut from sheet edge to sheet edge, such as a tail separator — goes after every part and every other cutoff, so the head never travels back across the separated edge. Cutoffs that cross no part but leave gaps keep their normal route position. Cutoff geometry, clearance, part programs, placements, and quantities are unchanged.