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