From c3fa8e79e082fca8d4e4fb70bbcc1eb791f53269 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 16 Oct 2026 09:11:13 -0400 Subject: [PATCH 1/3] 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 --- .../CuttingPlanning/BestEffortCuttingPlan.cs | 3 +- .../CuttingPlanning/CuttingPlanBatch.cs | 22 +- .../CuttingPlanning/CuttingPlanModels.cs | 18 +- .../CuttingPlanning/CuttingPlanService.cs | 3 +- .../CuttingPlanning/JointCuttingPlanSearch.cs | 3 +- .../Forms/CuttingInspectionPanelTests.cs | 41 +- .../Forms/CuttingValidationPanelTests.cs | 17 +- OpenNest/Forms/CuttingInspectionPanel.cs | 369 +++++++++++++----- docs/cutting-planner.md | 44 ++- docs/post-verification.md | 6 +- 10 files changed, 379 insertions(+), 147 deletions(-) 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