feat(overlap): auto-recheck the active plate after layout edits settle

Nest windows now rerun the overlap check once the layout has been
unchanged for 0.5 s, instead of leaving 'Overlaps: not checked'. Edits
show 'Overlaps: check pending...'; drags are caught by the paint-time
pose stamp, collection edits by their events. The check waits while a
mouse button, modal dialog or fill is active, supersedes a running check
when the layout moves again, and does not retry a canceled or failed
layout until it changes. Rechecks use the incremental analyzer, so only
the moved parts' neighbors are recomputed.

Automatic results update only the canvas label (the status bar keeps the
last command's message) and keep Display > Off. Check Active Plate still
runs immediately. InvalidateOverlapCheck now also drops cached material.
This commit is contained in:
aj
2026-09-29 09:31:51 -04:00
parent 99c31748ba
commit 29953ea601
10 changed files with 597 additions and 30 deletions
@@ -0,0 +1,114 @@
using OpenNest.CNC;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
namespace OpenNest.Tests.Diagnostics;
public class OverlapAutoCheckSchedulerTests
{
[Fact]
public void ChecksOnlyAfterTheLayoutStaysQuietForAWholePeriod()
{
var plate = PlateWithParts();
var state = new OverlapReportState();
var scheduler = new OverlapAutoCheckScheduler();
Assert.True(scheduler.Observe(plate, state)); // NotChecked: start the timer
Assert.False(scheduler.Observe(plate, state)); // repaint without change: leave it running
plate.Parts[0].Offset(1, 0); // drag step
Assert.True(scheduler.Observe(plate, state)); // restart the quiet period
plate.Parts[0].Offset(1, 0); // moved again, not yet observed
Assert.Equal(OverlapAutoCheckStep.Wait, scheduler.Elapsed(plate, state, interactionActive: false));
Assert.Equal(OverlapAutoCheckStep.Check, scheduler.Elapsed(plate, state, interactionActive: false));
Assert.False(scheduler.IsWaiting);
}
[Fact]
public void WaitsWhileAnInteractionIsActive()
{
var plate = PlateWithParts();
var state = new OverlapReportState();
var scheduler = new OverlapAutoCheckScheduler();
scheduler.Observe(plate, state);
Assert.Equal(OverlapAutoCheckStep.Wait, scheduler.Elapsed(plate, state, interactionActive: true));
Assert.Equal(OverlapAutoCheckStep.Check, scheduler.Elapsed(plate, state, interactionActive: false));
}
[Fact]
public void CurrentOrRunningChecksNeedNoAutomaticRequest()
{
var plate = PlateWithParts();
var state = new OverlapReportState();
var scheduler = new OverlapAutoCheckScheduler();
scheduler.Observe(plate, state);
var request = state.Begin(plate);
scheduler.Started(plate);
Assert.False(scheduler.Observe(plate, state));
Assert.Equal(OverlapAutoCheckStep.None, scheduler.Elapsed(plate, state, false));
Assert.True(state.TryPublish(request, plate, PlateOverlapAnalyzer.Analyze(plate.Parts.ToArray())));
Assert.False(scheduler.Observe(plate, state));
plate.Parts[1].Offset(0, 3);
state.EnsureFresh(plate); // Stale after the edit
Assert.True(scheduler.Observe(plate, state));
Assert.Equal(OverlapAutoCheckStep.Check, scheduler.Elapsed(plate, state, false));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanceledOrFailedLayoutIsNotRetriedUntilItChanges(bool cancel)
{
var plate = PlateWithParts();
var state = new OverlapReportState();
var scheduler = new OverlapAutoCheckScheduler();
var request = state.Begin(plate);
scheduler.Started(plate);
if (cancel)
state.Cancel();
else
Assert.True(state.TryFail(request, plate));
Assert.False(scheduler.Observe(plate, state)); // respect the cancel / avoid a failure loop
Assert.Equal(OverlapAutoCheckStep.None, scheduler.Elapsed(plate, state, false));
plate.Parts[0].Offset(0.5, 0);
Assert.True(scheduler.Observe(plate, state));
Assert.Equal(OverlapAutoCheckStep.Check, scheduler.Elapsed(plate, state, false));
}
[Fact]
public void ResetForgetsPendingWorkAndPreviousRequests()
{
var plate = PlateWithParts();
var state = new OverlapReportState();
var scheduler = new OverlapAutoCheckScheduler();
state.Begin(plate);
scheduler.Started(plate);
state.Cancel();
scheduler.Reset();
Assert.True(scheduler.Observe(plate, state)); // a new document/plate may check again
scheduler.Reset();
Assert.False(scheduler.IsWaiting);
Assert.Equal(OverlapAutoCheckStep.None, scheduler.Elapsed(plate, state, false));
Assert.False(scheduler.Observe(null, state));
}
private static Plate PlateWithParts()
{
var plate = new Plate();
plate.Parts.Add(Rectangle());
plate.Parts.Add(Rectangle());
return plate;
}
private static Part Rectangle()
{
var program = new Program(Mode.Absolute);
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(4, 0));
program.Codes.Add(new LinearMove(4, 4));
program.Codes.Add(new LinearMove(0, 4));
program.Codes.Add(new LinearMove(0, 0));
return new Part(new Drawing("same name", program));
}
}
@@ -150,6 +150,16 @@ public class OverlapReportStateTests
Assert.Equal(expected, state.DisplayMode);
}
[Fact]
public void AutomaticCheckKeepsDisplayOff()
{
var plate = PlateWithParts();
var state = new OverlapReportState { DisplayMode = OverlapDisplayMode.Off };
Assert.True(state.TryPublish(state.Begin(plate, automatic: true), plate, Analyze(plate)));
Assert.Equal(OverlapDisplayMode.Off, state.DisplayMode);
Assert.Single(state.Report.Pairs);
}
[Fact]
public void NewPlateResetsStateButRetainsDocumentDisplayPreference()
{