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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-16 09:18:48 -04:00
1 parent c3fa8e79e0
commit 405a867a77
5 files changed
+86 -4

No files matched your search

@@ -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<CheckBox>(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(() =>
{
+35
View File
@@ -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
}
}
/// <summary>
/// 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.
/// </summary>
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;
+3 -2
View File
@@ -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);
+11 -1
View File
@@ -805,6 +805,11 @@ internal sealed class CuttingInspectionPanel : UserControl
view.ZoomToFit();
}
/// <summary>A source part number as numbered on the canvas: proposed order on a preview, else unchanged.</summary>
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;
+2 -1
View File
@@ -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`