diff --git a/OpenNest.Engine/Sequencing/DrawnPathSequencing.cs b/OpenNest.Engine/Sequencing/DrawnPathSequencing.cs index 1ecc37cb..e0389e1a 100644 --- a/OpenNest.Engine/Sequencing/DrawnPathSequencing.cs +++ b/OpenNest.Engine/Sequencing/DrawnPathSequencing.cs @@ -18,22 +18,31 @@ namespace OpenNest.Engine.Sequencing /// Reorders the plate's parts; false when no line has length or nothing moves. public static bool Apply(Plate plate, IReadOnlyList> 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; } + /// + /// The order would give the plate, without changing it; null when + /// no line has length or there is nothing to order. + /// + public static List CutOrder(Plate plate, IReadOnlyList> 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); + } + /// The parts in line order; ties keep their current order. public static List Order(IEnumerable parts, IReadOnlyList> lines) { diff --git a/OpenNest.Tests/Sequencing/DrawnPathSequencingTests.cs b/OpenNest.Tests/Sequencing/DrawnPathSequencingTests.cs index 7cda22e6..aeec2edd 100644 --- a/OpenNest.Tests/Sequencing/DrawnPathSequencingTests.cs +++ b/OpenNest.Tests/Sequencing/DrawnPathSequencingTests.cs @@ -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() { diff --git a/OpenNest/Actions/ActionSequenceByPath.cs b/OpenNest/Actions/ActionSequenceByPath.cs index 8a727554..208462ec 100644 --- a/OpenNest/Actions/ActionSequenceByPath.cs +++ b/OpenNest/Actions/ActionSequenceByPath.cs @@ -92,9 +92,26 @@ namespace OpenNest.Actions if (plateView.LengthWorldToGui(point.DistanceTo(drawing[^1])) < PointSpacingPixels) return; drawing.Add(point); + UpdatePreview(); plateView.Invalidate(); } + /// Numbers the parts as if the line being drawn were finished, without reordering the plate. + private void UpdatePreview() + { + var order = DrawnPathSequencing.CutOrder(plateView.Plate, lines.Append(drawing).ToList()); + if (order == null) + { + plateView.PreviewSequence = null; + return; + } + + var numbers = new Dictionary(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(); } diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs index 0ac136a5..9ed3aa8e 100644 --- a/OpenNest/Controls/PlateRenderer.cs +++ b/OpenNest/Controls/PlateRenderer.cs @@ -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); diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 675a6683..8001f659 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -312,6 +312,12 @@ namespace OpenNest.Controls internal OverlapOverlayController OverlapOverlay => overlapOverlay; internal ValidationOverlay ValidationOverlay => validationOverlay; internal CutSimulationOverlay CutSimulation => cutSimulation; + + /// + /// Sequence numbers a tool previews in place of the parts' current order, without + /// reordering the plate; null shows the plate's own order. + /// + internal IReadOnlyDictionary PreviewSequence { get; set; } internal (float Scale, PointF Origin) CaptureViewport() => (ViewScale, origin); internal void RestoreViewport((float Scale, PointF Origin) viewport) { diff --git a/docs/automatic-scrap-cutoffs.md b/docs/automatic-scrap-cutoffs.md index 25ae914d..6478584d 100644 --- a/docs/automatic-scrap-cutoffs.md +++ b/docs/automatic-scrap-cutoffs.md @@ -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.