From 405a867a770870fbc21453534c44a729bc106fda Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 16 Oct 2026 09:18:48 -0400 Subject: [PATCH] fix(desktop): draw planned cut-offs and issue markers on the plan preview The proposal preview skipped the cut-off overlay: it carries cut-off parts but no cut-off definitions, so planner cut-offs looked like thin black part outlines instead of the orange-red dashes used everywhere else. The preview now draws each cut-off part's own cutting moves in that style. The same paint guard hid the validation overlay over a preview, so issue markers for unverified plans were set but never drawn. The overlay now draws on proposals too, with the marker's part numbers mapped to the preview's proposed order so the right parts stay highlighted. Co-Authored-By: Claude Opus 5.5 --- .../Forms/CuttingInspectionPanelTests.cs | 35 +++++++++++++++++++ OpenNest/Controls/PlateRenderer.cs | 35 +++++++++++++++++++ OpenNest/Controls/PlateView.cs | 5 +-- OpenNest/Forms/CuttingInspectionPanel.cs | 12 ++++++- docs/cutting-planner.md | 3 +- 5 files changed, 86 insertions(+), 4 deletions(-) diff --git a/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs b/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs index da9e58c9..a2334969 100644 --- a/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs +++ b/OpenNest.WinForms.Tests/Forms/CuttingInspectionPanelTests.cs @@ -217,6 +217,41 @@ public class CuttingInspectionPanelTests Assert.Null(view.ValidationOverlay.Finding); }); + [Fact] + public void PlannedCutOffsKeepTheCutOffLineStyleOnTheProposal() => RunSta(() => + { + using var editor = Editor(); + editor.Size = new System.Drawing.Size(1300, 800); + editor.Show(); + editor.PlateManager.LoadAt(0); + editor.OpenCuttingInspection(); + var panel = editor.CuttingInspection!; + PumpUntil(() => panel.Proposal != null && !panel.IsPlanning); + var view = editor.PlateView; + var baseline = CutOffPixels(view); + Field(panel, "autoCutOffsCheckBox").Checked = true; + PumpUntil(() => panel.Proposal != null && !panel.IsPlanning); + Assert.True(view.HasProposalDisplay); + Assert.Contains(view.DisplayPlate.Parts, part => part.BaseDrawing.IsCutOff); + Assert.True(CutOffPixels(view) > baseline + 50, "Planned cut-offs should draw as orange-red dashes."); + panel.RequestClose(); + + static int CutOffPixels(OpenNest.Controls.PlateView view) + { + using var bitmap = new System.Drawing.Bitmap(view.Width, view.Height); + view.DrawToBitmap(bitmap, new System.Drawing.Rectangle(0, 0, view.Width, view.Height)); + var count = 0; + for (var x = 0; x < bitmap.Width; x++) + for (var y = 0; y < bitmap.Height; y++) + { + var pixel = bitmap.GetPixel(x, y); + if (pixel.R > 220 && pixel.G is > 40 and < 110 && pixel.B < 40) + count++; + } + return count; + } + }); + [Fact] public void NextAndPreviousShowTheSelectedReadyPlateNotTheOpeningPlate() => RunSta(() => { diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs index 4bc35841..f0849f5c 100644 --- a/OpenNest/Controls/PlateRenderer.cs +++ b/OpenNest/Controls/PlateRenderer.cs @@ -250,6 +250,11 @@ namespace OpenNest.Controls public void DrawCutOffs(Graphics g) { var plate = view.DisplayPlate; + if (view.HasProposalDisplay) + { + DrawProposedCutOffs(g, plate); + return; + } if (plate?.CutOffs == null || plate.CutOffs.Count == 0) return; @@ -277,6 +282,36 @@ namespace OpenNest.Controls } } + /// + /// A proposal preview carries cut-off parts but no cut-off definitions, so draw each + /// cut-off part's own (absolute) cutting moves in the same dashed style. + /// + private void DrawProposedCutOffs(Graphics g, Plate plate) + { + if (plate == null) + return; + + using var pen = new Pen(CutOffLineColor, 2.0f) { DashStyle = DashStyle.Dash }; + + foreach (var part in plate.Parts) + { + var program = part.BaseDrawing.IsCutOff ? part.Program : null; + if (program == null || program.Mode != Mode.Absolute) + continue; + + Vector? position = null; + foreach (var code in program.Codes) + { + if (code is not Motion motion) + continue; + var end = motion.EndPoint + part.Location; + if (motion is LinearMove && position is { } start) + DrawLine(g, start, end, pen); + position = end; + } + } + } + public void DrawActiveWorkArea(Graphics g) { var workArea = view.ActiveWorkArea; diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 66a83b1a..6a6f4343 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -728,14 +728,15 @@ namespace OpenNest.Controls renderer.DrawParts(e.Graphics); renderer.DrawClampZones(e.Graphics); renderer.DrawRemovedClamps(e.Graphics); + renderer.DrawCutOffs(e.Graphics); if (!HasProposalDisplay) { - renderer.DrawCutOffs(e.Graphics); renderer.DrawActiveWorkArea(e.Graphics); renderer.DrawDebugRemnants(e.Graphics); DrawOverlapOverlay(e.Graphics); - validationOverlay.Draw(e.Graphics); } + // Plan Cutting marks findings on its proposal as well as on the live plate. + validationOverlay.Draw(e.Graphics); base.OnPaint(e); diff --git a/OpenNest/Forms/CuttingInspectionPanel.cs b/OpenNest/Forms/CuttingInspectionPanel.cs index 2cb2fd91..db538bb8 100644 --- a/OpenNest/Forms/CuttingInspectionPanel.cs +++ b/OpenNest/Forms/CuttingInspectionPanel.cs @@ -805,6 +805,11 @@ internal sealed class CuttingInspectionPanel : UserControl view.ZoomToFit(); } + /// A source part number as numbered on the canvas: proposed order on a preview, else unchanged. + private int? ShownPartNumber(int plateIndex, int? sourceNumber) => + preview == null || sourceNumber is not int source ? sourceNumber + : CuttingProposalInspection.ProposedPosition(Proposal, plateIndex, source) + 1; + private void NavigateFinding() { if (Mode == CuttingInspectionMode.Validate) @@ -817,7 +822,12 @@ internal sealed class CuttingInspectionPanel : UserControl return; ShowPlate(row.PlateIndex); // Witnesses are world coordinates, so they mark the proposal and a blocked plate's live view alike. - if (row.Witness is { } witness && ShowMarker(witness)) + // Their part numbers drive which parts stay highlighted: use the shown plate's numbering. + if (row.Witness is { } witness && ShowMarker(witness with + { + PartNumber = ShownPartNumber(row.PlateIndex, row.PartNumber), + OtherPartNumber = ShownPartNumber(row.PlateIndex, row.OtherPartNumber), + })) { SetDetail(MarkerHint(witness)); return; diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index 87a7da4b..2f5fd250 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -432,7 +432,8 @@ panel is wired in the editor. Non-editor callers still use the shared analyzer. *before* capture, so the cutting plan sequences the new lines like any other cut-off. These are the only live changes made before Apply: Apply keeps them, while unticking the box, Close or switching to Validate removes them and - restores the earlier cut-offs and sequence. A blocked cut-off plan adds nothing and + restores the earlier cut-offs and sequence. The proposal preview draws planned cut-offs + with the same orange-red dashed line as the editor. A blocked cut-off plan adds nothing and plans nothing; the panel names the plate and reason. `Cut-off Settings...` opens the Automatic Scrap Cut-offs dialog in settings mode for the same scope: it previews on the active plate as usual, but `Use These Settings`