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