mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 17:11:06 -04:00
feat(ui): choose Plan Cutting's plate scope in the panel
The panel gains a Current plate only / All plates selector, shown when the two scopes differ. Changing it takes the unapplied scrap cut-offs off, cancels any running plan and replans. The toolbar button opens with the scope last chosen there (default all plates); the Plate and Nest menu items keep their fixed scopes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87e0a70878
commit
fcb61f3b3e
7 files changed
+228
-14
No files matched your search
@@ -187,6 +187,88 @@ public class CuttingInspectionPanelTests
|
||||
Assert.Same(original, editor.PlateView.Plate);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ScopeSelectorReplansForAllPlatesAndBackAndRemembersTheChoice() => RunSta(() =>
|
||||
{
|
||||
using var editor = TwoPlateEditor();
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
var remembered = new List<bool>();
|
||||
panel.RememberScope = remembered.Add;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
var scope = Field<ComboBox>(panel, "scopeSelector");
|
||||
Assert.True(scope.Visible);
|
||||
Assert.Equal(0, scope.SelectedIndex);
|
||||
Assert.Equal("All plates (2)", scope.Items[1]);
|
||||
Assert.Single(panel.Proposal.Plates);
|
||||
|
||||
scope.SelectedIndex = 1;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.True(panel.AllPlates);
|
||||
Assert.Equal(2, panel.Proposal.Plates.Count);
|
||||
Assert.True(Field<Button>(panel, "nextButton").Visible);
|
||||
Assert.Contains("2 plates", Field<Label>(panel, "statusLabel").Text);
|
||||
|
||||
scope.SelectedIndex = 0;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.False(panel.AllPlates);
|
||||
Assert.Single(panel.Proposal.Plates);
|
||||
Assert.False(Field<Button>(panel, "nextButton").Visible);
|
||||
Assert.Equal(new[] { true, false }, remembered);
|
||||
panel.RequestClose();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ScopeChangedMidPlanReplansOnceTheRunningPlanCancels() => RunSta(() =>
|
||||
{
|
||||
using var editor = TwoPlateEditor();
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
using var entered = new ManualResetEventSlim();
|
||||
using var release = new ManualResetEventSlim();
|
||||
var runs = 0;
|
||||
editor.CuttingBeforePlan = token =>
|
||||
{
|
||||
if (Interlocked.Increment(ref runs) > 1) return;
|
||||
entered.Set();
|
||||
release.Wait(TimeSpan.FromSeconds(30));
|
||||
Assert.True(token.IsCancellationRequested);
|
||||
};
|
||||
try
|
||||
{
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
panel.RememberScope = _ => { };
|
||||
Assert.True(entered.Wait(TimeSpan.FromSeconds(30)));
|
||||
Field<ComboBox>(panel, "scopeSelector").SelectedIndex = 1;
|
||||
Assert.False(panel.AllPlates); // The running plan still reads the old scope.
|
||||
release.Set();
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.True(panel.AllPlates);
|
||||
Assert.Equal(2, panel.Proposal.Plates.Count);
|
||||
panel.RequestClose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
release.Set();
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ScopeSelectorIsHiddenWhenOnlyOnePlateHasParts() => RunSta(() =>
|
||||
{
|
||||
using var editor = Editor();
|
||||
editor.Show();
|
||||
editor.PlateManager.LoadAt(0);
|
||||
editor.OpenCuttingInspection();
|
||||
var panel = editor.CuttingInspection!;
|
||||
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
|
||||
Assert.False(Field<ComboBox>(panel, "scopeSelector").Visible);
|
||||
panel.RequestClose();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void BlockedOverlapRowShadesTheOverlapOnTheLivePlate() => RunSta(() =>
|
||||
{
|
||||
@@ -950,6 +1032,16 @@ public class CuttingInspectionPanelTests
|
||||
return new EditNestForm(nest);
|
||||
}
|
||||
|
||||
private static EditNestForm TwoPlateEditor()
|
||||
{
|
||||
var editor = Editor();
|
||||
var second = editor.Nest.Plates[^1]; // Fill the managed empty sentinel; a new one is appended.
|
||||
second.Size = new OpenNest.Geometry.Size(100, 100);
|
||||
second.Parts.Add(Square("left", 1));
|
||||
second.Parts.Add(Square("right", 20));
|
||||
return editor;
|
||||
}
|
||||
|
||||
private static Part Square(string name, double x)
|
||||
{
|
||||
var program = new OpenNest.CNC.Program();
|
||||
|
||||
@@ -25,8 +25,8 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
private readonly PlateView view;
|
||||
private readonly Plate plate;
|
||||
private CuttingParameters parameters;
|
||||
private readonly Plate[] plates;
|
||||
private readonly int[] plateNumbers;
|
||||
private Plate[] plates;
|
||||
private int[] plateNumbers;
|
||||
private int selectedPlate;
|
||||
private Plate preview;
|
||||
private bool populatingWarnings;
|
||||
@@ -36,6 +36,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
private readonly OverlapDisplayMode overlapDisplay;
|
||||
private CuttingProposalInspection inspection = new();
|
||||
private readonly ComboBox modeSelector = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 220 };
|
||||
private readonly ComboBox scopeSelector = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 220 };
|
||||
private CancellationTokenSource validationCancellation;
|
||||
private bool closeRequested;
|
||||
private CuttingInspectionMode? pendingMode;
|
||||
@@ -43,6 +44,8 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
private bool pendingReplan;
|
||||
private bool pendingClear;
|
||||
private bool pendingCutOffSettings;
|
||||
// A scope chosen while a plan ran; applied once that run's cancellation lands.
|
||||
private bool? pendingScope;
|
||||
// The running plan's headline, re-shown each second with its elapsed time; null when not ticking.
|
||||
private string planningStatus;
|
||||
private readonly System.Diagnostics.Stopwatch planningClock = new();
|
||||
@@ -94,7 +97,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
};
|
||||
private readonly Button cutOffSettingsButton = new() { Text = "Cut-off Settings…", AutoSize = true };
|
||||
private readonly Units units;
|
||||
private readonly bool allPlates;
|
||||
private bool allPlates;
|
||||
private AutomaticCutOffOptions cutOffOptions;
|
||||
// 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();
|
||||
@@ -155,12 +158,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
this.editor = editor;
|
||||
view = editor.PlateView;
|
||||
plate = view.Plate;
|
||||
var all = editor.Nest.Plates.ToList();
|
||||
plates = allPlates ? all.Where(p => p.Parts.Count > 0).ToArray() : [plate];
|
||||
if (plates.Length == 0)
|
||||
plates = [plate];
|
||||
plateNumbers = plates.Select(p => all.IndexOf(p) + 1).ToArray();
|
||||
selectedPlate = System.Math.Max(0, Array.IndexOf(plates, plate));
|
||||
SetPlates(allPlates);
|
||||
this.parameters = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
|
||||
context = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
|
||||
viewport = view.CaptureViewport();
|
||||
@@ -182,7 +180,11 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
{
|
||||
if (!configuringMode) SwitchMode((CuttingInspectionMode)modeSelector.SelectedIndex);
|
||||
};
|
||||
this.allPlates = allPlates;
|
||||
toolTip.SetToolTip(scopeSelector, "Plan the plate on screen, or every plate with parts in one plan.");
|
||||
scopeSelector.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
if (!configuringMode) ChangeScope(scopeSelector.SelectedIndex == 1);
|
||||
};
|
||||
units = editor.Nest.Units;
|
||||
cutOffOptions = lastCutOffOptions.TryGetValue(units, out var last) ? Copy(last) : DefaultCutOffOptions(units);
|
||||
ShowCutOffOptions();
|
||||
@@ -197,7 +199,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
existingWorkLabel.Font = new Font(Font, FontStyle.Bold);
|
||||
existingWorkBanner.Controls.AddRange([existingWorkLabel, clearExistingButton]);
|
||||
existingWorkBanner.SetTone(Tone.Bad);
|
||||
header.Controls.AddRange([modeSelector, existingWorkBanner, settingsRow, keepOrderCheckBox, cutOffRow,
|
||||
header.Controls.AddRange([modeSelector, scopeSelector, existingWorkBanner, settingsRow, keepOrderCheckBox, cutOffRow,
|
||||
validationScopeLabel, buttons, acceptWarningsCheckBox, navigation, previewLabel, detailLabel]);
|
||||
statusBanner.Controls.Add(statusLabel);
|
||||
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3 };
|
||||
@@ -415,10 +417,11 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
|
||||
private void ConfigureMode()
|
||||
{
|
||||
var planning = Mode == CuttingInspectionMode.Plan;
|
||||
configuringMode = true;
|
||||
modeSelector.SelectedIndex = (int)Mode;
|
||||
ShowScope(planning);
|
||||
configuringMode = false;
|
||||
var planning = Mode == CuttingInspectionMode.Plan;
|
||||
settingsButton.Visible = settingsLabel.Visible = keepOrderCheckBox.Visible = applyButton.Visible = planning;
|
||||
autoCutOffsCheckBox.Visible = cutOffSettingsButton.Visible = planning;
|
||||
RefreshExistingWork();
|
||||
@@ -429,6 +432,89 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
previewLabel.Text = "No current preview.";
|
||||
}
|
||||
|
||||
/// <summary>True when this panel plans every plate with parts rather than only the plate on screen.</summary>
|
||||
internal bool AllPlates => allPlates;
|
||||
|
||||
/// <summary>Saves the scope chosen in the panel for the next toolbar open; tests replace it.</summary>
|
||||
internal Action<bool> RememberScope { get; set; } = all =>
|
||||
{
|
||||
Properties.Settings.Default.CuttingPlanAllPlates = all;
|
||||
Properties.Settings.Default.Save();
|
||||
};
|
||||
|
||||
/// <summary>Scopes the panel to the plate on screen or to every plate with parts (that plate when none has).</summary>
|
||||
private void SetPlates(bool all)
|
||||
{
|
||||
allPlates = all;
|
||||
var nestPlates = editor.Nest.Plates.ToList();
|
||||
var current = nestPlates.Contains(view.Plate) ? view.Plate : plate;
|
||||
plates = ScopePlates(nestPlates, current, all);
|
||||
plateNumbers = plates.Select(p => nestPlates.IndexOf(p) + 1).ToArray();
|
||||
selectedPlate = System.Math.Max(0, Array.IndexOf(plates, current));
|
||||
}
|
||||
|
||||
private static Plate[] ScopePlates(System.Collections.Generic.List<Plate> nestPlates, Plate current, bool all)
|
||||
{
|
||||
var scoped = all ? nestPlates.Where(p => p.Parts.Count > 0).ToArray() : [current];
|
||||
return scoped.Length == 0 ? [current] : scoped;
|
||||
}
|
||||
|
||||
// Shown only when the two scopes would plan different plates.
|
||||
private void ShowScope(bool planning)
|
||||
{
|
||||
var nestPlates = editor.Nest.Plates.ToList();
|
||||
var current = nestPlates.Contains(view.Plate) ? view.Plate : plate;
|
||||
var everyPlate = ScopePlates(nestPlates, current, true);
|
||||
scopeSelector.Items.Clear();
|
||||
scopeSelector.Items.AddRange(["Current plate only", $"All plates ({everyPlate.Length})"]);
|
||||
scopeSelector.SelectedIndex = allPlates ? 1 : 0;
|
||||
scopeSelector.Visible = planning && (allPlates || everyPlate.Length > 1 || !ReferenceEquals(everyPlate[0], current));
|
||||
}
|
||||
|
||||
/// <summary>Replans for the chosen scope; a running plan is cancelled first so the plates never change under it.</summary>
|
||||
internal void ChangeScope(bool all)
|
||||
{
|
||||
if (closed || closeRequested || pendingMode != null || Mode != CuttingInspectionMode.Plan
|
||||
|| all == (pendingScope ?? allPlates))
|
||||
return;
|
||||
RememberScope?.Invoke(all);
|
||||
if (IsPlanning)
|
||||
{
|
||||
pendingScope = all;
|
||||
if (inspection.CloseRequested)
|
||||
return; // Already finishing; the scope is applied when that lands.
|
||||
pendingReplan = true;
|
||||
ClearResult();
|
||||
SetPlanning(true);
|
||||
SetStatus(Tone.Neutral, "Restarting…");
|
||||
inspection.RequestClose();
|
||||
return;
|
||||
}
|
||||
if (ApplyScope(all))
|
||||
StartPlanning();
|
||||
}
|
||||
|
||||
// Takes the unapplied cut-offs off the old scope's plates before rescoping; false when they couldn't come off.
|
||||
private bool ApplyScope(bool all)
|
||||
{
|
||||
ClearResult();
|
||||
try
|
||||
{
|
||||
RevertStagedCutOffs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus(Tone.Bad, "Couldn't remove the unapplied cut-offs.", ex.Message);
|
||||
configuringMode = true;
|
||||
ShowScope(true); // The plates stay in the old scope, so the selector does too.
|
||||
configuringMode = false;
|
||||
return false;
|
||||
}
|
||||
SetPlates(all);
|
||||
ConfigureMode();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void SwitchMode(CuttingInspectionMode mode)
|
||||
{
|
||||
if (closed || closeRequested || mode == Mode && pendingMode == null) return;
|
||||
@@ -704,6 +790,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
if (closeRequested || pendingMode != null)
|
||||
{
|
||||
pendingReplan = pendingClear = pendingCutOffSettings = false;
|
||||
pendingScope = null;
|
||||
FinishTransition();
|
||||
return;
|
||||
}
|
||||
@@ -717,6 +804,17 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
pendingClear = false; // The cancelled run no longer reads the plates.
|
||||
if (!ClearAppliedWork())
|
||||
{
|
||||
pendingScope = null;
|
||||
SetPlanning(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pendingScope is { } scoped)
|
||||
{
|
||||
pendingScope = null; // The clear above, if any, covered the scope it was confirmed for.
|
||||
if (!ApplyScope(scoped))
|
||||
{
|
||||
pendingCutOffSettings = false;
|
||||
SetPlanning(false);
|
||||
return;
|
||||
}
|
||||
@@ -875,7 +973,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
// unless closing or switching; validation still locks them while it runs.
|
||||
var replannable = !running || Mode == CuttingInspectionMode.Plan && !closeRequested && pendingMode == null;
|
||||
settingsButton.Enabled = keepOrderCheckBox.Enabled = planButton.Enabled = replannable;
|
||||
cutOffSettingsButton.Enabled = clearExistingButton.Enabled = replannable;
|
||||
cutOffSettingsButton.Enabled = clearExistingButton.Enabled = scopeSelector.Enabled = replannable;
|
||||
previousButton.Enabled = !running && selectedPlate > 0;
|
||||
nextButton.Enabled = !running && selectedPlate < plates.Length - 1;
|
||||
acceptWarningsCheckBox.Enabled = !running;
|
||||
|
||||
@@ -111,6 +111,10 @@ namespace OpenNest.Forms
|
||||
|
||||
private void NestPlanCutting_Click(object sender, EventArgs e) => ShowCuttingPlan(allPlates: true);
|
||||
|
||||
// The toolbar opens with the scope last chosen in the panel; the menus keep their own.
|
||||
private void ToolbarPlanCutting_Click(object sender, EventArgs e) =>
|
||||
ShowCuttingPlan(Properties.Settings.Default.CuttingPlanAllPlates);
|
||||
|
||||
// Native tests capture this notice without opening a modal dialog in session zero.
|
||||
internal System.Action<string, string> CuttingPlanNotice { get; set; }
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace OpenNest.Forms
|
||||
statusBar = new MainStatusBar(statusLabel1, locationStatusLabel, selectionStatusLabel,
|
||||
machineStatusLabel, materialStatusLabel, plateUtilStatusLabel, ledgerStatusLabel);
|
||||
toolStrip1.Items.Add(CreateActionButton("btnCutPlanner", "Plan Cutting",
|
||||
Resources.cut_planner, PlatePlanCutting_Click));
|
||||
Resources.cut_planner, ToolbarPlanCutting_Click));
|
||||
toolStrip1.Items.Add(CreateActionButton("btnSimulateCutting", "Simulate Cutting",
|
||||
Resources.cut_simulate, SimulateCutting_Click));
|
||||
toolStrip1.Items.Add(CreateActionButton("btnSequenceByPath", "Sequence by Path",
|
||||
|
||||
Generated
+13
@@ -372,6 +372,19 @@ namespace OpenNest.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("True")]
|
||||
// The Plan Cutting toolbar button plans every plate; false plans only the plate on screen.
|
||||
public bool CuttingPlanAllPlates {
|
||||
get {
|
||||
return ((bool)(this["CuttingPlanAllPlates"]));
|
||||
}
|
||||
set {
|
||||
this["CuttingPlanAllPlates"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("False")]
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
<Setting Name="CuttingPlanEffort" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">1</Value>
|
||||
</Setting>
|
||||
<Setting Name="CuttingPlanAllPlates" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">True</Value>
|
||||
</Setting>
|
||||
<Setting Name="CutOffLockHeadHeight" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">False</Value>
|
||||
</Setting>
|
||||
|
||||
@@ -615,6 +615,10 @@ on an `Applied` result, not as a rollback.
|
||||
|
||||
`Plate > Plan Cutting...` opens a cutting inspection panel beside the main plate canvas;
|
||||
`Nest > Plan Cutting (All Plates)...` opens the same panel for every occupied plate.
|
||||
The toolbar's Plan Cutting button opens it with the scope last chosen in the panel
|
||||
(`Settings.CuttingPlanAllPlates`, default all plates). When the two scopes differ, the
|
||||
panel's scope selector switches between **Current plate only** and **All plates**; a
|
||||
change takes the unapplied scrap cut-offs off first, cancels any running plan and replans.
|
||||
It captures scope on the editor thread, plans on a worker, and shows only a fresh,
|
||||
supported detached proposal in that canvas, including proposed part numbers and
|
||||
rapids. The live `Plate`, part order, programs and quantities remain unchanged
|
||||
|
||||
Reference in new issue
Block a user