feat(desktop): mark cut-plan issues on the plate and trim the inspection panel

Plan Cutting findings dropped the checker's geometry, so selecting one could only
zoom to part extents, and overlap-blocked plates (no preview) did nothing at all.
Findings now keep the check's world-coordinate witness (overlap regions, rapid,
contacts); selecting a row draws it with the Validate Nest overlay, on the proposal
or a blocked plate's live sheet. The witness is excluded from finding equality so
repeated search rejections still collapse to one finding.

The side panel's scrolling report was a wall of text operators skipped. It is now
a colored outcome line, one line for the shown plate, and one remedy or marker
hint above the issue grid, with secondary detail moved to tooltips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-16 09:11:13 -04:00
1 parent 658c1c9ee3
commit c3fa8e79e0
10 files changed
+379 -147

No files matched your search

@@ -66,7 +66,8 @@ internal static class BestEffortCuttingPlan
f.PartNumber is int ordinal ? snapshot.Placements[ordinal - 1].SourcePart : null,
f.OtherPartNumber is int other ? other - 1 : null,
f.OtherPartNumber is int otherOrdinal ? snapshot.Placements[otherOrdinal - 1].SourcePart : null,
f.Kind, f.Message)));
f.Kind, f.Message)
{ Witness = f }));
distance += execution.RapidDistanceFrom(position);
if (!double.IsFinite(distance))
throw new ArgumentException("Best-effort travel is not finite.");
@@ -238,7 +238,11 @@ public sealed class CuttingPlanPlateResult
/// <summary>A diagnostic tied to captured plate and source part positions, not display text.</summary>
public sealed record CuttingPlanDiagnosticRow(int PlateIndex, int PlateNumber, int? PartNumber,
int? OtherPartNumber, string Message);
int? OtherPartNumber, string Message)
{
/// <summary>World-coordinate markers for the problem (overlap area, rapid, contacts); null when none.</summary>
public PostVerificationFinding Witness { get; init; }
}
/// <summary>
/// The outcome of a batch. Apply is all-or-nothing. Unverified output needs explicit acceptance.
@@ -277,9 +281,16 @@ public sealed class CuttingPlanProposal
if (plate.Overlap != null)
{
foreach (var pair in plate.Overlap.Pairs)
rows.Add(new(index, plate.PlateNumber, pair.PartAId + 1, pair.PartBId + 1,
$"Part {pair.PartAId + 1}{Name(pair.PartAName)} overlaps part "
+ $"{pair.PartBId + 1}{Name(pair.PartBName)}."));
{
var message = $"Part {pair.PartAId + 1}{Name(pair.PartAName)} overlaps part "
+ $"{pair.PartBId + 1}{Name(pair.PartBName)}.";
rows.Add(new(index, plate.PlateNumber, pair.PartAId + 1, pair.PartBId + 1, message)
{
Witness = new(PostVerificationKind.Overlap, plate.PlateNumber, pair.PartAId + 1,
pair.PartBId + 1, message)
{ Location = pair.Marker, Overlap = pair }
});
}
foreach (var issue in plate.Overlap.Issues)
rows.Add(new(index, plate.PlateNumber, issue.PartAId + 1,
issue.PartBId is int other ? other + 1 : null,
@@ -291,7 +302,8 @@ public sealed class CuttingPlanProposal
rows.Add(new(index, plate.PlateNumber,
finding.SourceOrdinal is int part ? part + 1 : null,
finding.OtherSourceOrdinal is int other ? other + 1 : null,
DescribeFinding(finding)));
DescribeFinding(finding))
{ Witness = finding.Witness });
}
return rows;
}
@@ -178,7 +178,23 @@ public enum CuttingPlanStatus
/// <summary>Ordinals are zero-based source positions, not proposed sequence positions.</summary>
public sealed record CuttingPlanFinding(int? SourceOrdinal, Part SourcePart,
int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message);
int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message)
{
/// <summary>
/// The check's own finding, for its world-coordinate markers (location, rapid, contacts).
/// Its part numbers are check positions, not source ordinals. Null when the check gave none.
/// Not part of equality: repeated rejections of the same problem stay one finding.
/// </summary>
public PostVerificationFinding Witness { get; init; }
public bool Equals(CuttingPlanFinding other) => other is not null
&& SourceOrdinal == other.SourceOrdinal && Equals(SourcePart, other.SourcePart)
&& OtherSourceOrdinal == other.OtherSourceOrdinal && Equals(OtherSourcePart, other.OtherSourcePart)
&& Kind == other.Kind && Message == other.Message;
public override int GetHashCode() =>
HashCode.Combine(SourceOrdinal, SourcePart, OtherSourceOrdinal, OtherSourcePart, Kind, Message);
}
/// <summary>
/// A replayed direct-XY proposal, optionally with regenerated programs, that respects the
@@ -403,6 +403,7 @@ public static class CuttingPlanService
var source = finding.PartNumber is { } part ? snapshot.Placements[part - 1] : null;
var other = finding.OtherPartNumber is { } otherPart ? snapshot.Placements[otherPart - 1] : null;
return new CuttingPlanFinding(source?.SourceOrdinal, source?.SourcePart,
other?.SourceOrdinal, other?.SourcePart, finding.Kind, finding.Message);
other?.SourceOrdinal, other?.SourcePart, finding.Kind, finding.Message)
{ Witness = finding };
});
}
@@ -179,7 +179,8 @@ internal static class JointCuttingPlanSearch
var source = f.PartNumber is { } p ? snapshot.Placements[p - 1] : null;
var other = f.OtherPartNumber is { } o ? snapshot.Placements[o - 1] : null;
return new CuttingPlanFinding(source?.SourceOrdinal, source?.SourcePart,
other?.SourceOrdinal, other?.SourcePart, f.Kind, f.Message);
other?.SourceOrdinal, other?.SourcePart, f.Kind, f.Message)
{ Witness = f };
});
/// <summary>