mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-02 01:58:48 -04:00
fix(overlap): retain unchanged pair highlights during rechecks
This commit is contained in:
@@ -32,6 +32,20 @@ public sealed class OverlapGeometryStamp
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Conservative display reuse: only unchanged ordered slots on the same plate survive.
|
||||
/// Index-changing edits may hide extra pairs, but cannot attach old geometry to new parts.
|
||||
/// </summary>
|
||||
internal bool[] UnchangedSlots(Plate current)
|
||||
{
|
||||
var matches = new bool[entries.Length];
|
||||
if (!ReferenceEquals(plate, current))
|
||||
return matches;
|
||||
for (var i = 0; i < entries.Length && i < current.Parts.Count; i++)
|
||||
matches[i] = entries[i].Matches(current.Parts[i]);
|
||||
return matches;
|
||||
}
|
||||
|
||||
private readonly struct Entry
|
||||
{
|
||||
private readonly Part part;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Diagnostics;
|
||||
|
||||
@@ -9,12 +11,16 @@ public enum OverlapCheckStatus { NotChecked, Checking, Current, Incomplete, Fail
|
||||
public sealed class OverlapReportState
|
||||
{
|
||||
private OverlapGeometryStamp stamp;
|
||||
private OverlapGeometryStamp displayStamp;
|
||||
private OverlapGeometryStamp observedDisplayStamp;
|
||||
private int uncheckedPartCount;
|
||||
|
||||
public long Generation { get; private set; }
|
||||
public OverlapCheckStatus Status { get; private set; } = OverlapCheckStatus.NotChecked;
|
||||
public OverlapDisplayMode DisplayMode { get; set; } = OverlapDisplayMode.Areas;
|
||||
public PlateOverlapReport Report { get; private set; }
|
||||
/// <summary>Known overlap pairs safe to draw, even while the full layout needs a recheck.</summary>
|
||||
public IReadOnlyList<PlateOverlapPair> DisplayPairs { get; private set; } = Array.Empty<PlateOverlapPair>();
|
||||
public bool IsRunning => Status == OverlapCheckStatus.Checking;
|
||||
|
||||
public string Message => Status switch
|
||||
@@ -36,7 +42,8 @@ public sealed class OverlapReportState
|
||||
/// </summary>
|
||||
public long Begin(Plate plate, bool automatic = false)
|
||||
{
|
||||
Clear(OverlapCheckStatus.Checking);
|
||||
RefreshDisplayPairs(plate);
|
||||
Clear(OverlapCheckStatus.Checking, preserveDisplay: true);
|
||||
stamp = OverlapGeometryStamp.Capture(plate);
|
||||
if (!automatic && DisplayMode == OverlapDisplayMode.Off)
|
||||
DisplayMode = OverlapDisplayMode.Areas;
|
||||
@@ -48,6 +55,9 @@ public sealed class OverlapReportState
|
||||
if (!CanComplete(generation, plate))
|
||||
return false;
|
||||
Report = report;
|
||||
DisplayPairs = report.Pairs;
|
||||
displayStamp = stamp;
|
||||
observedDisplayStamp = stamp;
|
||||
uncheckedPartCount = CountUncheckedParts(report.Issues);
|
||||
Status = report.IsComplete ? OverlapCheckStatus.Current : OverlapCheckStatus.Incomplete;
|
||||
return true;
|
||||
@@ -66,20 +76,52 @@ public sealed class OverlapReportState
|
||||
|
||||
public bool EnsureFresh(Plate plate)
|
||||
{
|
||||
RefreshDisplayPairs(plate);
|
||||
if (stamp == null)
|
||||
return false;
|
||||
if (stamp.Matches(plate))
|
||||
return true;
|
||||
Invalidate();
|
||||
Invalidate(plate);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Layout edit: retain only pairs whose two ordered slots still match exactly.</summary>
|
||||
public void Invalidate(Plate plate)
|
||||
{
|
||||
RefreshDisplayPairs(plate);
|
||||
if (Status is OverlapCheckStatus.Checking or OverlapCheckStatus.Current or OverlapCheckStatus.Incomplete)
|
||||
Clear(OverlapCheckStatus.Stale, preserveDisplay: true);
|
||||
}
|
||||
|
||||
/// <summary>In-place geometry edits and teardown must forget every cached display pair.</summary>
|
||||
public void Invalidate()
|
||||
{
|
||||
ClearDisplayPairs();
|
||||
if (Status is OverlapCheckStatus.Checking or OverlapCheckStatus.Current or OverlapCheckStatus.Incomplete)
|
||||
Clear(OverlapCheckStatus.Stale);
|
||||
}
|
||||
|
||||
private void RefreshDisplayPairs(Plate plate)
|
||||
{
|
||||
if (displayStamp == null || observedDisplayStamp?.Matches(plate) == true)
|
||||
return;
|
||||
var unchanged = displayStamp.UnchangedSlots(plate);
|
||||
var retained = DisplayPairs.Where(pair => unchanged[pair.PartAId] && unchanged[pair.PartBId]).ToList();
|
||||
if (retained.Count != DisplayPairs.Count)
|
||||
DisplayPairs = retained.AsReadOnly();
|
||||
if (DisplayPairs.Count == 0)
|
||||
ClearDisplayPairs();
|
||||
else
|
||||
observedDisplayStamp = OverlapGeometryStamp.Capture(plate);
|
||||
}
|
||||
|
||||
private void ClearDisplayPairs()
|
||||
{
|
||||
DisplayPairs = Array.Empty<PlateOverlapPair>();
|
||||
displayStamp = null;
|
||||
observedDisplayStamp = null;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
if (IsRunning)
|
||||
@@ -88,8 +130,10 @@ public sealed class OverlapReportState
|
||||
|
||||
public void Reset() => Clear(OverlapCheckStatus.NotChecked);
|
||||
|
||||
private void Clear(OverlapCheckStatus status)
|
||||
private void Clear(OverlapCheckStatus status, bool preserveDisplay = false)
|
||||
{
|
||||
if (!preserveDisplay)
|
||||
ClearDisplayPairs();
|
||||
Generation++;
|
||||
Report = null;
|
||||
uncheckedPartCount = 0;
|
||||
|
||||
@@ -215,6 +215,145 @@ public class OverlapReportStateTests
|
||||
Assert.Equal("No material overlaps detected", state.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovingOnePartRetainsOnlyUnaffectedPairsThroughRecheck()
|
||||
{
|
||||
var plate = PlateWithTwoPairs();
|
||||
var state = new OverlapReportState();
|
||||
var report = Analyze(plate);
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, report));
|
||||
var untouched = report.Pairs[0];
|
||||
plate.Parts[3].Offset(1e-10, 0);
|
||||
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
Assert.Null(state.Report);
|
||||
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
||||
Assert.Same(untouched, Assert.Single(state.DisplayPairs));
|
||||
var display = state.DisplayPairs;
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
Assert.Same(display, state.DisplayPairs);
|
||||
|
||||
var request = state.Begin(plate, automatic: true);
|
||||
Assert.Equal(OverlapCheckStatus.Checking, state.Status);
|
||||
Assert.Null(state.Report);
|
||||
Assert.Same(display, state.DisplayPairs);
|
||||
Assert.True(state.TryPublish(request, plate, Analyze(plate)));
|
||||
Assert.Equal(2, state.DisplayPairs.Count);
|
||||
Assert.Same(Assert.IsType<PlateOverlapReport>(state.Report).Pairs, state.DisplayPairs);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("move")]
|
||||
[InlineData("rotate")]
|
||||
[InlineData("placed-program")]
|
||||
[InlineData("clean-program")]
|
||||
[InlineData("cutoff")]
|
||||
[InlineData("replace")]
|
||||
[InlineData("remove")]
|
||||
[InlineData("reorder")]
|
||||
public void FurtherEditsDropAffectedPairsEvenBeforeTheNextCompletedCheck(string edit)
|
||||
{
|
||||
var plate = PlateWithTwoPairs();
|
||||
var state = new OverlapReportState();
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||
plate.Parts[3].Offset(0.5, 0);
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
Assert.Single(state.DisplayPairs);
|
||||
var request = state.Begin(plate, automatic: true);
|
||||
var pending = Analyze(plate);
|
||||
var part = plate.Parts[0];
|
||||
switch (edit)
|
||||
{
|
||||
case "move": part.Offset(0, 1e-10); break;
|
||||
case "rotate": part.Rotate(0.01); break;
|
||||
case "placed-program": part.Update(); break;
|
||||
case "clean-program": part.BaseDrawing.Program = (Program)part.BaseDrawing.Program.Clone(); break;
|
||||
case "cutoff": part.BaseDrawing.IsCutOff = true; break;
|
||||
case "replace": plate.Parts[0] = Rectangle(); break;
|
||||
case "remove": plate.Parts.RemoveAt(0); break;
|
||||
case "reorder": (plate.Parts[0], plate.Parts[1]) = (plate.Parts[1], plate.Parts[0]); break;
|
||||
}
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
Assert.Empty(state.DisplayPairs);
|
||||
Assert.False(state.TryPublish(request, plate, pending));
|
||||
Assert.Null(state.Report);
|
||||
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
||||
Assert.DoesNotContain("No material overlaps", state.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("invalidate")]
|
||||
[InlineData("reset")]
|
||||
[InlineData("plate")]
|
||||
[InlineData("cancel")]
|
||||
[InlineData("fail")]
|
||||
public void HardInvalidationAndRequestFailureClearRetainedPairs(string edit)
|
||||
{
|
||||
var plate = PlateWithTwoPairs();
|
||||
var state = new OverlapReportState();
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||
plate.Parts[3].Offset(1, 0);
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
Assert.Single(state.DisplayPairs);
|
||||
var request = state.Begin(plate);
|
||||
switch (edit)
|
||||
{
|
||||
case "invalidate": state.Invalidate(); break;
|
||||
case "reset": state.Reset(); break;
|
||||
case "plate": Assert.False(state.EnsureFresh(new Plate())); break;
|
||||
case "cancel": state.Cancel(); break;
|
||||
case "fail": Assert.True(state.TryFail(request, plate)); break;
|
||||
}
|
||||
Assert.Empty(state.DisplayPairs);
|
||||
Assert.Null(state.Report);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdditionalUnrelatedMotionDoesNotReplaceTheRetainedDisplayList()
|
||||
{
|
||||
var plate = PlateWithTwoPairs();
|
||||
var state = new OverlapReportState();
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||
plate.Parts[3].Offset(1, 0);
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
var display = state.DisplayPairs;
|
||||
Assert.Single(display);
|
||||
plate.Parts[3].Offset(1, 0);
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
Assert.Same(display, state.DisplayPairs);
|
||||
state.Begin(plate);
|
||||
Assert.Same(display, state.DisplayPairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendingPartInvalidatesTheFullReportWithoutReplacingUnchangedDisplayPairs()
|
||||
{
|
||||
var plate = PlateWithParts();
|
||||
var state = new OverlapReportState();
|
||||
Assert.True(state.TryPublish(state.Begin(plate), plate, Analyze(plate)));
|
||||
var display = state.DisplayPairs;
|
||||
plate.Parts.Add(Rectangle());
|
||||
state.Invalidate(plate); // Same entry point as the controller's collection event.
|
||||
Assert.Null(state.Report);
|
||||
Assert.Equal(OverlapCheckStatus.Stale, state.Status);
|
||||
Assert.Same(display, state.DisplayPairs);
|
||||
Assert.Single(state.DisplayPairs);
|
||||
Assert.False(state.EnsureFresh(plate));
|
||||
Assert.Same(display, state.DisplayPairs);
|
||||
}
|
||||
|
||||
private static Plate PlateWithTwoPairs()
|
||||
{
|
||||
var plate = PlateWithParts();
|
||||
var a = Rectangle();
|
||||
var b = Rectangle();
|
||||
a.Offset(20, 0);
|
||||
b.Offset(20, 0);
|
||||
plate.Parts.Add(a);
|
||||
plate.Parts.Add(b);
|
||||
return plate;
|
||||
}
|
||||
|
||||
private static PlateOverlapReport Analyze(Plate plate) => PlateOverlapAnalyzer.Analyze(plate.Parts.ToArray());
|
||||
|
||||
private static Plate PlateWithParts()
|
||||
|
||||
@@ -97,6 +97,80 @@ public class PlateOverlapOverlayTests
|
||||
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void UnchangedPairStaysVisibleDuringMotionAndPendingRecheck() => RunSta(() =>
|
||||
{
|
||||
using var run = new OverlayRun();
|
||||
run.View.Plate.Parts.Add(Rectangle(20));
|
||||
run.View.Plate.Parts.Add(Rectangle(21));
|
||||
run.Finish();
|
||||
run.View.OverlapDisplay = OverlapDisplayMode.Both;
|
||||
run.View.ZoomToPoint(new Vector(), 20);
|
||||
using var image = new Bitmap(240, 240);
|
||||
using var graphics = Graphics.FromImage(image);
|
||||
var overlay = run.View.OverlapOverlay;
|
||||
var untouchedPoint = run.View.PointWorldToGraph(new Vector(2, 2));
|
||||
var movedPoint = run.View.PointWorldToGraph(new Vector(22, 2));
|
||||
overlay.Draw(graphics);
|
||||
Assert.True(overlay.CachedPath.IsVisible(untouchedPoint));
|
||||
Assert.True(overlay.CachedPath.IsVisible(movedPoint));
|
||||
|
||||
run.View.Plate.Parts[3].Offset(1e-10, 0);
|
||||
overlay.Draw(graphics);
|
||||
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
||||
Assert.Null(run.View.OverlapReport);
|
||||
Assert.True(overlay.CachedPath.IsVisible(untouchedPoint));
|
||||
Assert.False(overlay.CachedPath.IsVisible(movedPoint));
|
||||
var retainedPath = overlay.CachedPath;
|
||||
run.View.Plate.Parts[3].Offset(1, 0);
|
||||
overlay.Draw(graphics);
|
||||
Assert.Same(retainedPath, overlay.CachedPath);
|
||||
Assert.Equal(1, run.Calls); // Paint never analyzes.
|
||||
|
||||
var task = run.Start();
|
||||
var pending = run.Next();
|
||||
overlay.Draw(graphics); // Both areas and centroid markers must render without a full report.
|
||||
Assert.Equal(OverlapCheckStatus.Checking, run.View.OverlapStatus);
|
||||
Assert.Same(retainedPath, overlay.CachedPath);
|
||||
run.View.Plate.Parts[0].Offset(1e-10, 0);
|
||||
overlay.Draw(graphics);
|
||||
Assert.Null(overlay.CachedPath);
|
||||
pending.Complete();
|
||||
run.Pump(task);
|
||||
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
||||
Assert.Null(run.View.OverlapReport);
|
||||
overlay.Draw(graphics);
|
||||
Assert.Null(overlay.CachedPath); // Late completion cannot restore the removed pair.
|
||||
});
|
||||
|
||||
[Theory]
|
||||
[InlineData("editor")]
|
||||
[InlineData("cancel")]
|
||||
[InlineData("failure")]
|
||||
public void RetainedPathClearsOnHardInvalidationOrRequestFailure(string edit) => RunSta(() =>
|
||||
{
|
||||
using var run = new OverlayRun();
|
||||
run.View.Plate.Parts.Add(Rectangle(20));
|
||||
run.Finish();
|
||||
run.View.Plate.Parts[2].Offset(1, 0);
|
||||
run.Paint();
|
||||
var path = run.View.OverlapOverlay.CachedPath;
|
||||
Assert.NotNull(path);
|
||||
var task = run.Start();
|
||||
var pending = run.Next();
|
||||
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
||||
switch (edit)
|
||||
{
|
||||
case "editor": run.View.InvalidateOverlapCheck(); pending.Complete(); break;
|
||||
case "cancel": run.View.CancelOverlapCheck(); pending.Complete(); break;
|
||||
case "failure": pending.Fail(); break;
|
||||
}
|
||||
run.Pump(task);
|
||||
Assert.Null(run.View.OverlapReport);
|
||||
run.Paint();
|
||||
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void DisplayPanZoomAndPreviewDoNotAnalyzeOrChangeCommittedParts() => RunSta(() =>
|
||||
{
|
||||
@@ -233,11 +307,10 @@ public class PlateOverlapOverlayTests
|
||||
});
|
||||
|
||||
[Theory]
|
||||
[InlineData("add")]
|
||||
[InlineData("remove")]
|
||||
[InlineData("replace")]
|
||||
[InlineData("clear")]
|
||||
public void CollectionEventsImmediatelyDiscardCurrentReportAndPath(string edit) => RunSta(() =>
|
||||
public void SlotChangingCollectionEventsImmediatelyDiscardCurrentReportAndPath(string edit) => RunSta(() =>
|
||||
{
|
||||
using var run = new OverlayRun();
|
||||
run.Finish();
|
||||
@@ -247,7 +320,6 @@ public class PlateOverlapOverlayTests
|
||||
Assert.NotNull(run.View.OverlapOverlay.CachedPath);
|
||||
switch (edit)
|
||||
{
|
||||
case "add": run.View.Plate.Parts.Add(Rectangle()); break;
|
||||
case "remove": run.View.Plate.Parts.RemoveAt(0); break;
|
||||
case "replace": run.View.Plate.Parts[0] = Rectangle(); break;
|
||||
case "clear": run.View.Plate.Parts.Clear(); break;
|
||||
@@ -257,6 +329,23 @@ public class PlateOverlapOverlayTests
|
||||
Assert.Null(run.View.OverlapOverlay.CachedPath);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void AppendingPartRetainsTheExistingPairsPathButInvalidatesTheFullReport() => RunSta(() =>
|
||||
{
|
||||
using var run = new OverlayRun();
|
||||
run.Finish();
|
||||
run.Paint();
|
||||
var path = run.View.OverlapOverlay.CachedPath;
|
||||
Assert.NotNull(path);
|
||||
run.View.Plate.Parts.Add(Rectangle());
|
||||
Assert.Equal(OverlapCheckStatus.Stale, run.View.OverlapStatus);
|
||||
Assert.Null(run.View.OverlapReport);
|
||||
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
||||
run.Paint();
|
||||
Assert.Same(path, run.View.OverlapOverlay.CachedPath);
|
||||
Assert.Equal(1, run.Calls);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void MenusFollowMdiActivationDocumentDisplayAndRunningState() => RunSta(() =>
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
private ObservableList<Part> observedParts;
|
||||
private CancellationTokenSource cancellation;
|
||||
private GraphicsPath path;
|
||||
private PlateOverlapReport pathReport;
|
||||
private IReadOnlyList<PlateOverlapPair> pathPairs;
|
||||
private float pathScale;
|
||||
private Units capturedUnits;
|
||||
private IReadOnlyList<PlateOverlapPair> hoveredPairs = Array.Empty<PlateOverlapPair>();
|
||||
@@ -139,7 +139,6 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
var source = new CancellationTokenSource();
|
||||
cancellation = source;
|
||||
var token = source.Token;
|
||||
ReleasePath();
|
||||
NotifyChanged(toStatusBar: !automatic);
|
||||
try
|
||||
{
|
||||
@@ -204,13 +203,22 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
{
|
||||
materialCache.Clear();
|
||||
baseline = null;
|
||||
Invalidate();
|
||||
state.Invalidate();
|
||||
CancelWorker();
|
||||
ReleasePath();
|
||||
NotifyChanged();
|
||||
}
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
var generation = state.Generation;
|
||||
state.Invalidate();
|
||||
var display = state.DisplayPairs;
|
||||
state.Invalidate(view.Plate);
|
||||
if (!ReferenceEquals(display, state.DisplayPairs))
|
||||
{
|
||||
ReleasePath();
|
||||
ClearHover();
|
||||
}
|
||||
if (generation == state.Generation)
|
||||
{
|
||||
// Already unchecked/stale: an edit restarts the quiet period. Bulk fills raise one
|
||||
@@ -223,7 +231,6 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
return;
|
||||
}
|
||||
CancelWorker();
|
||||
ReleasePath();
|
||||
NotifyChanged();
|
||||
}
|
||||
|
||||
@@ -250,6 +257,8 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
|
||||
private void NotifyChanged(bool toStatusBar = false)
|
||||
{
|
||||
if (!ReferenceEquals(pathPairs, state.DisplayPairs))
|
||||
ReleasePath();
|
||||
ClearHover();
|
||||
if (disposed || view.IsDisposed || view.Disposing)
|
||||
return;
|
||||
@@ -343,7 +352,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
UpdateAutoCheck(repaint: false); // Drags/nudges are only visible to the stamp; the label is drawn below.
|
||||
ValidateHoverTransform();
|
||||
|
||||
if (state.Report != null)
|
||||
if (state.DisplayPairs.Count > 0)
|
||||
{
|
||||
if (state.DisplayMode is OverlapDisplayMode.Areas or OverlapDisplayMode.Both)
|
||||
{
|
||||
@@ -366,11 +375,16 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
if (disposed)
|
||||
return false;
|
||||
var generation = state.Generation;
|
||||
var display = state.DisplayPairs;
|
||||
var fresh = state.EnsureFresh(view.Plate);
|
||||
if (!ReferenceEquals(display, state.DisplayPairs))
|
||||
{
|
||||
ReleasePath();
|
||||
ClearHover();
|
||||
}
|
||||
if (generation != state.Generation)
|
||||
{
|
||||
CancelWorker();
|
||||
ReleasePath();
|
||||
NotifyChanged();
|
||||
}
|
||||
return fresh;
|
||||
@@ -457,7 +471,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
using var crosshair = new Pen(Color.DarkRed, 2 * dpiScale);
|
||||
// Stack coincident pair labels instead of replacing them with a fragment count.
|
||||
var labelRows = new Dictionary<PointF, int>();
|
||||
foreach (var pair in state.Report.Pairs.OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId))
|
||||
foreach (var pair in state.DisplayPairs.OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId))
|
||||
{
|
||||
var center = view.PointWorldToGraph(pair.Centroid);
|
||||
if (!float.IsFinite(center.X) || !float.IsFinite(center.Y))
|
||||
@@ -532,13 +546,13 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
|
||||
private void EnsurePath()
|
||||
{
|
||||
if (ReferenceEquals(pathReport, state.Report) && pathScale == view.ViewScale && path != null)
|
||||
if (ReferenceEquals(pathPairs, state.DisplayPairs) && pathScale == view.ViewScale && path != null)
|
||||
return;
|
||||
ReleasePath();
|
||||
var next = new GraphicsPath(FillMode.Winding);
|
||||
try
|
||||
{
|
||||
foreach (var pair in state.Report.Pairs)
|
||||
foreach (var pair in state.DisplayPairs)
|
||||
{
|
||||
foreach (var region in pair.Regions)
|
||||
{
|
||||
@@ -560,7 +574,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
}
|
||||
}
|
||||
path = next;
|
||||
pathReport = state.Report;
|
||||
pathPairs = state.DisplayPairs;
|
||||
pathScale = view.ViewScale;
|
||||
}
|
||||
catch
|
||||
@@ -592,7 +606,7 @@ internal sealed class OverlapOverlayController : IDisposable
|
||||
{
|
||||
path?.Dispose();
|
||||
path = null;
|
||||
pathReport = null;
|
||||
pathPairs = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -34,8 +34,10 @@ fragment counts. This diagnostic checks shared material, not minimum spacing,
|
||||
plate edges, or cutting-path crossings.
|
||||
|
||||
In a nest window the active plate is checked automatically. Any layout edit
|
||||
(add, remove, reorder, move, rotate, fill) clears the overlay and shows
|
||||
**Overlaps: check pending…**; once the layout has been unchanged for 0.5 s and no
|
||||
(add, remove, reorder, move, rotate, fill) invalidates the full report and shows
|
||||
**Overlaps: check pending…**, but highlights for unchanged pairs remain visible.
|
||||
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
|
||||
@@ -211,8 +213,21 @@ changed code count or rotation is detected). Preparing material dominates
|
||||
first-check time for drawings with many holes. Incremental analysis reuses a pair
|
||||
only when both parts have the same cached source, bit-identical pose, and the same
|
||||
relative input order (clipping is operand-order sensitive), then renumbers it.
|
||||
`InvalidateOverlapCheck()` clears the cache and baseline, so in-place program
|
||||
editors must keep calling it before loading.
|
||||
`InvalidateOverlapCheck()` clears the cache, baseline and every highlight, so
|
||||
in-place program editors must keep calling it before loading.
|
||||
|
||||
Moving one part hides only highlights involving changed parts. Unchanged pairs
|
||||
remain visible throughout the quiet period and background recheck, sharing their
|
||||
existing immutable regions rather than recalculating them. `OverlapReportState`
|
||||
keeps these `DisplayPairs` separate from its full `Report`: the full report is
|
||||
unavailable and the label stays pending/out-of-date/checking until a fresh result
|
||||
is published. Retained highlights are known overlaps, never an all-clear for the
|
||||
edited layout. Each paint checks exact poses and references again, including
|
||||
further edits while an earlier check is pending. Hover details remain disabled
|
||||
until the full report is current. Collection edits conservatively discard pairs
|
||||
whose ordered input slots changed; this display-only path does not renumber them.
|
||||
Explicit geometry invalidation, plate switch, handle loss, cancellation and
|
||||
failure clear all retained highlights.
|
||||
|
||||
Measured on 501 real PEP-converted plates with 2 to 384 parts, a from-scratch check
|
||||
takes median 1 ms, p99 368 ms and max 6.5 s (a 299-part plate); an incremental
|
||||
@@ -227,7 +242,7 @@ PlateView draws the controller overlay after work-area/debug-remnant drawing and
|
||||
before action paint subscribers and hover tooltips. One consistently wound path
|
||||
is filled once, avoiding fragment outlines, internal triangulation seams, and
|
||||
darker triple coverage. World-to-graph conversion excludes pan, because PlateView
|
||||
already applies origin translation. Paths are rebuilt for report/scale changes,
|
||||
already applies origin translation. Paths are rebuilt for displayed-pair/scale changes,
|
||||
not ordinary repaints or panning. The state label saves/restores graphics state.
|
||||
Centroid hit tests use only cached report coordinates and DPI-scaled screen
|
||||
radii. Hover clears on edits, mode/request/view changes, leave, and teardown.
|
||||
@@ -258,7 +273,8 @@ coincident duplicates, and covers pair reuse, issue renumbering, cache clearing
|
||||
cancellation. `OverlapAutoCheckSchedulerTests` covers the quiet period, interaction
|
||||
waits, and the no-retry rule for canceled or failed layouts.
|
||||
`OverlapReportStateTests` verifies request supersession, exact pose/reference
|
||||
freshness, stale clearing, cancellation, and incomplete-versus-clear messaging.
|
||||
freshness, per-pair display retention through repeated edits and pending checks,
|
||||
hard invalidation, cancellation, and incomplete-versus-clear messaging.
|
||||
`PolygonAreaMomentsTests` covers analytic
|
||||
centers, unequal/disconnected fragments, winding, closure, large translations,
|
||||
and invalid/overflow cases. `OverlapPairPresentationTests` checks adaptive unit
|
||||
@@ -277,7 +293,8 @@ dotnet test OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj
|
||||
Linux can cross-build with `-p:EnableWindowsTargeting=true`, but that does not
|
||||
execute Windows tests or verify appearance, DPI, or interaction. On Windows,
|
||||
check partial overlap, containment, inside-hole placement, pan/zoom and quadrant
|
||||
alignment, stale clearing during edits/plate switches, converter cancellation,
|
||||
alignment, changed-pair clearing and unchanged-pair retention during edits and
|
||||
pending rechecks, full clearing on plate switches and converter cancellation,
|
||||
the pending label and automatic recheck after dragging a part onto another,
|
||||
crowded-marker PageUp/PageDown access to the last pair, and repeated
|
||||
check/toggle/close cycles without GDI/disposed-control errors.
|
||||
|
||||
Reference in New Issue
Block a user