diff --git a/OpenNest.Core/Diagnostics/OverlapReportState.cs b/OpenNest.Core/Diagnostics/OverlapReportState.cs
index ee2f32ba..daaf34cf 100644
--- a/OpenNest.Core/Diagnostics/OverlapReportState.cs
+++ b/OpenNest.Core/Diagnostics/OverlapReportState.cs
@@ -55,6 +55,34 @@ public sealed class OverlapReportState
_ => "Overlaps: not checked"
};
+ /// A few steady words for the status bar; has the detail.
+ 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"
+ };
+
+ /// True when reports findings or a check that did not finish.
+ 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();
+ 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()
{
diff --git a/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs b/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs
index 4b494d4b..80aac806 100644
--- a/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs
+++ b/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs
@@ -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()
{
diff --git a/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs b/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs
index 26049c85..5fcd0a64 100644
--- a/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs
+++ b/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs
@@ -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();
+ 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);
diff --git a/OpenNest/Controls/OverlapOverlayController.cs b/OpenNest/Controls/OverlapOverlayController.cs
index 66d2c1de..f8e325f3 100644
--- a/OpenNest/Controls/OverlapOverlayController.cs
+++ b/OpenNest/Controls/OverlapOverlayController.cs
@@ -32,6 +32,7 @@ internal sealed class OverlapOverlayController : IDisposable
private Func autoCheckUnits;
private ObservableList observedParts;
private CancellationTokenSource cancellation;
+ private string reportedSummary;
private GraphicsPath path;
private IReadOnlyList pathPairs;
private float pathScale;
@@ -80,6 +81,11 @@ internal sealed class OverlapOverlayController : IDisposable
? "Overlaps: check pending…"
: state.Message;
+ /// The status bar's words: one steady "checking" for both pending and running.
+ public string Summary => IsSettling ? "Layout: checking…" : state.Summary;
+
+ public bool NeedsAttention => !IsSettling && state.NeedsAttention;
+
///
/// 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)
diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs
index e5864533..0073079b 100644
--- a/OpenNest/Controls/PlateView.cs
+++ b/OpenNest/Controls/PlateView.cs
@@ -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
{
diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs
index e145aec8..b1cd8ec9 100644
--- a/OpenNest/Forms/MainForm.cs
+++ b/OpenNest/Forms/MainForm.cs
@@ -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",
diff --git a/OpenNest/Forms/MainStatusBar.cs b/OpenNest/Forms/MainStatusBar.cs
index 530ec760..93aed25c 100644
--- a/OpenNest/Forms/MainStatusBar.cs
+++ b/OpenNest/Forms/MainStatusBar.cs
@@ -14,7 +14,7 @@ namespace OpenNest.Forms
///
/// 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();
}
/// The hidden-by-default hold label MainForm inserts after the material label.
@@ -155,6 +163,38 @@ namespace OpenNest.Forms
ledger.ToolTipText = tip + Environment.NewLine + (holds.IsRefreshing ? "Refreshing…" : "Click to refresh.");
}
+ /// The layout check label MainForm inserts after utilization; hidden without a nest.
+ internal static ToolStripStatusLabel CreateLayoutLabel() => new()
+ {
+ Name = "layoutStatusLabel",
+ BorderSides = ToolStripStatusLabelBorderSides.Left,
+ Padding = new Padding(5, 0, 5, 0),
+ Visible = false,
+ };
+
+ ///
+ /// 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.
+ ///
+ 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();
diff --git a/docs/geometry/visual-overlap-check.md b/docs/geometry/visual-overlap-check.md
index de57678d..d824b248 100644
--- a/docs/geometry/visual-overlap-check.md
+++ b/docs/geometry/visual-overlap-check.md
@@ -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