mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-01 21:41:37 -05:00
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:
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();
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user