mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-03 06:51:34 -05:00
feat(ui): show the layout check in the status bar
An empty canvas corner gave no sign of whether a check was still running, which matters on large nests. A Layout section beside utilization now follows the automatic check: checking, OK, or findings in red, with the full wording in its tooltip. It has its own label so rechecks never replace the last command's status text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac7ee41801
commit
0a0da50006
8 files changed
+179
-5
No files matched your search
@@ -55,6 +55,34 @@ public sealed class OverlapReportState
|
||||
_ => "Overlaps: not checked"
|
||||
};
|
||||
|
||||
/// <summary>A few steady words for the status bar; <see cref="Message"/> has the detail.</summary>
|
||||
public string Summary => Status switch
|
||||
{
|
||||
OverlapCheckStatus.Checking => "Layout: checking…",
|
||||
OverlapCheckStatus.Current => IsAllClear ? "Layout: OK" : "Layout: " + Findings(),
|
||||
OverlapCheckStatus.Incomplete => "Layout: check incomplete",
|
||||
OverlapCheckStatus.Failed => "Layout: check failed",
|
||||
OverlapCheckStatus.Canceled => "Layout: check canceled",
|
||||
OverlapCheckStatus.Stale => "Layout: out of date",
|
||||
_ => "Layout: not checked"
|
||||
};
|
||||
|
||||
/// <summary>True when <see cref="Summary"/> reports findings or a check that did not finish.</summary>
|
||||
public bool NeedsAttention => Status is OverlapCheckStatus.Incomplete or OverlapCheckStatus.Failed
|
||||
|| Status == OverlapCheckStatus.Current && !IsAllClear;
|
||||
|
||||
private string Findings()
|
||||
{
|
||||
var overlaps = Report.Pairs.Count + Report.ClampFindings.Count + Report.CutOffClampFindings.Count;
|
||||
var spacing = Report.SpacingPairs.Count;
|
||||
var words = new List<string>();
|
||||
if (overlaps != 0)
|
||||
words.Add(overlaps == 1 ? "1 overlap" : $"{overlaps} overlaps");
|
||||
if (spacing != 0)
|
||||
words.Add($"{spacing} too close");
|
||||
return string.Join(", ", words);
|
||||
}
|
||||
|
||||
// Only a fully checked report that required a gap may clear spacing as well as overlaps.
|
||||
private string CurrentMessage()
|
||||
{
|
||||
|
||||
@@ -223,6 +223,45 @@ public class OverlapSpacingLabelTests
|
||||
Assert.False(state.SettledClear);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(4.5, "Layout: OK", false)]
|
||||
[InlineData(4.1, "Layout: 1 too close", true)]
|
||||
[InlineData(3.0, "Layout: 1 overlap", true)]
|
||||
public void Summary_StaysShort(double x, string expected, bool attention)
|
||||
{
|
||||
var plate = PlateWith(Spacing, Square(0), Square(x));
|
||||
var state = new OverlapReportState();
|
||||
Assert.Equal("Layout: not checked", state.Summary);
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||
|
||||
Assert.Equal(expected, state.Summary);
|
||||
Assert.Equal(attention, state.NeedsAttention);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Summary_CountsOverlapsAndSpacingTogether()
|
||||
{
|
||||
var plate = PlateWith(Spacing, Square(0), Square(3), Square(3.5), Square(7.6));
|
||||
var state = new OverlapReportState();
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||
|
||||
Assert.Equal("Layout: 3 overlaps, 1 too close", state.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Summary_FlagsAnUnfinishedCheck()
|
||||
{
|
||||
var plate = PlateWith(Spacing, Square(0), new Part(new Drawing("invalid", new Program())));
|
||||
var state = new OverlapReportState();
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||
Assert.Equal("Layout: check incomplete", state.Summary);
|
||||
Assert.True(state.NeedsAttention);
|
||||
|
||||
Assert.True(state.TryFail(state.Begin(plate), plate));
|
||||
Assert.Equal("Layout: check failed", state.Summary);
|
||||
Assert.True(state.NeedsAttention);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Presentation_ShowsPositionsNamesAndGap()
|
||||
{
|
||||
|
||||
@@ -434,6 +434,51 @@ public class PlateOverlapOverlayTests
|
||||
run.Pump(task);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void SummaryStaysSteadyThroughARecheckAndNotifiesFromPaint() => RunSta(() =>
|
||||
{
|
||||
using var run = new OverlayRun();
|
||||
run.View.Plate.Parts.RemoveAt(1);
|
||||
run.EnableAutoCheck();
|
||||
var overlay = run.View.OverlapOverlay;
|
||||
var task = run.AutoTick();
|
||||
Assert.Equal("Layout: checking…", overlay.Summary);
|
||||
run.Next().Complete();
|
||||
run.Pump(task);
|
||||
Assert.Equal("Layout: OK", overlay.Summary);
|
||||
|
||||
var changes = new List<string>();
|
||||
run.View.OverlapStateChanged += (_, _) => changes.Add(overlay.Summary);
|
||||
run.View.Plate.Parts[0].Offset(1, 0); // A drag: only paint can notice it.
|
||||
run.Paint();
|
||||
Assert.Equal("Layout: checking…", Assert.Single(changes.Distinct()));
|
||||
Assert.False(overlay.NeedsAttention);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void StatusBarLabelFollowsTheActiveViewWithoutTouchingTheStatusText() => RunSta(() =>
|
||||
{
|
||||
using var form = new EditNestForm(new Nest("layout"));
|
||||
var status = new ToolStripStatusLabel();
|
||||
var layout = MainStatusBar.CreateLayoutLabel();
|
||||
var bar = new MainStatusBar(status, new ToolStripStatusLabel(), new ToolStripStatusLabel(),
|
||||
new ToolStripStatusLabel(), new ToolStripStatusLabel(), new ToolStripStatusLabel(), null, layout);
|
||||
form.PlateView.Status = "Fill: 2 parts in 5 ms";
|
||||
|
||||
bar.Bind(form);
|
||||
Assert.True(layout.Available);
|
||||
Assert.Equal(form.PlateView.OverlapSummary, layout.Text);
|
||||
Assert.Equal(form.PlateView.OverlapMessage, layout.ToolTipText);
|
||||
form.PlateView.Plate.Parts.Add(Rectangle());
|
||||
form.PlateView.InvalidateOverlapCheck();
|
||||
Assert.Equal("Layout: checking…", layout.Text);
|
||||
Assert.Equal("Fill: 2 parts in 5 ms", status.Text);
|
||||
|
||||
bar.Bind(null);
|
||||
Assert.False(layout.Available);
|
||||
Assert.Equal("", layout.Text);
|
||||
});
|
||||
|
||||
private static bool CornerIsEmpty(OverlayRun run)
|
||||
{
|
||||
using var image = new Bitmap(64, 64);
|
||||
|
||||
@@ -32,6 +32,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
private Func<Units> autoCheckUnits;
|
||||
private ObservableList<Part> observedParts;
|
||||
private CancellationTokenSource cancellation;
|
||||
private string reportedSummary;
|
||||
private GraphicsPath path;
|
||||
private IReadOnlyList<PlateOverlapPair> pathPairs;
|
||||
private float pathScale;
|
||||
@@ -80,6 +81,11 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
? "Overlaps: check pending…"
|
||||
: state.Message;
|
||||
|
||||
/// <summary>The status bar's words: one steady "checking" for both pending and running.</summary>
|
||||
public string Summary => IsSettling ? "Layout: checking…" : state.Summary;
|
||||
|
||||
public bool NeedsAttention => !IsSettling && state.NeedsAttention;
|
||||
|
||||
/// <summary>
|
||||
/// The corner stays empty unless something is wrong: on a clear result, and while an
|
||||
/// automatic recheck settles after one or before the first. Views without automatic rechecks
|
||||
@@ -284,6 +290,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
if (toStatusBar || autoCheckUnits == null)
|
||||
view.Status = Message;
|
||||
view.Invalidate();
|
||||
reportedSummary = Summary;
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -375,6 +382,12 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
return;
|
||||
EnsureFresh();
|
||||
UpdateAutoCheck(repaint: false); // Drags/nudges are only visible to the stamp; the label is drawn below.
|
||||
// A wait can start or end here without a state change, and the status bar must follow.
|
||||
if (reportedSummary != Summary)
|
||||
{
|
||||
reportedSummary = Summary;
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
ValidateHoverTransform();
|
||||
|
||||
if (state.DisplayPairs.Count > 0)
|
||||
|
||||
@@ -355,6 +355,9 @@ namespace OpenNest.Controls
|
||||
}
|
||||
public OverlapCheckStatus OverlapStatus => overlapOverlay.Status;
|
||||
public PlateOverlapReport OverlapReport => overlapOverlay.Report;
|
||||
public string OverlapSummary => overlapOverlay.Summary;
|
||||
public string OverlapMessage => overlapOverlay.Message;
|
||||
public bool OverlapNeedsAttention => overlapOverlay.NeedsAttention;
|
||||
public bool IsOverlapCheckRunning => overlapOverlay.IsRunning;
|
||||
public OverlapDisplayMode OverlapDisplay
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace OpenNest.Forms
|
||||
|
||||
private readonly MainStatusBar statusBar;
|
||||
private readonly ToolStripStatusLabel ledgerStatusLabel = MainStatusBar.CreateLedgerLabel();
|
||||
private readonly ToolStripStatusLabel layoutStatusLabel = MainStatusBar.CreateLayoutLabel();
|
||||
private bool nestingInProgress;
|
||||
private CancellationTokenSource nestingCts;
|
||||
private readonly NestStorageController storage;
|
||||
@@ -114,9 +115,10 @@ namespace OpenNest.Forms
|
||||
Icon = Resources.app_icon;
|
||||
statusStrip1.Items.Insert(statusStrip1.Items.IndexOf(materialStatusLabel) + 1, ledgerStatusLabel);
|
||||
ledgerStatusLabel.Click += LedgerStatusLabel_Click;
|
||||
statusStrip1.Items.Insert(statusStrip1.Items.IndexOf(plateUtilStatusLabel) + 1, layoutStatusLabel);
|
||||
statusStripToolTips = new StatusStripToolTips(statusStrip1); // The hold list lives in the ledger label's tooltip.
|
||||
statusBar = new MainStatusBar(statusLabel1, locationStatusLabel, selectionStatusLabel,
|
||||
machineStatusLabel, materialStatusLabel, plateUtilStatusLabel, ledgerStatusLabel);
|
||||
machineStatusLabel, materialStatusLabel, plateUtilStatusLabel, ledgerStatusLabel, layoutStatusLabel);
|
||||
btnCutPlanner = CreateActionButton("btnCutPlanner", "Plan Cutting",
|
||||
Resources.cut_planner, ToolbarPlanCutting_Click);
|
||||
btnSimulateCutting = CreateActionButton("btnSimulateCutting", "Simulate Cutting",
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace OpenNest.Forms
|
||||
/// <summary>
|
||||
/// The main window's status labels for the active nest document: status text, cursor
|
||||
/// location, selection bounds, nest machine and material, MaterialLedger holds of a server nest,
|
||||
/// and plate utilization. Plate number, size and
|
||||
/// plate utilization and the automatic layout check. Plate number, size and
|
||||
/// quantity live in the plate view header instead. The location
|
||||
/// follows the mouse (the default, throttled), or only left clicks while click mode is on;
|
||||
/// the mode persists as a user setting.
|
||||
@@ -34,6 +34,7 @@ namespace OpenNest.Forms
|
||||
private readonly ToolStripStatusLabel material;
|
||||
private readonly ToolStripStatusLabel plateUtil;
|
||||
private readonly ToolStripStatusLabel ledger;
|
||||
private readonly ToolStripStatusLabel layout;
|
||||
private readonly Timer locationSettleTimer;
|
||||
|
||||
private EditNestForm form;
|
||||
@@ -42,9 +43,11 @@ namespace OpenNest.Forms
|
||||
|
||||
public MainStatusBar(ToolStripStatusLabel status, ToolStripStatusLabel location,
|
||||
ToolStripStatusLabel selection, ToolStripStatusLabel machine, ToolStripStatusLabel material,
|
||||
ToolStripStatusLabel plateUtil, ToolStripStatusLabel ledger = null)
|
||||
ToolStripStatusLabel plateUtil, ToolStripStatusLabel ledger = null,
|
||||
ToolStripStatusLabel layout = null)
|
||||
{
|
||||
this.ledger = ledger;
|
||||
this.layout = layout;
|
||||
this.status = status;
|
||||
this.location = location;
|
||||
this.selection = selection;
|
||||
@@ -67,6 +70,7 @@ namespace OpenNest.Forms
|
||||
UpdatePlate();
|
||||
UpdateLocation();
|
||||
UpdateLedger();
|
||||
UpdateLayout();
|
||||
|
||||
if (form == null)
|
||||
{
|
||||
@@ -79,6 +83,7 @@ namespace OpenNest.Forms
|
||||
UpdateSelection();
|
||||
var view = form.PlateView;
|
||||
view.StatusChanged += PlateView_StatusChanged;
|
||||
view.OverlapStateChanged += PlateView_OverlapStateChanged;
|
||||
view.SelectionChanged += PlateView_SelectionChanged;
|
||||
view.PartAdded += PlateView_PartAdded;
|
||||
view.PartRemoved += PlateView_PartRemoved;
|
||||
@@ -96,12 +101,14 @@ namespace OpenNest.Forms
|
||||
view.MouseMove -= PlateView_MouseMove;
|
||||
view.MouseClick -= PlateView_MouseClick;
|
||||
view.StatusChanged -= PlateView_StatusChanged;
|
||||
view.OverlapStateChanged -= PlateView_OverlapStateChanged;
|
||||
view.SelectionChanged -= PlateView_SelectionChanged;
|
||||
view.PartAdded -= PlateView_PartAdded;
|
||||
view.PartRemoved -= PlateView_PartRemoved;
|
||||
form.HoldStatus.Changed -= HoldStatus_Changed;
|
||||
form = null;
|
||||
UpdateLedger();
|
||||
UpdateLayout();
|
||||
}
|
||||
|
||||
public void ToggleLocationMode()
|
||||
@@ -118,6 +125,7 @@ namespace OpenNest.Forms
|
||||
UpdatePlate();
|
||||
UpdateSelection();
|
||||
UpdateLedger();
|
||||
UpdateLayout();
|
||||
}
|
||||
|
||||
/// <summary>The hidden-by-default hold label MainForm inserts after the material label.</summary>
|
||||
@@ -155,6 +163,38 @@ namespace OpenNest.Forms
|
||||
ledger.ToolTipText = tip + Environment.NewLine + (holds.IsRefreshing ? "Refreshing…" : "Click to refresh.");
|
||||
}
|
||||
|
||||
/// <summary>The layout check label MainForm inserts after utilization; hidden without a nest.</summary>
|
||||
internal static ToolStripStatusLabel CreateLayoutLabel() => new()
|
||||
{
|
||||
Name = "layoutStatusLabel",
|
||||
BorderSides = ToolStripStatusLabelBorderSides.Left,
|
||||
Padding = new Padding(5, 0, 5, 0),
|
||||
Visible = false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The automatic layout check has its own label: unlike the status text, it may change
|
||||
/// on every edit without replacing the last command's message.
|
||||
/// </summary>
|
||||
public void UpdateLayout()
|
||||
{
|
||||
if (layout == null)
|
||||
return;
|
||||
var view = form?.PlateView;
|
||||
layout.Visible = view != null;
|
||||
if (view == null)
|
||||
{
|
||||
layout.Text = string.Empty;
|
||||
layout.ToolTipText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
if (layout.Text != view.OverlapSummary)
|
||||
layout.Text = view.OverlapSummary;
|
||||
layout.ForeColor = view.OverlapNeedsAttention ? Color.DarkRed : SystemColors.ControlText;
|
||||
layout.ToolTipText = view.OverlapMessage;
|
||||
}
|
||||
|
||||
public void UpdateLocation()
|
||||
{
|
||||
if (form == null)
|
||||
@@ -313,6 +353,8 @@ namespace OpenNest.Forms
|
||||
|
||||
private void PlateView_StatusChanged(object sender, EventArgs e) => status.Text = form.PlateView.Status;
|
||||
|
||||
private void PlateView_OverlapStateChanged(object sender, EventArgs e) => UpdateLayout();
|
||||
|
||||
private void PlateView_SelectionChanged(object sender, EventArgs e) => UpdateSelection();
|
||||
|
||||
private void HoldStatus_Changed(object sender, EventArgs e) => UpdateLedger();
|
||||
|
||||
@@ -61,8 +61,10 @@ In a nest window the active plate is checked automatically. Any layout edit
|
||||
Only highlights involving changed ordered input slots are removed; once the
|
||||
layout has been unchanged for 0.5 s and no
|
||||
mouse button, modal dialog, or fill progress window is active, the check reruns.
|
||||
Automatic results appear only in the canvas label, so the status bar keeps the
|
||||
last command's message, and an automatic check keeps Display > Off rather than
|
||||
Automatic results do not replace the status bar's message, so it keeps the last
|
||||
command's text; a separate **Layout** section beside utilization follows the check
|
||||
instead, as `Layout: OK`, `Layout: checking…` or `Layout: 1 overlap, 3 too close`,
|
||||
in red for findings or an unfinished check, with the full wording in its tooltip, and an automatic check keeps Display > Off rather than
|
||||
switching to Areas. Rechecks are incremental: drawing material is prepared once
|
||||
and reused, and pairs whose two parts have not moved reuse the previous result,
|
||||
so only the moved parts' neighbors are clipped again. Canceling a check is
|
||||
|
||||
Reference in new issue
Block a user