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`