diff --git a/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs b/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs
index efc39683..821f7ced 100644
--- a/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs
+++ b/OpenNest.Engine/CuttingPlanning/BestEffortCuttingPlan.cs
@@ -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.");
diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs
index 06474fa2..aea9df1e 100644
--- a/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs
+++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs
@@ -238,7 +238,11 @@ public sealed class CuttingPlanPlateResult
/// A diagnostic tied to captured plate and source part positions, not display text.
public sealed record CuttingPlanDiagnosticRow(int PlateIndex, int PlateNumber, int? PartNumber,
- int? OtherPartNumber, string Message);
+ int? OtherPartNumber, string Message)
+{
+ /// World-coordinate markers for the problem (overlap area, rapid, contacts); null when none.
+ public PostVerificationFinding Witness { get; init; }
+}
///
/// 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;
}
diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs
index 39360560..c07ee0c5 100644
--- a/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs
+++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanModels.cs
@@ -178,7 +178,23 @@ public enum CuttingPlanStatus
/// Ordinals are zero-based source positions, not proposed sequence positions.
public sealed record CuttingPlanFinding(int? SourceOrdinal, Part SourcePart,
- int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message);
+ int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message)
+{
+ ///
+ /// 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.
+ ///
+ 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);
+}
///
/// A replayed direct-XY proposal, optionally with regenerated programs, that respects the
diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs
index 23b7eae4..f03d4035 100644
--- a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs
+++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs
@@ -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 };
});
}
diff --git a/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs b/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs
index e7288bae..00433724 100644
--- a/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs
+++ b/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs
@@ -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 };
});
///
diff --git a/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs b/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs
index e30841a4..da9e58c9 100644
--- a/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs
+++ b/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs
@@ -171,13 +171,13 @@ public class CuttingInspectionPanelTests
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal(2, panel.Proposal.Plates.Count);
- Assert.Equal(ScrollBars.Vertical, Field(panel, "resultLabel").ScrollBars);
+ Assert.Contains("Can't apply", Field