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:
ajandClaude Opus 5.5 committed 2026-11-01 23:42:25 -05:00
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)
+3
View File
@@ -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
{
+3 -1
View File
@@ -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",
+44 -2
View File
@@ -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();
+4 -2
View File
@@ -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