feat(diagnostics): cache overlap material and recheck incrementally

PlateOverlapAnalyzer.Capture(parts, OverlapMaterialCache) reuses each clean
program's converted entities and prepared material across requests, and
Analyze(snapshot, previous) reuses pair results whose two parts kept the
same source, exact pose and relative order, renumbering them. A recheck
after moving one part only clips that part's neighbors again.

On 501 real PEP plates: full check median 1 ms / max 6.4 s; incremental
recheck after one move median 0.1 ms / max 38 ms. Incremental results
matched uncached full analysis exactly across 2505 edits.
This commit is contained in:
aj
2026-09-29 09:31:26 -04:00
parent 2b8a3ce7db
commit 99c31748ba
4 changed files with 395 additions and 39 deletions
@@ -7,12 +7,17 @@ namespace OpenNest.Diagnostics;
/// <summary>An owned diagnostic result. An empty Pairs list is clear only if IsComplete is true.</summary>
public sealed class PlateOverlapReport
{
internal PlateOverlapReport(List<PlateOverlapPair> pairs, List<PlateOverlapIssue> issues)
internal PlateOverlapReport(List<PlateOverlapPair> pairs, List<PlateOverlapIssue> issues,
PlateOverlapSnapshot snapshot)
{
Pairs = pairs.AsReadOnly();
Issues = issues.AsReadOnly();
Snapshot = snapshot;
}
/// <summary>The owned input this report was computed from; the baseline for incremental rechecks.</summary>
internal PlateOverlapSnapshot Snapshot { get; }
public IReadOnlyList<PlateOverlapPair> Pairs { get; }
public IReadOnlyList<PlateOverlapIssue> Issues { get; }
public bool IsComplete => Issues.Count == 0;
@@ -24,6 +29,23 @@ public sealed class PlateOverlapPair
{
private readonly Box bounds;
private PlateOverlapPair(PlateOverlapPair source, int partAId, int partBId, string partAName,
string partBName)
{
PartAId = partAId;
PartBId = partBId;
PartAName = partAName;
PartBName = partBName;
Regions = source.Regions;
Area = source.Area;
Centroid = source.Centroid;
bounds = source.bounds;
}
/// <summary>The same immutable geometry under new input positions and captured names.</summary>
internal PlateOverlapPair Renumber(int partAId, int partBId, string partAName, string partBName) =>
new(this, partAId, partBId, partAName, partBName);
internal PlateOverlapPair(int partAId, int partBId, string partAName, string partBName,
List<PlateOverlapRegion> regions, Vector centroid)
{
@@ -90,5 +112,5 @@ public sealed class PlateOverlapSnapshot
internal IReadOnlyList<PlateOverlapIssue> Issues { get; }
}
internal sealed record CapturedOverlapPart(int Id, string Name, List<Entity> Entities,
internal sealed record CapturedOverlapPart(int Id, string Name, OverlapSource Source,
double Rotation, Vector Location);