feat(desktop): undo cut-offs, sequencing, lead-ins and cutting plans

Cutting edits were unrecorded, so Undo skipped them or folded them into
an anonymous "Change". Each is now one labelled step: Add Cut-Off, Move
Cut-Off (one step per drag), Automatic Cut-Offs, Auto Sequence (current
plate or all plates, only when the order changed), Set Sequence (per
click), Sequence by Path (the whole tool session; Backspace and Ctrl+Z
inside it still take back one line), Place Lead-In, Remove Lead-Ins (the
tool's menu, the plate and the nest commands), Cutting Parameters (an
edited lead-in panel when the tool closes), Plan Cutting and Clear
Lead-ins and Cut-offs.

Plan Cutting stages scrap cut-offs on the live plates before it plans,
so its scope opens with the staging and records only when the plan is
applied; closing without applying records nothing. Clearing applied work
is its own step and no longer warns that it cannot be undone.

Opening the lead-in tool used to give a plate without parameters the
defaults straight away; the plate now takes the tool's parameters with
its first lead-in, so opening and closing the tool changes nothing.
Undo no longer waits while the tool has a part picked: it ends the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-26 12:11:52 -04:00
1 parent 9a62a5102d
commit 3dd1acd3cf
11 files changed
+425 -40

No files matched your search

@@ -327,7 +327,9 @@ public class CuttingInspectionPanelTests
program.LineTo(2, 4); program.LineTo(2, 2);
program.MoveTo(4, 2); program.LineTo(6, 2); program.LineTo(6, 4);
program.LineTo(4, 4); program.LineTo(4, 2);
plate.Parts.Add(new Part(new Drawing("touching contours", program), new Vector(1, 1)));
var touching = new Drawing("touching contours", program);
editor.Nest.Drawings.Add(touching);
plate.Parts.Add(new Part(touching, new Vector(1, 1)));
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
@@ -772,6 +774,8 @@ public class CuttingInspectionPanelTests
Assert.False(Field<Control>(first, "existingWorkBanner").Visible);
Field<Button>(first, "applyButton").PerformClick();
Assert.All(plate.Parts, part => Assert.True(part.HasManualLeadIns));
Assert.Equal(1, editor.History.UndoCount);
Assert.Equal("Plan Cutting", editor.History.UndoLabel);
plate.CutOffs.Add(new CutOff(new Vector(60, 0), CutOffAxis.Vertical));
plate.RegenerateCutOffs(editor.PlateView.CutOffSettings);
var real = plate.Parts.Where(p => !p.BaseDrawing.IsCutOff).ToArray();
@@ -787,7 +791,7 @@ public class CuttingInspectionPanelTests
string? asked = null;
panel.ConfirmClearExisting = message => { asked = message; return false; };
Field<Button>(panel, "clearExistingButton").PerformClick();
Assert.Contains("cannot be undone", asked);
Assert.Contains("Locked and hand-placed lead-ins are removed too", asked);
Assert.Single(plate.CutOffs);
Assert.All(real, part => Assert.True(part.HasManualLeadIns));
@@ -798,6 +802,8 @@ public class CuttingInspectionPanelTests
Assert.Empty(plate.CutOffs);
Assert.Equal(real, plate.Parts);
Assert.All(real, part => Assert.False(part.HasManualLeadIns));
// The clear is its own step, recorded before the replan (the hand-added cut-off before it).
Assert.Equal("Clear Lead-ins and Cut-offs", editor.History.UndoLabel);
Assert.False(banner.Visible);
Assert.True(panel.Proposal.CanApply);
panel.RequestClose();
@@ -939,6 +945,8 @@ public class CuttingInspectionPanelTests
plate.CuttingParameters = Parameters();
plate.Parts.Add(Square("far", 12));
plate.Parts.Add(Square("near", 1));
foreach (var part in plate.Parts)
nest.Drawings.Add(part.BaseDrawing);
return new EditNestForm(nest);
}
@@ -1,5 +1,6 @@
using System.Windows.Forms;
using OpenNest.Actions;
using OpenNest.Controls;
using OpenNest.Engine;
using OpenNest.Engine.Strategies;
using OpenNest.Forms;
@@ -182,6 +183,260 @@ public class UndoRecordingTests
env.AssertOneUndoableEdit("Delete Clamp", () => UndoTestEnv.Invoke(action, "Delete", added));
}, NestWithClamps);
// Cutting
[Fact]
public void PlacingACutOffRecordsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.View.ZoomToFit();
env.View.SetAction(typeof(ActionCutOff));
var at = env.View.PointWorldToControl(new Vector(70, 50));
UndoTestEnv.Mouse(env.View, "OnMouseMove", MouseButtons.None, at);
env.AssertOneUndoableEdit("Add Cut-Off", () => UndoTestEnv.Mouse(env.View, "OnMouseDown", MouseButtons.Left, at));
});
[Fact]
public void DraggingACutOffRecordsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.View.ZoomToFit();
var grab = env.View.PointWorldToControl(new Vector(70, 60));
env.AssertOneUndoableEdit("Move Cut-Off", () =>
{
UndoTestEnv.Mouse(env.View, "OnMouseMove", MouseButtons.None, grab);
UndoTestEnv.Mouse(env.View, "OnMouseDown", MouseButtons.Left, grab);
for (var step = 1; step <= 4; step++)
UndoTestEnv.Mouse(env.View, "OnMouseMove", MouseButtons.Left, new System.Drawing.Point(grab.X + step * 5, grab.Y));
UndoTestEnv.Mouse(env.View, "OnMouseUp", MouseButtons.Left, new System.Drawing.Point(grab.X + 20, grab.Y));
});
Assert.Equal(1, env.Editor.History.UndoCount);
}, NestWithCutOff);
[Fact]
public void DeletingASelectedCutOffRecordsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.View.Selection.SelectedCutOffs.Add(env.View.Plate.CutOffs[0]);
env.AssertOneUndoableEdit("Delete", env.View.Selection.DeleteSelected);
}, NestWithCutOff);
[Fact]
public void AutomaticCutOffsRecordOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
using var form = new AutomaticCutOffForm(env.View, env.Nest.Units);
env.AssertOneUndoableEdit("Automatic Cut-Offs", () => UndoTestEnv.Invoke(form, "ApplyButton_Click", null, EventArgs.Empty));
});
[Fact]
public void AutoSequencingRecordsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.AssertOneUndoableEdit("Auto Sequence", env.Editor.AutoSequenceCurrentPlate);
env.AssertOneUndoableEdit("Auto Sequence All Plates", env.Editor.AutoSequenceAllPlates);
}, NestOutOfOrder);
[Fact]
public void SetSequenceRecordsOneEntryPerClick() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.View.ZoomToFit();
env.View.SetAction(typeof(ActionSetSequence));
var action = env.View.Actions.CurrentAction;
var second = env.View.PointWorldToControl(env.View.Plate.Parts[1].BoundingBox.Center);
env.AssertOneUndoableEdit("Set Sequence", () => UndoTestEnv.Invoke(action, "plateView_MouseClick", env.View,
new MouseEventArgs(MouseButtons.Left, 1, second.X, second.Y, 0)));
env.View.EndAction();
});
[Fact]
public void SequenceByPathRecordsTheWholeSessionAsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.View.ZoomToFit();
var parts = env.View.Plate.Parts;
var first = env.View.PointWorldToControl(parts[1].BoundingBox.Center);
var second = env.View.PointWorldToControl(parts[0].BoundingBox.Center);
env.AssertOneUndoableEdit("Sequence by Path", () =>
{
env.View.SetAction(typeof(ActionSequenceByPath));
var action = env.View.Actions.CurrentAction!;
Click(env.View, first);
Click(env.View, second);
// A line taken back inside the tool is the tool's own step, not the nest's.
Assert.True(action.TryUndoStep());
Click(env.View, second);
Assert.Equal(0, env.Editor.History.UndoCount);
env.View.EndAction();
});
});
[Fact]
public void PlacingALeadInRecordsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(3);
env.View.ZoomToFit();
var part = env.View.Plate.Parts[0];
env.Editor.PlaceLeadIn_Click(null!, EventArgs.Empty);
Assert.Equal(0, env.Editor.History.UndoCount); // Opening the tool changes nothing.
env.AssertOneUndoableEdit("Place Lead-In", () =>
{
PlaceLeadIn(env);
Assert.True(part.HasManualLeadIns);
});
env.View.EndAction();
});
[Fact]
public void TheLeadInToolRecordsItsMenuRemovalAndAParameterChange() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(3);
env.View.ZoomToFit();
env.Editor.PlaceLeadIn_Click(null!, EventArgs.Empty);
var action = env.View.Actions.CurrentAction!;
var center = PlaceLeadIn(env);
Assert.Equal("Place Lead-In", env.Editor.History.UndoLabel);
Assert.True(action.IsBusy(), "The square is no longer picked.");
env.AssertOneUndoableEdit("Remove Lead-Ins", () =>
{
UndoTestEnv.Invoke(action, "ShowContextMenu", center);
var menu = (ContextMenuStrip)action.GetType().GetField("contextMenu", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(action)!;
menu.Items[0].PerformClick();
menu.Close();
});
// Undo ended the tool; a changed panel is recorded when the tool closes.
env.Editor.PlaceLeadIn_Click(null!, EventArgs.Empty);
action = env.View.Actions.CurrentAction!;
var panel = (CuttingPanel)action.GetType().GetField("cuttingPanel", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(action)!;
panel.LoadFromParameters(new CNC.CuttingStrategy.CuttingParameters
{
ExternalLeadIn = new CNC.CuttingStrategy.LineLeadIn { Length = 0.75, ApproachAngle = 30 },
});
env.AssertOneUndoableEdit("Cutting Parameters", env.View.EndAction);
});
[Fact]
public void ClosingTheLeadInToolUnchangedRecordsNothing() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.Editor.PlaceLeadIn_Click(null!, EventArgs.Empty);
env.View.EndAction();
Assert.Equal(0, env.Editor.History.UndoCount);
}, NestWithLeadIns);
[Fact]
public void RemovingLeadInsRecordsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.AssertOneUndoableEdit("Remove Lead-Ins", () => env.Editor.RemoveLeadIns_Click(null!, EventArgs.Empty));
env.AssertOneUndoableEdit("Remove Lead-Ins", env.Editor.RemoveLeadInsAllPlates);
}, NestWithLeadIns);
[Fact]
public void ApplyingACuttingPlanRecordsOneEntry() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
env.AssertOneUndoableEdit("Plan Cutting", () =>
{
env.Editor.OpenCuttingInspection();
var panel = env.Editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
UndoTestEnv.Invoke(panel, "Apply");
PumpUntil(() => env.Editor.CuttingInspection == null);
});
});
[Fact]
public void ClosingACuttingPlanWithoutApplyingRecordsNothing() => UndoTestEnv.Run(env =>
{
env.Editor.PlateManager.LoadAt(0);
var before = UndoTestEnv.Describe(env.Nest);
env.Editor.OpenCuttingInspection();
var panel = env.Editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
panel.RequestClose();
PumpUntil(() => env.Editor.CuttingInspection == null);
Assert.Equal(0, env.Editor.History.UndoCount);
Assert.Equal(before, UndoTestEnv.Describe(env.Nest));
});
// Picks the current plate's first part, locks its left edge and places a lead-in there;
// returns the pick point.
private static System.Drawing.Point PlaceLeadIn(UndoTestEnv env)
{
var box = env.View.Plate.Parts[0].BoundingBox;
var center = env.View.PointWorldToControl(box.Center);
var edge = env.View.PointWorldToControl(new Vector(box.Left, box.Center.Y));
UndoTestEnv.Mouse(env.View, "OnMouseMove", MouseButtons.None, center);
UndoTestEnv.Mouse(env.View, "OnMouseDown", MouseButtons.Left, center);
UndoTestEnv.Mouse(env.View, "OnMouseMove", MouseButtons.None, edge);
UndoTestEnv.Mouse(env.View, "OnMouseDown", MouseButtons.Left, edge);
UndoTestEnv.Mouse(env.View, "OnMouseMove", MouseButtons.None, edge);
UndoTestEnv.Mouse(env.View, "OnMouseDown", MouseButtons.Left, edge);
return center;
}
private static void Click(PlateView view, System.Drawing.Point at)
{
UndoTestEnv.Mouse(view, "OnMouseMove", MouseButtons.None, at);
UndoTestEnv.Mouse(view, "OnMouseDown", MouseButtons.Left, at);
UndoTestEnv.Mouse(view, "OnMouseUp", MouseButtons.Left, at);
}
private static void PumpUntil(Func<bool> condition)
{
var deadline = DateTime.UtcNow.AddSeconds(30);
while (!condition())
{
Assert.True(DateTime.UtcNow < deadline, "Timed out.");
Application.DoEvents();
Thread.Sleep(5);
}
}
private static Nest NestWithCutOff()
{
var nest = UndoTestEnv.BuildNest();
nest.Plates[0].CutOffs.Add(new CutOff(new Vector(70, 0), CutOffAxis.Vertical));
nest.Plates[0].RegenerateCutOffs(new CutOffSettings());
return nest;
}
// Three squares in a row, cut middle, far, near: any sequencer reorders them.
private static Nest NestOutOfOrder()
{
var nest = UndoTestEnv.BuildNest();
var square = nest.Drawings.Single();
foreach (var plate in nest.Plates.Take(2))
{
plate.Parts.Clear();
foreach (var x in new[] { 35.0, 65.0, 5.0 })
plate.Parts.Add(new Part(square, new Vector(x, 5)));
}
return nest;
}
private static Nest NestWithLeadIns()
{
var nest = UndoTestEnv.BuildNest();
var parameters = new CNC.CuttingStrategy.CuttingParameters();
foreach (var plate in nest.Plates.Where(p => p.Parts.Count > 0))
{
plate.CuttingParameters = parameters;
foreach (var part in plate.Parts)
part.ApplyLeadIns(parameters, new Vector(0, 0));
}
return nest;
}
private static Nest NestWithClamps()
{
var nest = UndoTestEnv.BuildNest();
+9 -2
View File
@@ -130,11 +130,18 @@ internal sealed record UndoTestEnv(MainForm Host, EditNestForm Editor)
}
foreach (var plate in nest.Plates)
{
Line($"plate {plate.Size} x{plate.Quantity} spacing {plate.PartSpacing} edge {plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top} quadrant {plate.Quadrant} params {plate.CuttingParameters != null}");
Line($"plate {plate.Size} x{plate.Quantity} spacing {plate.PartSpacing} edge {plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top} quadrant {plate.Quadrant}");
Line($" params {(plate.CuttingParameters == null ? "none" : IO.CuttingParametersSerializer.Serialize(plate.CuttingParameters))}");
var clamps = plate.ClampZones.Select(z => FormattableString.Invariant($"{z.Id}@{z.Bounds.X:F4},{z.Bounds.Y:F4},{z.Bounds.Width:F4},{z.Bounds.Length:F4}"));
Line($" clamps [{string.Join(";", clamps)}] removed {plate.RemovedClamps.Count} support {plate.SupportBed != null} stock {string.Join(",", plate.StockItemCodes)}");
foreach (var part in plate.Parts)
Line($" part {part.BaseDrawing.Name} {part.Location.X:F4},{part.Location.Y:F4} r{part.Rotation:F6} leads {part.HasManualLeadIns} locked {part.LeadInsLocked} codes {part.Program.Length}");
{
// A cut-off's part is regenerated from its definition, listed below; only its place in the order counts.
if (part.BaseDrawing.IsCutOff)
Line($" cutoff part");
else
Line($" part {part.BaseDrawing.Name} {part.Location.X:F4},{part.Location.Y:F4} r{part.Rotation:F6} leads {part.HasManualLeadIns} locked {part.LeadInsLocked} codes {part.Program.Length}");
}
foreach (var cutOff in plate.CutOffs)
Line($" cutoff {cutOff.Axis} {cutOff.Position.X:F4},{cutOff.Position.Y:F4}");
}
+3
View File
@@ -66,9 +66,12 @@ namespace OpenNest.Actions
var pt = plateView.CurrentPoint;
var cutoff = new CutOff(pt, lockedAxis) { HeadLockOvercut = settings.NewCutOffHeadLockOvercut };
using var change = plateView.BeginChange("Add Cut-Off");
change.MarkChanged();
plateView.Plate.CutOffs.Add(cutoff);
plateView.Plate.RegenerateCutOffs(settings);
plateView.Invalidate();
change.Complete();
}
private void OnKeyDown(object sender, KeyEventArgs e)
+27 -3
View File
@@ -39,6 +39,9 @@ namespace OpenNest.Actions
private ShapeInfo lockedContour;
private ContextMenuStrip contextMenu;
private CuttingPanel cuttingPanel;
// The panel's parameters as loaded, to tell an edit from the panel's own round trip.
private string loadedParameters;
private static readonly Brush grayOverlay = new SolidBrush(
Color.FromArgb(160, 180, 180, 180)
);
@@ -86,6 +89,9 @@ namespace OpenNest.Actions
public override bool IsBusy() => selectedPart != null;
// A picked part is not work in progress for undo, which ends the tool.
public override bool BlocksUndo => false;
// Escape closes the side panel; a second Escape must not reopen it.
public override bool ResumeOnEscape => false;
@@ -101,6 +107,7 @@ namespace OpenNest.Actions
// Load current parameters or defaults
var plate = plateView.Plate;
cuttingPanel.LoadFromParameters(plate?.CuttingParameters ?? MachineSettings.DefaultCuttingParameters(form.Nest));
loadedParameters = CuttingParametersSerializer.Serialize(cuttingPanel.BuildParameters());
form.ShowSidePanel(cuttingPanel, "Place Lead-in");
}
@@ -137,8 +144,16 @@ namespace OpenNest.Actions
Properties.Settings.Default.CuttingParametersJson = json;
Properties.Settings.Default.Save();
if (plateView.Plate != null)
plateView.Plate.CuttingParameters = parameters;
// A plate takes the tool's parameters with its first lead-in; after that an edit in
// the panel is a change of its own. Closing the tool otherwise changes nothing.
var plate = plateView.Plate;
if (plate?.CuttingParameters == null || json == loadedParameters
|| CuttingParametersSerializer.Serialize(plate.CuttingParameters) == json)
return;
using var change = plateView.BeginChange("Cutting Parameters");
change.MarkChanged();
plate.CuttingParameters = parameters;
change.Complete();
}
private void OnToolParametersChanged(object sender, System.EventArgs e)
@@ -483,14 +498,18 @@ namespace OpenNest.Actions
private void CommitLeadIn()
{
using var change = plateView.BeginChange("Place Lead-In");
change.MarkChanged();
if (selectedPart.HasManualLeadIns)
selectedPart.RemoveLeadIns();
plateView.Plate.CuttingParameters ??= GetCurrentParameters();
// A part the support grid cannot hold level is tabbed at the clicked entry.
var parameters = Support.SupportTabSettings.ForManualLeadIn(GetCurrentParameters(),
plateView.Plate, selectedPart);
selectedPart.ApplySingleLeadIn(parameters, snapPoint, snapEntity, snapContourType);
selectedPart.LeadInsLocked = true;
change.Complete();
selectedLayoutPart.IsDirty = true;
UnlockContour();
@@ -523,7 +542,12 @@ namespace OpenNest.Actions
var removeItem = new ToolStripMenuItem("Remove All Lead-ins");
removeItem.Click += (s, e) =>
{
selectedPart.RemoveLeadIns();
using (var change = plateView.BeginChange("Remove Lead-Ins"))
{
change.MarkChanged();
selectedPart.RemoveLeadIns();
change.Complete();
}
selectedLayoutPart.IsDirty = true;
selectedLayoutPart.Update();
DeselectPart();
+11
View File
@@ -7,6 +7,7 @@ using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Engine.Sequencing;
using OpenNest.Geometry;
using OpenNest.Undo;
namespace OpenNest.Actions
{
@@ -40,6 +41,10 @@ namespace OpenNest.Actions
private bool disconnecting;
private IReadOnlySet<Part> muted;
// Open from the first resequence until the tool ends: the whole session undoes as one
// step, while Backspace or Ctrl+Z inside it still take back one line at a time.
private ChangeScope change;
// Starting the tool again builds the new one before this one is cancelled and
// disconnected, so this one must leave the view's muting to its successor.
private bool OwnsView => ReferenceEquals(plateView.MutedParts, muted);
@@ -184,6 +189,8 @@ namespace OpenNest.Actions
private void Resequence()
{
var plate = plateView.Plate;
change ??= plateView.BeginChange("Sequence by Path");
change.MarkChanged();
if (lines.Count > 0)
DrawnPathSequencing.Apply(plate, lines);
else if (plate.Parts.Count == originalOrder.Count && originalOrder.All(plate.Parts.Contains))
@@ -241,6 +248,10 @@ namespace OpenNest.Actions
public override void DisconnectEvents()
{
disconnecting = true;
var scope = change;
change = null;
scope?.Complete();
scope?.Dispose();
plateView.MouseDown -= plateView_MouseDown;
plateView.MouseMove -= plateView_MouseMove;
plateView.MouseUp -= plateView_MouseUp;
+10 -1
View File
@@ -50,8 +50,17 @@ namespace OpenNest.Actions
// 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))
if (part == null)
return;
using (var change = plateView.BeginChange("Set Sequence"))
{
var moved = ManualPartSequencing.Move(plateView.Plate, part, (int)sequenceForm.numericUpDown1.Value);
if (moved)
change.MarkChanged();
change.Complete();
if (!moved)
return;
}
if (sequenceForm.numericUpDown1.Value < sequenceForm.numericUpDown1.Maximum)
sequenceForm.numericUpDown1.Value++;
+11
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Undo;
namespace OpenNest.Controls
{
@@ -9,6 +10,9 @@ namespace OpenNest.Controls
private readonly PlateView view;
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
// Open from the press on a cut-off to the release: a drag undoes as one move.
private ChangeScope drag;
public CutOffHandler(PlateView view)
{
this.view = view;
@@ -24,6 +28,7 @@ namespace OpenNest.Controls
IsDragging = true;
dragPerimeterCache = Plate.BuildPerimeterCache(view.Plate);
drag = view.BeginChange("Move Cut-Off");
return hitCutOff;
}
@@ -32,6 +37,7 @@ namespace OpenNest.Controls
if (!IsDragging || cutOff == null)
return;
drag?.MarkChanged();
if (cutOff.Axis == CutOffAxis.Vertical)
cutOff.Position = new Vector(currentPoint.X, cutOff.Position.Y);
else
@@ -50,6 +56,11 @@ namespace OpenNest.Controls
dragPerimeterCache = null;
view.Plate.RegenerateCutOffs(view.CutOffSettings);
view.Invalidate();
var scope = drag;
drag = null;
scope?.Complete();
scope?.Dispose();
}
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
+10 -1
View File
@@ -375,7 +375,16 @@ public partial class AutomaticCutOffForm : Form
try
{
// The batch service replans every plate before mutating any; never accept previews.
var (plates, plans) = Calculate(apply: true);
Plate[] plates;
IReadOnlyList<AutomaticCutOffPlan> plans;
using (var change = plateView.BeginChange("Automatic Cut-Offs"))
{
(plates, plans) = Calculate(apply: true);
// A blocked batch changes nothing.
if (!plans.Any(p => p.HasBlockingDiagnostics) && plans.Any(p => p.Definitions.Count > 0))
change.MarkChanged();
change.Complete();
}
DisplayPlans(plates, plans);
if (plans.Any(p => p.HasBlockingDiagnostics) || plans.All(p => p.Definitions.Count == 0))
return;
+36 -6
View File
@@ -12,6 +12,7 @@ using OpenNest.Controls;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Engine.Sequencing;
using OpenNest.Undo;
namespace OpenNest.Forms;
@@ -98,6 +99,10 @@ internal sealed class CuttingInspectionPanel : UserControl
// Last cut-off settings chosen here, per unit system, for the rest of this session.
private static readonly System.Collections.Generic.Dictionary<Units, AutomaticCutOffOptions> lastCutOffOptions = new();
private AutomaticCutOffStaging stagedCutOffs;
// Open from staging scrap cut-offs (or the apply) until the plan is applied or the staged
// cut-offs come back off: they become an edit only when the plan is applied.
private ChangeScope planChange;
private readonly CheckBox acceptWarningsCheckBox = new() { Text = "Accept the unverified plan", AutoSize = true, Visible = false };
private readonly Label settingsLabel = new()
{
@@ -1107,12 +1112,15 @@ internal sealed class CuttingInspectionPanel : UserControl
if (!applyButton.Enabled || IsPlanning || invalidated || Proposal == null)
return;
// This handler runs on the owner thread; Apply independently rechecks all captured plates.
BeginPlanChange();
CommitResult = Proposal.Apply(acceptWarningsCheckBox.Checked);
if (CommitResult.Status == CuttingCommitStatus.Applied)
{
// The plan was made with these cut-offs in place; applying it keeps them.
stagedCutOffs?.Keep();
stagedCutOffs = null;
planChange.MarkChanged();
EndPlanChange();
Exception postApplyError = null;
try
{
@@ -1182,6 +1190,7 @@ internal sealed class CuttingInspectionPanel : UserControl
}
if (stagedCutOffs != null)
return true;
BeginPlanChange();
var staging = AutomaticCutOffBatch.Stage(plates, cutOffOptions, view.CutOffSettings);
if (staging.IsBlocked)
{
@@ -1218,11 +1227,24 @@ internal sealed class CuttingInspectionPanel : UserControl
{
var staged = stagedCutOffs;
stagedCutOffs = null;
if (staged == null)
return;
staged.Revert();
if (!view.IsDisposed)
view.Invalidate();
if (staged != null)
{
staged.Revert();
if (!view.IsDisposed)
view.Invalidate();
}
EndPlanChange();
}
private void BeginPlanChange() => planChange ??= editor.BeginChange("Plan Cutting");
// Records the plan once it was marked applied; otherwise the plates are as they were.
private void EndPlanChange()
{
var change = planChange;
planChange = null;
change?.Complete();
change?.Dispose();
}
private void RefreshExistingWork()
@@ -1246,7 +1268,7 @@ internal sealed class CuttingInspectionPanel : UserControl
var message = $"Remove the applied lead-ins ({summary.LeadInParts} parts, {summary.LockedLeadInParts} locked) and "
+ $"{summary.CutOffs} cut-offs from plate(s) {string.Join(", ", summary.PlateNumbers)}?"
+ Environment.NewLine + Environment.NewLine
+ "Locked and hand-placed lead-ins are removed too, and this cannot be undone. The plan is then recalculated.";
+ "Locked and hand-placed lead-ins are removed too. The plan is then recalculated.";
var confirmed = ConfirmClearExisting?.Invoke(message)
?? MessageBox.Show(editor, message, "Clear Lead-ins and Cut-offs", MessageBoxButtons.YesNo,
MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes;
@@ -1271,9 +1293,13 @@ internal sealed class CuttingInspectionPanel : UserControl
private bool ClearAppliedWork()
{
ClearResult();
ChangeScope change = null;
try
{
RevertStagedCutOffs();
// Recorded even when it stops part-way: the plates keep what was cleared.
change = editor.BeginChange("Clear Lead-ins and Cut-offs");
change.MarkChanged();
AppliedCuttingWork.Clear(plates, plateNumbers);
}
catch (Exception ex)
@@ -1284,6 +1310,8 @@ internal sealed class CuttingInspectionPanel : UserControl
}
finally
{
change?.Complete();
change?.Dispose();
foreach (var part in view.Parts)
{
part.IsDirty = true;
@@ -1347,6 +1375,8 @@ internal sealed class CuttingInspectionPanel : UserControl
view.InspectionCloseRequested = null;
try { ownedScope?.Dispose(); }
catch (Exception ex) { restoreError ??= ex; }
try { EndPlanChange(); }
catch (Exception ex) { restoreError ??= ex; }
try { editor.FinishCuttingInspection(this); }
catch (Exception ex) { restoreError ??= ex; }
try
+43 -25
View File
@@ -1022,17 +1022,32 @@ namespace OpenNest.Forms
public void AutoSequenceCurrentPlate()
{
SequencePlate(PlateView.Plate);
var plate = PlateView.Plate;
using var change = BeginChange("Auto Sequence");
var before = CutOrder(plate).ToList();
SequencePlate(plate);
if (!CutOrder(plate).SequenceEqual(before))
change.MarkChanged();
change.Complete();
PlateView.Invalidate();
}
public void AutoSequenceAllPlates()
{
using var change = BeginChange("Auto Sequence All Plates");
var before = Nest.Plates.SelectMany(CutOrder).ToList();
var parameters = new SequenceParameters { Method = SequenceMethod.LeastCode };
PlateSequencing.ApplyAll(Nest.Plates, parameters);
if (!Nest.Plates.SelectMany(CutOrder).SequenceEqual(before))
change.MarkChanged();
change.Complete();
PlateView.Invalidate();
}
// Sequencing reorders parts and cut-offs; a plate already in order is left as it was.
private static IEnumerable<object> CutOrder(Plate plate) =>
plate.Parts.Cast<object>().Concat(plate.CutOffs);
private static void SequencePlate(Plate plate)
{
var parameters = new SequenceParameters { Method = SequenceMethod.LeastCode };
@@ -1224,21 +1239,21 @@ namespace OpenNest.Forms
return;
var plate = PlateView.Plate;
var count = 0;
foreach (var part in plate.Parts)
{
if (part.HasManualLeadIns)
{
part.RemoveLeadIns();
count++;
}
}
if (count == 0)
if (!plate.Parts.Any(part => part.HasManualLeadIns))
return;
plate.CuttingParameters = null;
using (var change = BeginChange("Remove Lead-Ins"))
{
change.MarkChanged();
foreach (var part in plate.Parts)
{
if (part.HasManualLeadIns)
part.RemoveLeadIns();
}
plate.CuttingParameters = null;
change.Complete();
}
// Rebuild all layout part graphics
foreach (var lp in PlateView.Parts)
@@ -1255,15 +1270,21 @@ namespace OpenNest.Forms
if (Nest == null)
return;
foreach (var plate in Nest.Plates)
using (var change = BeginChange("Remove Lead-Ins"))
{
foreach (var part in plate.Parts)
if (Nest.Plates.Any(plate => plate.CuttingParameters != null || plate.Parts.Any(part => part.HasManualLeadIns)))
change.MarkChanged();
foreach (var plate in Nest.Plates)
{
if (part.HasManualLeadIns)
part.RemoveLeadIns();
}
foreach (var part in plate.Parts)
{
if (part.HasManualLeadIns)
part.RemoveLeadIns();
}
plate.CuttingParameters = null;
plate.CuttingParameters = null;
}
change.Complete();
}
foreach (var lp in PlateView.Parts)
@@ -1280,11 +1301,8 @@ namespace OpenNest.Forms
if (PlateView?.Plate == null)
return;
var plate = PlateView.Plate;
if (plate.CuttingParameters == null)
plate.CuttingParameters = MachineSettings.DefaultCuttingParameters(Nest);
// The tool's panel starts from the plate's parameters or the defaults; the plate
// takes them with its first lead-in, so opening the tool changes nothing.
PlateView.SetAction(typeof(Actions.ActionLeadIn));
}