diff --git a/OpenNest.Core/Diagnostics/OverlapReportState.cs b/OpenNest.Core/Diagnostics/OverlapReportState.cs
index 9a32e93e..ee2f32ba 100644
--- a/OpenNest.Core/Diagnostics/OverlapReportState.cs
+++ b/OpenNest.Core/Diagnostics/OverlapReportState.cs
@@ -31,6 +31,13 @@ public sealed class OverlapReportState
public bool IsAllClear => Status == OverlapCheckStatus.Current && Report.Pairs.Count == 0
&& Report.ClampFindings.Count == 0 && Report.CutOffClampFindings.Count == 0
&& Report.SpacingPairs.Count == 0;
+ ///
+ /// The last published result's message, kept through layout edits and rechecks so the canvas
+ /// stays quiet until the next result; never a claim about the current layout. Null when none.
+ ///
+ public string SettledMessage { get; private set; }
+ /// Whether that last published result was all clear.
+ public bool SettledClear { get; private set; }
public string Message => Status switch
{
@@ -91,6 +98,8 @@ public sealed class OverlapReportState
observedDisplayStamp = stamp;
uncheckedPartCount = CountUncheckedParts(report.Issues.Concat(report.SpacingIssues));
Status = report.IsComplete ? OverlapCheckStatus.Current : OverlapCheckStatus.Incomplete;
+ SettledMessage = Message;
+ SettledClear = IsAllClear;
return true;
}
@@ -175,7 +184,11 @@ public sealed class OverlapReportState
private void Clear(OverlapCheckStatus status, bool preserveDisplay = false)
{
if (!preserveDisplay)
+ {
ClearDisplayPairs();
+ SettledMessage = null;
+ SettledClear = false;
+ }
Generation++;
Report = null;
uncheckedPartCount = 0;
diff --git a/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs b/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs
index 592241ca..4b494d4b 100644
--- a/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs
+++ b/OpenNest.Tests/Diagnostics/OverlapSpacingLabelTests.cs
@@ -200,6 +200,29 @@ public class OverlapSpacingLabelTests
Assert.False(state.IsAllClear);
}
+ [Fact]
+ public void SettledResult_SurvivesEditsAndRechecksButNotFailureOrCancel()
+ {
+ var plate = PlateWith(Spacing, Square(0), Square(4.1));
+ var state = new OverlapReportState();
+ Assert.Null(state.SettledMessage);
+ Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
+ const string message = "No material overlaps; part spacing: 1 pairs too close";
+
+ plate.Parts[1].Offset(1, 0);
+ state.EnsureFresh(plate);
+ Assert.Equal((message, false), (state.SettledMessage, state.SettledClear));
+ var request = state.Begin(plate);
+ Assert.Equal(message, state.SettledMessage);
+
+ Assert.True(state.TryPublish(request, plate, Analyze(plate)));
+ Assert.True(state.SettledClear);
+
+ Assert.True(state.TryFail(state.Begin(plate), plate));
+ Assert.Null(state.SettledMessage);
+ Assert.False(state.SettledClear);
+ }
+
[Fact]
public void Presentation_ShowsPositionsNamesAndGap()
{
diff --git a/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs b/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs
index 15d5d813..aa42adbd 100644
--- a/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs
+++ b/OpenNest.WinForms.Tests/PlateOverlapOverlayTests.cs
@@ -358,6 +358,62 @@ public class PlateOverlapOverlayTests
Assert.False(CheckIsDrawn(run));
});
+ [Fact]
+ public void ClearPlateKeepsAGrayCheckWhileRechecking() => RunSta(() =>
+ {
+ using var run = new OverlayRun();
+ run.View.Plate.Parts.RemoveAt(1);
+ run.EnableAutoCheck();
+ var overlay = run.View.OverlapOverlay;
+ var task = run.AutoTick();
+ run.Next().Complete();
+ run.Pump(task);
+ Assert.True(overlay.ShowsAllClear);
+
+ run.View.Plate.Parts[0].Offset(1, 0);
+ run.Paint();
+ Assert.True(overlay.IsAutoCheckScheduled);
+ Assert.True(overlay.ShowsPendingCheck);
+ Assert.False(overlay.ShowsAllClear);
+ Assert.False(CheckIsDrawn(run)); // Gray, not green: this layout is unconfirmed.
+
+ task = run.AutoTick();
+ var work = run.Next();
+ Assert.True(overlay.ShowsPendingCheck);
+ work.Complete();
+ run.Pump(task);
+ Assert.False(overlay.ShowsPendingCheck);
+ Assert.True(overlay.ShowsAllClear);
+ });
+
+ [Fact]
+ public void FindingsStayInTheLabelWhileRechecking() => RunSta(() =>
+ {
+ using var run = new OverlayRun();
+ run.EnableAutoCheck();
+ var task = run.AutoTick();
+ run.Next().Complete();
+ run.Pump(task);
+
+ run.View.Plate.Parts[1].Offset(0.5, 0);
+ run.Paint();
+ Assert.False(run.View.OverlapOverlay.ShowsPendingCheck);
+ Assert.False(run.View.OverlapOverlay.ShowsAllClear);
+ });
+
+ [Fact]
+ public void StaleWithoutAutoCheckStillAsksForACheck() => RunSta(() =>
+ {
+ using var run = new OverlayRun();
+ run.View.Plate.Parts.RemoveAt(1);
+ run.Finish();
+ run.View.Plate.Parts[0].Offset(1, 0);
+ run.Paint();
+ Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
+ Assert.False(run.View.OverlapOverlay.ShowsPendingCheck);
+ Assert.False(run.View.OverlapOverlay.ShowsAllClear);
+ });
+
private static bool CheckIsDrawn(OverlayRun run)
{
using var image = new Bitmap(64, 64);
diff --git a/OpenNest/Controls/OverlapOverlayController.cs b/OpenNest/Controls/OverlapOverlayController.cs
index c2fbaa52..c532f538 100644
--- a/OpenNest/Controls/OverlapOverlayController.cs
+++ b/OpenNest/Controls/OverlapOverlayController.cs
@@ -81,7 +81,12 @@ internal sealed class OverlapOverlayController : IDisposable
: state.Message;
/// The canvas shows a green check instead of the label: a pending recheck is not clear.
- internal bool ShowsAllClear => state.IsAllClear && !autoCheck.IsWaiting;
+ internal bool ShowsAllClear => state.IsAllClear && !IsSettling;
+
+ /// A gray check while a recheck settles: unconfirmed, and steady in the corner.
+ internal bool ShowsPendingCheck => IsSettling && (state.SettledMessage == null || state.SettledClear);
+
+ private bool IsSettling => autoCheck.IsWaiting || state.IsRunning;
///
/// Recheck automatically after layout edits settle. Units are read when each request
@@ -683,13 +688,13 @@ internal sealed class OverlapOverlayController : IDisposable
}
// Same corner as the label; Message keeps the words for the status bar.
- private void DrawAllClear(Graphics graphics)
+ private void DrawCheck(Graphics graphics, Color color)
{
var scale = view.DeviceDpi / 96f;
var box = 24 * scale;
using var background = new SolidBrush(Color.FromArgb(235, Color.White));
graphics.FillRectangle(background, 6, 6, box, box);
- using var pen = new Pen(Color.FromArgb(0, 140, 60), 3 * scale)
+ using var pen = new Pen(color, 3 * scale)
{
StartCap = LineCap.Round,
EndCap = LineCap.Round,
@@ -712,16 +717,19 @@ internal sealed class OverlapOverlayController : IDisposable
try
{
graphics.ResetTransform();
- if (ShowsAllClear)
+ if (ShowsAllClear || ShowsPendingCheck)
{
- DrawAllClear(graphics);
+ DrawCheck(graphics, ShowsAllClear ? Color.FromArgb(0, 140, 60) : Color.FromArgb(150, 150, 150));
return;
}
- var text = Message + (state.DisplayMode == OverlapDisplayMode.Off ? " (display off)" : "");
+ // While settling, the last findings stay put, dimmed, instead of status text flickering.
+ var settling = IsSettling;
+ var text = (settling ? state.SettledMessage : Message)
+ + (state.DisplayMode == OverlapDisplayMode.Off ? " (display off)" : "");
var size = graphics.MeasureString(text, view.Font);
using var background = new SolidBrush(Color.FromArgb(235, Color.White));
graphics.FillRectangle(background, 6, 6, size.Width + 8, size.Height + 6);
- graphics.DrawString(text, view.Font, Brushes.DarkRed, 10, 9);
+ graphics.DrawString(text, view.Font, settling ? Brushes.Gray : Brushes.DarkRed, 10, 9);
}
finally
{
diff --git a/docs/geometry/visual-overlap-check.md b/docs/geometry/visual-overlap-check.md
index a3b9da7b..47d59614 100644
--- a/docs/geometry/visual-overlap-check.md
+++ b/docs/geometry/visual-overlap-check.md
@@ -30,7 +30,10 @@ zero. Selection and dragging remain ordinary plate operations, not overlay
interactions.
A current, fully checked report with no finding of any kind draws a green check in
-the corner instead of words; the status bar still uses the wording below. Every
+the corner instead of words; the status bar still uses the wording below. While a
+recheck is pending or running the corner stays quiet: a gray check after a clear
+result or when there is no result yet, or the last findings dimmed in gray, neither
+of which says anything about the edited layout. Every
other state keeps a persistent label, which distinguishes unchecked, checking,
current with findings, incomplete, stale,
canceled, and failed checks. Only a completed, current, fully checked report can