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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-16 09:11:13 -04:00
1 parent 658c1c9ee3
commit c3fa8e79e0
10 files changed
+379 -147

No files matched your search

@@ -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.");
@@ -238,7 +238,11 @@ public sealed class CuttingPlanPlateResult
/// <summary>A diagnostic tied to captured plate and source part positions, not display text.</summary>
public sealed record CuttingPlanDiagnosticRow(int PlateIndex, int PlateNumber, int? PartNumber,
int? OtherPartNumber, string Message);
int? OtherPartNumber, string Message)
{
/// <summary>World-coordinate markers for the problem (overlap area, rapid, contacts); null when none.</summary>
public PostVerificationFinding Witness { get; init; }
}
/// <summary>
/// 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;
}
@@ -178,7 +178,23 @@ public enum CuttingPlanStatus
/// <summary>Ordinals are zero-based source positions, not proposed sequence positions.</summary>
public sealed record CuttingPlanFinding(int? SourceOrdinal, Part SourcePart,
int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message);
int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message)
{
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// A replayed direct-XY proposal, optionally with regenerated programs, that respects the
@@ -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 };
});
}
@@ -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 };
});
/// <summary>
@@ -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<TextBox>(panel, "resultLabel").ScrollBars);
Assert.Contains("Can't apply", Field<Label>(panel, "statusLabel").Text);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.True(editor.PlateView.HasProposalDisplay);
Field<Button>(panel, "nextButton").PerformClick();
Assert.Same(second, editor.PlateView.Plate); // Refusal shows the requested live plate, not the opening plate.
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.Contains("no current preview", Field<Label>(panel, "previewLabel").Text);
Assert.Contains("blocked", Field<Label>(panel, "previewLabel").Text);
var grid = Field<DataGridView>(panel, "warningGrid");
Assert.Equal(panel.Proposal.DiagnosticRows().Count, grid.Rows.Count);
var row = grid.Rows.Cast<DataGridViewRow>().First(r => r.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow d && d.PlateIndex == 1);
@@ -189,6 +189,34 @@ public class CuttingInspectionPanelTests
Assert.Same(original, editor.PlateView.Plate);
});
[Fact]
public void BlockedOverlapRowShadesTheOverlapOnTheLivePlate() => RunSta(() =>
{
using var editor = Editor();
var plate = editor.Nest.Plates[0];
plate.Parts.Add(Square("overlapping", 5));
editor.Show();
editor.PlateManager.LoadAt(0);
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.Contains("Can't apply", Field<Label>(panel, "statusLabel").Text);
Assert.Contains("parts overlap", Field<Label>(panel, "previewLabel").Text);
var view = editor.PlateView;
Assert.False(view.HasProposalDisplay);
var grid = Field<DataGridView>(panel, "warningGrid");
var row = grid.Rows.Cast<DataGridViewRow>().First(r =>
r.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow { Witness.Overlap: not null });
grid.ClearSelection();
row.Selected = true;
Assert.Same(plate, view.Plate);
Assert.NotNull(view.ValidationOverlay.Finding?.Overlap);
Assert.Contains("overlapping material", Field<Label>(panel, "detailLabel").Text);
panel.RequestClose();
Assert.Null(view.ValidationOverlay.Finding);
});
[Fact]
public void NextAndPreviousShowTheSelectedReadyPlateNotTheOpeningPlate() => RunSta(() =>
{
@@ -260,7 +288,10 @@ public class CuttingInspectionPanelTests
var partRow = grid.Rows.Cast<DataGridViewRow>().First(r => r.Cells[1].Value is int);
grid.ClearSelection();
partRow.Selected = true;
Assert.Contains("affected part extent", Field<Label>(panel, "previewLabel").Text);
if (partRow.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow { Witness.Location: not null })
Assert.NotNull(editor.PlateView.ValidationOverlay.Finding);
else
Assert.Contains("affected part", Field<Label>(panel, "detailLabel").Text);
Field<Button>(panel, "applyButton").PerformClick();
Assert.Null(panel.CommitResult);
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
@@ -287,7 +318,7 @@ public class CuttingInspectionPanelTests
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.False(Field<CheckBox>(panel, "acceptWarningsCheckBox").Checked);
Assert.Contains("Replan", Field<TextBox>(panel, "resultLabel").Text);
Assert.Contains("Replan", Field<Label>(panel, "statusLabel").Text);
Assert.Equal(programs, plate.Parts.Select(p => p.Program));
Assert.All(plate.Parts, p => Assert.False(p.HasManualLeadIns));
panel.StartPlanning();
@@ -349,7 +380,7 @@ public class CuttingInspectionPanelTests
Assert.False(Field<Button>(panel, "applyButton").Enabled);
Assert.False(Field<CheckBox>(panel, "acceptWarningsCheckBox").Checked);
Assert.False(editor.PlateView.HasProposalDisplay);
Assert.Contains("Nothing can be applied", Field<TextBox>(panel, "resultLabel").Text);
Assert.Contains("Nothing can be applied", Field<Label>(panel, "statusLabel").Text);
Assert.Equal(original, editor.PlateView.Plate.Parts);
panel.RequestClose();
});
@@ -57,10 +57,10 @@ public class CuttingValidationPanelTests
Assert.Contains(Enumerable.Range(-2, 5).SelectMany(dx => Enumerable.Range(3, 7)
.Select(dy => bitmap.GetPixel(cx + dx, cy + dy))), pixel =>
pixel.R > 150 && pixel.B > 50 && pixel.G < 100);
Assert.Contains("Crosshairs", Field<TextBox>(panel, "resultLabel").Text);
Assert.Contains("Confirm minimum spacing, plate boundaries and final post output separately", Field<Label>(panel, "validationScopeLabel").Text);
Assert.Contains("crosshairs", Field<Label>(panel, "detailLabel").Text);
Assert.Contains("Spacing, plate edges and post output are not checked", Field<Label>(panel, "validationScopeLabel").Text);
Assert.True(Field<Label>(panel, "validationScopeLabel").Visible);
Assert.Contains("Nest validation (live programs)", Field<TextBox>(panel, "resultLabel").Text);
Assert.Contains("rapid crossing", Field<Label>(panel, "statusLabel").Text);
panel.RequestClose();
Assert.Same(original, view.Plate);
Assert.Equal(viewport, view.CaptureViewport());
@@ -82,9 +82,9 @@ public class CuttingValidationPanelTests
editor.OpenValidationInspection();
var panel = editor.CuttingInspection!;
Pump(() => panel.ValidationReport != null && !panel.IsPlanning);
Assert.Contains(incomplete ? "Validation incomplete" : "No warnings found", Field<Label>(panel, "previewLabel").Text);
Assert.Contains(incomplete ? "Validation incomplete" : "No warnings found", Field<Label>(panel, "statusLabel").Text);
Assert.True(Field<Label>(panel, "validationScopeLabel").Visible);
Assert.Contains("LIVE NEST only", Field<Label>(panel, "validationScopeLabel").Text);
Assert.Contains("live nest", Field<Label>(panel, "validationScopeLabel").Text);
Assert.False(Field<Button>(panel, "applyButton").Enabled);
panel.RequestClose();
});
@@ -184,8 +184,8 @@ public class CuttingValidationPanelTests
Assert.Null(panel.ValidationReport);
Assert.Null(editor.PlateView.ValidationOverlay.Finding);
Assert.Empty(Field<DataGridView>(panel, "warningGrid").Rows.Cast<DataGridViewRow>());
Assert.Contains("Validation failed", Field<TextBox>(panel, "resultLabel").Text);
Assert.DoesNotContain("No warnings found", Field<Label>(panel, "previewLabel").Text);
Assert.Contains("Validation failed", Field<Label>(panel, "statusLabel").Text);
Assert.DoesNotContain("No warnings found", Field<Label>(panel, "statusLabel").Text);
panel.RequestClose();
});
@@ -245,13 +245,12 @@ public class CuttingValidationPanelTests
Assert.True(grid.ReadOnly);
Assert.Equal(DataGridViewSelectionMode.FullRowSelect, grid.SelectionMode);
Assert.All(grid.Columns.Cast<DataGridViewColumn>(), c => Assert.Equal(DataGridViewColumnSortMode.NotSortable, c.SortMode));
Assert.True(Field<TextBox>(panel, "resultLabel").ReadOnly);
var row = grid.Rows.Cast<DataGridViewRow>().Single(r => r.Tag is PostVerificationFinding { Kind: PostVerificationKind.Overlap });
row.Selected = true;
var view = editor.PlateView;
Assert.Same(plate, view.Plate);
Assert.NotNull(view.ValidationOverlay.Finding.Overlap);
Assert.Contains("largest patch", Field<TextBox>(panel, "resultLabel").Text);
Assert.Contains("overlapping material", Field<Label>(panel, "detailLabel").Text);
var scale = view.ViewScale;
InvokeView(view, "OnMouseWheel", new MouseEventArgs(MouseButtons.None, 0, 100, 100, 120));
Assert.True(view.ViewScale > scale);
+267 -102
View File
@@ -1,5 +1,6 @@
using System;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
@@ -42,17 +43,22 @@ internal sealed class CuttingInspectionPanel : UserControl
internal Func<Nest, CancellationToken, Task<PostVerificationReport>> Analyze { get; set; }
= (nest, token) => Task.Run(() => PostVerificationAnalyzer.Analyze(nest, token), token);
private readonly Button planButton = new() { Text = "Replan", AutoSize = true };
private readonly Button cancelButton = new() { Text = "Cancel / Close", AutoSize = true };
private readonly TextBox resultLabel = new()
private readonly Button cancelButton = new() { Text = "Close", AutoSize = true };
// One headline the operator reads first; detail holds at most a remedy or a marker hint.
private readonly Label statusLabel = new()
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
WordWrap = true,
MaxLength = int.MaxValue,
AccessibleName = "Cutting plan outcome and remedies"
AutoSize = true,
MaximumSize = new Size(440, 0),
Margin = new Padding(3, 10, 3, 3),
AccessibleName = "Cutting plan outcome"
};
private readonly Label detailLabel = new()
{
AutoSize = true,
MaximumSize = new Size(440, 0),
AccessibleName = "Cutting plan remedy or marker hint"
};
private readonly ToolTip toolTip = new();
private readonly Button settingsButton = new() { Text = "Cutting Settings…", AutoSize = true };
private readonly Button applyButton = new() { Text = "Apply", AutoSize = true, Enabled = false };
private readonly Button previousButton = new() { Text = "Previous", AutoSize = true };
@@ -91,15 +97,23 @@ internal sealed class CuttingInspectionPanel : UserControl
// Last cut-off settings chosen here, per unit system, for the rest of this session.
private static readonly System.Collections.Generic.Dictionary<Units, AutomaticCutOffOptions> lastCutOffOptions = new();
private AutomaticCutOffStaging stagedCutOffs;
private readonly CheckBox acceptWarningsCheckBox = new() { Text = "Accept UNVERIFIED plan warnings (post consent happens separately)", AutoSize = true, Visible = false };
private readonly Label settingsLabel = new() { AutoSize = true, MaximumSize = new Size(440, 0) };
private readonly CheckBox acceptWarningsCheckBox = new() { Text = "Accept the unverified plan", AutoSize = true, Visible = false };
private readonly Label settingsLabel = new()
{
AutoSize = true,
MaximumSize = new Size(300, 0),
ForeColor = SystemColors.GrayText,
Anchor = AnchorStyles.Left,
};
private readonly Label previewLabel = new() { AutoSize = true, MaximumSize = new Size(440, 0) };
private readonly Label validationScopeLabel = new()
{
AutoSize = true,
MaximumSize = new Size(440, 0),
Visible = false,
Text = "LIVE NEST only — reviews overlaps, lead-ins and rapid crossings. Confirm minimum spacing, plate boundaries and final post output separately, before posting."
ForeColor = SystemColors.GrayText,
Text = "Checks the live nest for overlaps, missing lead-ins and rapid crossings. "
+ "Spacing, plate edges and post output are not checked."
};
private readonly DataGridView warningGrid = GridStyle.Apply(new DataGridView
{
@@ -133,10 +147,15 @@ internal sealed class CuttingInspectionPanel : UserControl
viewport = view.CaptureViewport();
overlapDisplay = view.OverlapDisplay;
var header = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, FlowDirection = FlowDirection.TopDown, WrapContents = false };
var buttons = new FlowLayoutPanel { AutoSize = true };
buttons.Controls.AddRange([settingsButton, planButton, applyButton, cancelButton]);
var navigation = new FlowLayoutPanel { AutoSize = true };
var settingsRow = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = Padding.Empty };
settingsRow.Controls.AddRange([settingsButton, settingsLabel]);
var buttons = new FlowLayoutPanel { AutoSize = true, Margin = new Padding(0, 6, 0, 0) };
buttons.Controls.AddRange([planButton, applyButton, cancelButton]);
var navigation = new FlowLayoutPanel { AutoSize = true, Margin = Padding.Empty };
navigation.Controls.AddRange([previousButton, nextButton]);
statusLabel.Font = new Font(Font.FontFamily, Font.Size + 1, FontStyle.Bold);
toolTip.SetToolTip(acceptWarningsCheckBox, "Some checks did not pass. Posting still runs its own checks and asks separately.");
toolTip.SetToolTip(keepOrderCheckBox, "Plan lead-ins and cut direction without changing which part is cut first.");
modeSelector.Items.AddRange(["Plan Cutting", "Validate Nest (live programs)"]);
Mode = mode;
modeSelector.SelectedIndex = (int)mode;
@@ -152,23 +171,21 @@ internal sealed class CuttingInspectionPanel : UserControl
cutOffRow.Controls.AddRange([autoCutOffsCheckBox, cutOffSettingsButton]);
existingWorkLabel.Font = new Font(Font, FontStyle.Bold);
existingWorkBanner.Controls.AddRange([existingWorkLabel, clearExistingButton]);
header.Controls.AddRange([modeSelector, existingWorkBanner, settingsLabel, keepOrderCheckBox, cutOffRow, buttons,
acceptWarningsCheckBox, navigation, previewLabel, validationScopeLabel]);
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3 };
header.Controls.AddRange([modeSelector, existingWorkBanner, settingsRow, keepOrderCheckBox, cutOffRow,
validationScopeLabel, buttons, acceptWarningsCheckBox, statusLabel, navigation, previewLabel, detailLabel]);
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 2 };
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 30));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 70));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
layout.Controls.Add(header, 0, 0);
layout.Controls.Add(resultLabel, 0, 1);
layout.Controls.Add(warningGrid, 0, 2);
layout.Controls.Add(warningGrid, 0, 1);
Controls.Add(layout);
foreach (var name in new[] { "Plate", "Part", "With", "Warning / finding" })
foreach (var name in new[] { "Plate", "Part", "With", "Issue" })
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = name,
Width = 48,
SortMode = DataGridViewColumnSortMode.NotSortable,
AutoSizeMode = name == "Warning / finding" ? DataGridViewAutoSizeColumnMode.Fill : DataGridViewAutoSizeColumnMode.None
AutoSizeMode = name == "Issue" ? DataGridViewAutoSizeColumnMode.Fill : DataGridViewAutoSizeColumnMode.None
});
Padding = new Padding(10);
planButton.Click += (_, _) => StartCurrentMode();
@@ -184,8 +201,28 @@ internal sealed class CuttingInspectionPanel : UserControl
nextButton.Click += (_, _) => ShowPlate(selectedPlate + 1);
warningGrid.SelectionChanged += (_, _) => NavigateFinding();
ShowSettings();
resultLabel.Text = "Read-only proposal inspection. Nothing live changes before Apply, except automatic cut-offs "
+ "you choose to add; those are removed again unless you Apply.";
}
private enum Tone { Neutral, Good, Warning, Bad }
/// <summary>Sets the headline and replaces the detail line (empty hides it).</summary>
private void SetStatus(Tone tone, string text, string detail = null)
{
statusLabel.Text = text;
statusLabel.ForeColor = tone switch
{
Tone.Good => Color.FromArgb(0, 128, 0),
Tone.Warning => Color.FromArgb(176, 96, 0),
Tone.Bad => Color.FromArgb(192, 0, 0),
_ => SystemColors.ControlText,
};
SetDetail(detail);
}
private void SetDetail(string text)
{
detailLabel.Text = text ?? string.Empty;
detailLabel.Visible = !string.IsNullOrEmpty(text);
}
internal bool IsPlanning => inspection.IsRunning || validationCancellation != null;
@@ -217,8 +254,9 @@ internal sealed class CuttingInspectionPanel : UserControl
private void ShowCutOffOptions()
{
var unit = UnitsHelper.GetShortString(units);
autoCutOffsCheckBox.Text = $"Add automatic scrap cut-offs ({cutOffOptions.Spacing:0.###} {unit} strips, "
+ $"keep tails of {cutOffOptions.MinimumTailLength:0.###} {unit} or more)";
autoCutOffsCheckBox.Text = $"Add scrap cut-offs ({cutOffOptions.Spacing:0.###} {unit} strips)";
toolTip.SetToolTip(autoCutOffsCheckBox, $"Keeps tails of {cutOffOptions.MinimumTailLength:0.###} {unit} or more. "
+ "The cut-offs are added for planning and removed again unless you Apply.");
}
/// <summary>
@@ -237,7 +275,7 @@ internal sealed class CuttingInspectionPanel : UserControl
}
catch (Exception ex)
{
resultLabel.Text = $"Unable to remove the unapplied automatic cut-offs: {ex.Message}";
SetStatus(Tone.Bad, "Couldn't remove the unapplied cut-offs.", ex.Message);
return;
}
RefreshExistingWork();
@@ -251,7 +289,7 @@ internal sealed class CuttingInspectionPanel : UserControl
}
catch (Exception ex)
{
resultLabel.Text = $"Unable to open the cut-off settings: {ex.Message}";
SetStatus(Tone.Bad, "Couldn't open the cut-off settings.", ex.Message);
}
finally
{
@@ -311,10 +349,11 @@ internal sealed class CuttingInspectionPanel : UserControl
settingsButton.Visible = settingsLabel.Visible = keepOrderCheckBox.Visible = applyButton.Visible = planning;
autoCutOffsCheckBox.Visible = cutOffSettingsButton.Visible = planning;
RefreshExistingWork();
previousButton.Visible = nextButton.Visible = planning;
previousButton.Visible = nextButton.Visible = planning && plates.Length > 1;
previewLabel.Visible = planning;
validationScopeLabel.Visible = !planning;
planButton.Text = planning ? "Replan" : "Recheck all plates";
previewLabel.Text = planning ? "No current preview." : "LIVE NEST — read-only validation of all plates.";
previewLabel.Text = "No current preview.";
}
internal void SwitchMode(CuttingInspectionMode mode)
@@ -323,7 +362,7 @@ internal sealed class CuttingInspectionPanel : UserControl
pendingMode = mode;
ClearResult();
SetPlanning(true);
resultLabel.Text = "Switching modes… waiting for the worker.";
SetStatus(Tone.Neutral, "Switching…");
if (validationCancellation != null) validationCancellation.Cancel();
if (inspection.IsRunning) inspection.RequestClose();
if (!IsPlanning) FinishTransition();
@@ -358,12 +397,12 @@ internal sealed class CuttingInspectionPanel : UserControl
try { RevertStagedCutOffs(); } // Validate what is really on the plates, not an unapplied plan.
catch (Exception ex)
{
resultLabel.Text = $"Unable to remove the unapplied automatic cut-offs: {ex.Message}";
SetStatus(Tone.Bad, "Couldn't remove the unapplied cut-offs.", ex.Message);
return;
}
ConfigureMode();
SetPlanning(true);
resultLabel.Text = "Checking all live plates: overlaps, missing lead-ins and rapid crossings…";
SetStatus(Tone.Neutral, "Checking all plates…");
var cancellation = validationCancellation = new CancellationTokenSource();
var run = ++generation;
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -383,23 +422,22 @@ internal sealed class CuttingInspectionPanel : UserControl
SetPlanning(false);
if (!task.IsCompletedSuccessfully)
{
resultLabel.Text = task.IsCanceled ? "Validation cancelled. Run it again to review this nest."
: $"Validation failed. Run it again to review this nest. {task.Exception?.GetBaseException().Message}";
if (task.IsCanceled)
SetStatus(Tone.Neutral, "Validation cancelled.", "Recheck to review this nest.");
else
SetStatus(Tone.Bad, "Validation failed. Recheck to review this nest.",
task.Exception?.GetBaseException().Message);
return;
}
ValidationReport = task.Result;
resultLabel.Text = ValidationReport.ToDisplayText("Nest validation (live programs)");
previewLabel.Text = ValidationReport.Findings.Any(f => f.Kind == PostVerificationKind.Incomplete)
? "Validation incomplete. Review the findings below."
: ValidationReport.HasWarnings ? "Warnings found. Review the findings below."
: "Validation complete. No warnings found by these checks.";
ShowValidationStatus();
populatingWarnings = true;
try
{
foreach (var finding in ValidationReport.Findings)
{
var index = warningGrid.Rows.Add(finding.PlateNumber, finding.PartNumber, finding.OtherPartNumber,
$"{finding.Kind}: {finding.Message}");
$"{KindName(finding.Kind)}: {finding.Message}");
warningGrid.Rows[index].Tag = finding;
}
warningGrid.ClearSelection();
@@ -409,12 +447,40 @@ internal sealed class CuttingInspectionPanel : UserControl
catch (Exception ex)
{
view.ClearValidationFinding();
resultLabel.Text = $"Validation failed. Run it again to review this nest. {ex.Message}";
SetStatus(Tone.Bad, "Validation failed. Recheck to review this nest.", ex.Message);
}
finally { completion.TrySetResult(); }
}, null), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
}
private void ShowValidationStatus()
{
var findings = ValidationReport.Findings;
if (findings.Any(f => f.Kind == PostVerificationKind.Incomplete))
SetStatus(Tone.Bad, "Validation incomplete: some checks could not run.", SelectIssueHint);
else if (findings.Count == 0)
SetStatus(Tone.Good, "No warnings found.");
else
{
var counts = findings.GroupBy(f => f.Kind).OrderBy(g => g.Key)
.Select(g => Count(g.Count(), KindName(g.Key).ToLowerInvariant()));
SetStatus(Tone.Warning, $"{Count(findings.Count, "warning")}: {string.Join(", ", counts)}.", SelectIssueHint);
}
}
private const string SelectIssueHint = "Select an issue below to see it on the plate.";
private static string KindName(PostVerificationKind kind) => kind switch
{
PostVerificationKind.Overlap => "Overlap",
PostVerificationKind.MissingLeadIn => "Missing lead-in",
PostVerificationKind.RapidCrossing => "Rapid crossing",
PostVerificationKind.Incomplete => "Not checked",
_ => kind.ToString(),
};
private static string Count(int count, string noun) => $"{count} {noun}{(count == 1 ? string.Empty : "s")}";
private void NavigateValidationFinding()
{
if (closed || closeRequested || pendingMode != null || populatingWarnings || IsPlanning || editor.IsDisposed)
@@ -427,41 +493,53 @@ internal sealed class CuttingInspectionPanel : UserControl
var location = finding.Location;
if (location == null && finding.PartNumber is { } part && part > 0 && part <= view.Plate.Parts.Count)
location = view.Plate.Parts[part - 1]?.BoundingBox?.Center;
if (location is { } point && double.IsFinite(point.X) && double.IsFinite(point.Y))
if (!ShowMarker(finding with { Location = location }))
{
var extent = finding.Overlap?.Bounds;
var size = System.Math.Max(10, System.Math.Max(extent?.Length ?? 0, extent?.Width ?? 0) * 2);
if (finding.RapidStart is { } start && finding.RapidEnd is { } end)
size = System.Math.Max(size, start.DistanceTo(end) * 1.5);
view.ZoomToArea(point.X - size / 2, point.Y - size / 2, size, size);
// A narrow docked canvas can put a contact at its edge after zoom.
// Centre a typed witness, never infer one from the diagnostic text.
var focus = finding.Kind == PostVerificationKind.RapidCrossing
&& finding.ContactPoints.Any(contact => double.IsFinite(contact.X) && double.IsFinite(contact.Y))
? finding.ContactPoints.First(contact => double.IsFinite(contact.X) && double.IsFinite(contact.Y))
: point;
view.CenterValidationPoint(focus, new Point(view.Width / 2, view.Height / 2));
view.ZoomToFit();
view.ShowValidationFinding(finding with { Location = location });
}
else view.ZoomToFit();
view.ShowValidationFinding(finding with { Location = location });
var hint = finding.Kind switch
{
PostVerificationKind.Overlap when finding.ClampId != null =>
"The ring marks a nominal cut-off contacting a clamp; no material-overlap area is implied.",
PostVerificationKind.Overlap => "Shaded material is the overlap. The ring marks a point inside its largest patch.",
PostVerificationKind.RapidCrossing when finding.ClampId != null =>
"The crosshair marks the rapid's contact with the clamp. Head height is not modeled.",
PostVerificationKind.RapidCrossing => finding.ContactPoints.Count > 0
? "Crosshairs mark contacts with the completed contour. The pulsing dashed orange line shows the rapid."
: "The pulsing dashed orange line shows the rapid inside the completed contour; there is no boundary contact point to mark.",
PostVerificationKind.MissingLeadIn => "The ring marks the cutting contour start where a lead-in is missing.",
_ => "The ring marks the affected part's center when available; no exact problem location is known."
};
// Keep the complete report visible; selecting a finding does not hide completeness or other warnings.
resultLabel.Text = $"Plate {finding.PlateNumber}, part {finding.PartNumber}: {finding.Message}\r\n{hint}\r\n\r\n"
+ ValidationReport.ToDisplayText("Nest validation (live programs)");
// The headline keeps completeness and the warning count; the grid keeps every finding.
SetDetail(MarkerHint(finding));
}
/// <summary>
/// Zooms to a finding's world-coordinate witness and marks it with the validation overlay.
/// False, with nothing shown, when the finding has no usable location.
/// </summary>
private bool ShowMarker(PostVerificationFinding finding)
{
var location = finding.Location ?? finding.Overlap?.Marker;
if (location is not { } point || !double.IsFinite(point.X) || !double.IsFinite(point.Y))
return false;
var extent = finding.Overlap?.Bounds;
var size = System.Math.Max(10, System.Math.Max(extent?.Length ?? 0, extent?.Width ?? 0) * 2);
if (finding.RapidStart is { } start && finding.RapidEnd is { } end)
size = System.Math.Max(size, start.DistanceTo(end) * 1.5);
view.ZoomToArea(point.X - size / 2, point.Y - size / 2, size, size);
// A narrow docked canvas can put a contact at its edge after zoom.
// Centre a typed witness, never infer one from the diagnostic text.
var focus = finding.Kind == PostVerificationKind.RapidCrossing
&& finding.ContactPoints.Any(contact => double.IsFinite(contact.X) && double.IsFinite(contact.Y))
? finding.ContactPoints.First(contact => double.IsFinite(contact.X) && double.IsFinite(contact.Y))
: point;
view.CenterValidationPoint(focus, new Point(view.Width / 2, view.Height / 2));
view.ShowValidationFinding(finding with { Location = point });
return true;
}
private static string MarkerHint(PostVerificationFinding finding) => finding.Kind switch
{
PostVerificationKind.Overlap when finding.ClampId != null => "The ring marks where a cut-off touches a clamp.",
PostVerificationKind.Overlap => "Red shading is the overlapping material.",
PostVerificationKind.RapidCrossing when finding.ClampId != null =>
"The crosshair marks where the rapid hits the clamp. Head height is not modeled.",
PostVerificationKind.RapidCrossing => finding.ContactPoints.Count > 0
? "Orange dashes show the rapid; crosshairs mark where it crosses a cut contour."
: "Orange dashes show the rapid running inside a cut contour.",
PostVerificationKind.MissingLeadIn => "The ring marks the contour start that has no lead-in.",
_ => "The ring marks the affected part; the exact spot is not known."
};
internal void StartPlanning()
{
if (Mode != CuttingInspectionMode.Plan || closed || IsPlanning || closeRequested || inspection.CloseRequested)
@@ -474,9 +552,7 @@ internal sealed class CuttingInspectionPanel : UserControl
}
RefreshExistingWork();
SetPlanning(true);
resultLabel.Text = stagedCutOffs != null
? "Planning… Automatic cut-offs were added for this plan; they are removed again unless you Apply."
: "Planning… Read-only; the live plate is unchanged.";
SetStatus(Tone.Neutral, "Planning…");
try
{
// Capture on the owner thread; no worker recapture of the live plate or settings.
@@ -487,7 +563,8 @@ internal sealed class CuttingInspectionPanel : UserControl
var progress = new PostingProgress(context, value =>
{
if (!closed && !inspection.CloseRequested && inspection.IsRunning && generation == run)
resultLabel.Text = $"Planning plate {value.PlateNumber}: {value.Phase}…";
SetStatus(Tone.Neutral, (plates.Length > 1 ? $"Planning plate {value.PlateNumber}: " : "Planning: ")
+ PhaseText(value.Phase) + "…");
});
var work = inspection.BeginPlanning(batch, BeforePlan, progress);
work.ContinueWith(task => context.Post(_ => FinishPlanning(task, run, completion), null),
@@ -495,11 +572,19 @@ internal sealed class CuttingInspectionPanel : UserControl
}
catch (Exception ex)
{
resultLabel.Text = $"Unable to start planning: {ex.Message}. No current preview.";
SetStatus(Tone.Bad, "Couldn't start planning.", ex.Message);
SetPlanning(false);
}
}
private static string PhaseText(CuttingPlanPhase phase) => phase switch
{
CuttingPlanPhase.Reordering => "finding a part order",
CuttingPlanPhase.KeepingOrder => "keeping the current order",
CuttingPlanPhase.CheckingOverlap => "checking for overlaps",
_ => phase.ToString(),
};
private void FinishPlanning(Task<CuttingPlanProposal> work, int run, TaskCompletionSource completion)
{
try
@@ -515,18 +600,15 @@ internal sealed class CuttingInspectionPanel : UserControl
SetPlanning(false);
if (work.IsFaulted)
{
resultLabel.Text = $"Planning failed: {work.Exception.GetBaseException().Message}. No current preview.";
SetStatus(Tone.Bad, "Planning failed. Replan to try again.", work.Exception.GetBaseException().Message);
return;
}
if (work.IsCanceled)
{
resultLabel.Text = "Cancelled. No current preview; nothing changed.";
SetStatus(Tone.Neutral, "Planning cancelled. Nothing changed.");
return;
}
invalidated = false;
resultLabel.Text = string.Join(Environment.NewLine, work.Result.Describe(UnitsHelper.GetShortString(editor.Nest.Units))
.Where(line => !line.TrimStart().StartsWith("- ", StringComparison.Ordinal)
&& !line.TrimStart().StartsWith("... and ", StringComparison.Ordinal)));
populatingWarnings = true;
try
{
@@ -538,6 +620,7 @@ internal sealed class CuttingInspectionPanel : UserControl
warningGrid.ClearSelection();
}
finally { populatingWarnings = false; }
ShowPlanStatus();
acceptWarningsCheckBox.Visible = Proposal.RequiresWarningAcceptance;
UpdateApply();
BeforePresent?.Invoke();
@@ -551,8 +634,8 @@ internal sealed class CuttingInspectionPanel : UserControl
acceptWarningsCheckBox.Checked = false;
acceptWarningsCheckBox.Visible = false;
applyButton.Enabled = false;
previewLabel.Text = "No current preview. Replan to continue.";
resultLabel.Text = $"Unable to show the plan: {ex.Message}. Nothing can be applied until replanning.";
previewLabel.Text = "No current preview.";
SetStatus(Tone.Bad, "Couldn't show the plan. Nothing can be applied until you replan.", ex.Message);
SetPlanning(false);
}
finally
@@ -561,6 +644,75 @@ internal sealed class CuttingInspectionPanel : UserControl
}
}
/// <summary>One headline for the whole proposal; per-plate state goes on the plate line.</summary>
private void ShowPlanStatus()
{
var issues = warningGrid.Rows.Count;
if (Proposal.IsCancelled)
SetStatus(Tone.Neutral, "Planning cancelled. Nothing changed.");
else if (Proposal.CanApply)
SetStatus(Tone.Good, plates.Length > 1 ? $"Ready to apply to {Count(plates.Length, "plate")}." : "Ready to apply.");
else if (Proposal.RequiresWarningAcceptance)
SetStatus(Tone.Warning, $"Unverified plan: review {Count(issues, "issue")}, then accept it to apply.",
issues > 0 ? SelectIssueHint : null);
else
{
var blocked = Proposal.Plates.Count(p => !p.CanApplyWithWarnings);
SetStatus(Tone.Bad, plates.Length > 1
? $"Can't apply: {blocked} of {Count(plates.Length, "plate")} blocked."
: "Can't apply: the plate is blocked.", issues > 0 ? SelectIssueHint : null);
}
}
/// <summary>What the operator needs to know about one planned plate, in one line.</summary>
private string DescribePlate(int index)
{
var planned = Proposal.Plates[index];
var name = plates.Length > 1 ? $"Plate {plateNumbers[index]} ({index + 1} of {plates.Length})" : $"Plate {plateNumbers[index]}";
if (planned.IsReady)
{
var order = planned.KeptCurrentOrder ? "current order kept (no better order found)"
: planned.OrderChanged ? "new part order" : "part order unchanged";
var rapid = planned.Result.RapidDistance.ToString("0.#", CultureInfo.CurrentCulture);
return $"{name}: {Count(planned.PartCount, "part")}, {order}, {rapid} {UnitsHelper.GetShortString(units)} of rapids.";
}
if (planned.CanApplyWithWarnings)
return $"{name}: UNVERIFIED. Check its lead-ins and cut order before applying.";
return $"{name}: blocked — {BlockReason(planned)}.";
}
private static string BlockReason(CuttingPlanPlateResult plate)
{
if (plate.Overlap is { } overlap && overlap.Pairs.Count > 0)
return "parts overlap";
if (plate.IsRouteReady)
return "the overlap check could not finish";
if (plate.Result.Findings.Any(f => f.Kind == PostVerificationKind.MissingLeadIn))
return "missing or zero-length lead-in";
return plate.Result.Status switch
{
CuttingPlanStatus.Ready => "the plan could not be verified",
CuttingPlanStatus.ConstraintConflict => "the required cut order conflicts",
CuttingPlanStatus.UnsupportedGeometry => "unsupported geometry",
CuttingPlanStatus.InvalidInput => "invalid input",
CuttingPlanStatus.NoSolutionWithinBudget => "no plan found within the search limit",
CuttingPlanStatus.Cancelled => "cancelled",
var status => status.ToString(),
};
}
/// <summary>The fix for a blocked plate's main problem, or null when there is no simple one.</summary>
private static string Remedy(CuttingPlanPlateResult plate)
{
if (plate.CanApplyWithWarnings)
return null;
if (plate.Overlap is { } overlap && overlap.Pairs.Count > 0)
return "Move the overlapping parts apart, then Replan.";
if (!plate.IsRouteReady && plate.Result.Findings.Any(f => f.Kind == PostVerificationKind.MissingLeadIn))
return "In Cutting Settings, choose a lead-in with a nonzero length (or unlock the part), then Replan.";
return null;
}
private void ClearResult()
{
ValidationReport = null;
@@ -572,6 +724,7 @@ internal sealed class CuttingInspectionPanel : UserControl
acceptWarningsCheckBox.Visible = false;
applyButton.Enabled = false;
previewLabel.Text = "No current preview.";
SetStatus(Tone.Neutral, string.Empty);
populatingWarnings = true;
try { warningGrid.Rows.Clear(); }
finally { populatingWarnings = false; }
@@ -601,7 +754,8 @@ internal sealed class CuttingInspectionPanel : UserControl
preview = null;
acceptWarningsCheckBox.Checked = false;
applyButton.Enabled = false;
resultLabel.Text = previewLabel.Text = "No current preview: the plate changed. Replan to inspect it. Nothing applied.";
previewLabel.Text = "No current preview.";
SetStatus(Tone.Bad, "The plate changed. Replan to continue; nothing was applied.");
return false;
}
@@ -618,7 +772,7 @@ internal sealed class CuttingInspectionPanel : UserControl
if (sourceIndex < 0)
{
ClearResult();
resultLabel.Text = "The selected plate changed after capture. Replan to continue.";
SetStatus(Tone.Bad, "The selected plate changed after planning. Replan to continue.");
return;
}
if (!ReferenceEquals(view.Plate, plates[index]))
@@ -626,7 +780,7 @@ internal sealed class CuttingInspectionPanel : UserControl
if (!ReferenceEquals(view.Plate, plates[index]))
{
ClearResult();
resultLabel.Text = "The selected plate is no longer current. Replan to continue.";
SetStatus(Tone.Bad, "The selected plate is no longer current. Replan to continue.");
return;
}
if (plates[index].Parts.Count == 0)
@@ -638,13 +792,15 @@ internal sealed class CuttingInspectionPanel : UserControl
}
preview = inspection.RefreshPreview(index); // Only the guarded authority may clone programs.
SetPlanning(false);
previewLabel.Text = DescribePlate(index);
SetDetail(Remedy(Proposal.Plates[index]) ?? (warningGrid.Rows.Count > 0 ? SelectIssueHint : null));
if (preview == null)
{
previewLabel.Text = $"Plate {plateNumbers[index]}: no current preview — stale, blocked or unsupported. Live plate shown; source numbers are not canvas locations.";
// Blocked or stale: the live plate stays on screen, numbered in its current order.
if (Proposal.Plates[index].CanApplyWithWarnings)
previewLabel.Text += " No current preview: the plate changed since planning.";
return;
}
previewLabel.Text = (Proposal.Plates[index].IsReady ? "PROPOSAL" : "UNVERIFIED PROPOSAL")
+ $" — Plate {plateNumbers[index]} ({index + 1} of {plates.Length}), numbered in proposed cutting order; read-only.";
view.ShowProposalDisplay(preview, IsPreviewCurrent);
view.ZoomToFit();
}
@@ -660,6 +816,12 @@ internal sealed class CuttingInspectionPanel : UserControl
|| warningGrid.SelectedRows[0].Tag is not CuttingPlanDiagnosticRow row)
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))
{
SetDetail(MarkerHint(witness));
return;
}
if (preview == null || row.PartNumber is not int source)
return;
var position = CuttingProposalInspection.ProposedPosition(Proposal, row.PlateIndex, source);
@@ -682,7 +844,7 @@ internal sealed class CuttingInspectionPanel : UserControl
var pad = System.Math.Max(0.5, System.Math.Max(bounds.Length, bounds.Width) * 0.12);
view.ZoomToArea(bounds.X - pad, bounds.Y - pad, bounds.Length + 2 * pad, bounds.Width + 2 * pad);
}
previewLabel.Text += " Showing affected part extent, not an exact warning point.";
SetDetail("Zoomed to the affected part; this issue has no exact location.");
}
internal CuttingCommitResult CommitResult { get; private set; }
@@ -723,7 +885,7 @@ internal sealed class CuttingInspectionPanel : UserControl
return;
}
ClearResult();
resultLabel.Text = $"Nothing applied: {CommitResult.Message ?? CommitResult.Status.ToString()}. Replan to continue.";
SetStatus(Tone.Bad, "Nothing applied. Replan to continue.", CommitResult.Message ?? CommitResult.Status.ToString());
}
private void EditSettings()
@@ -739,7 +901,8 @@ internal sealed class CuttingInspectionPanel : UserControl
private void ShowSettings()
{
settingsLabel.Text = $"Lead-ins: outside {Describe(parameters.ExternalLeadIn)}, holes {Describe(parameters.InternalLeadIn)}, arcs/circles {Describe(parameters.ArcCircleLeadIn)}. Tabs {(parameters.TabsEnabled ? "on" : "off")}.";
settingsLabel.Text = $"Outside: {Describe(parameters.ExternalLeadIn)} · Holes: {Describe(parameters.InternalLeadIn)} · "
+ $"Arcs: {Describe(parameters.ArcCircleLeadIn)} · Tabs: {(parameters.TabsEnabled ? "on" : "off")}";
}
private static string Describe(LeadIn leadIn)
@@ -770,9 +933,9 @@ internal sealed class CuttingInspectionPanel : UserControl
{
var reasons = staging.Plans.SelectMany((plan, index) => plan.Diagnostics.Where(d => d.IsBlocking)
.Select(d => $"Plate {plateNumbers[index]}: {d.Message}"));
resultLabel.Text = "Automatic cut-offs were not added, so nothing was planned:" + Environment.NewLine
+ string.Join(Environment.NewLine, reasons) + Environment.NewLine
+ "Untick \"Add automatic scrap cut-offs\" to plan without them, or clear or fix the existing cut-offs.";
SetStatus(Tone.Bad, "Scrap cut-offs couldn't be added, so nothing was planned.",
string.Join(Environment.NewLine, reasons) + Environment.NewLine
+ "Untick scrap cut-offs to plan without them, or clear the existing cut-offs.");
return false;
}
stagedCutOffs = staging.CanRevert ? staging : null;
@@ -781,7 +944,7 @@ internal sealed class CuttingInspectionPanel : UserControl
}
catch (Exception ex)
{
resultLabel.Text = $"Automatic cut-offs could not be added: {ex.Message} Nothing was planned.";
SetStatus(Tone.Bad, "Scrap cut-offs couldn't be added, so nothing was planned.", ex.Message);
return false;
}
}
@@ -829,7 +992,7 @@ internal sealed class CuttingInspectionPanel : UserControl
catch (Exception ex)
{
RefreshExistingWork();
resultLabel.Text = $"Clearing stopped part-way: {ex.Message} Review the plates before planning or posting.";
SetStatus(Tone.Bad, "Clearing stopped part-way. Review the plates before planning or posting.", ex.Message);
return;
}
finally
@@ -854,7 +1017,7 @@ internal sealed class CuttingInspectionPanel : UserControl
ClearResult();
SetPlanning(true);
cancelButton.Enabled = false;
resultLabel.Text = "Cancelling… waiting for the worker. Nothing has changed.";
SetStatus(Tone.Neutral, "Closing… nothing has changed.");
validationCancellation?.Cancel();
inspection.RequestClose();
if (!IsPlanning)
@@ -928,6 +1091,8 @@ internal sealed class CuttingInspectionPanel : UserControl
if (!closed)
return; // The worker still owns the inspection lock and controls.
}
if (disposing)
toolTip.Dispose();
base.Dispose(disposing);
}
+24 -20
View File
@@ -397,8 +397,9 @@ on an `Applied` result, not as a rollback.
It captures scope on the editor thread, plans on a worker, and shows only a fresh,
supported detached proposal in that canvas, including proposed part numbers and
rapids. The live `Plate`, part order, programs and quantities remain unchanged
until Apply. The summary and complete structured findings remain available while
navigating plates; selecting a finding frames its affected part extent, not an
until Apply. The outcome and complete structured findings remain available while
navigating plates; selecting a finding marks its recorded witness (overlap area, rapid,
contacts) like Validate Nest, or frames the affected part extent when it has none, never an
invented contact point. Unsupported or stale output has no proposal preview.
The panel shows the current cutting settings; editing them or switching Keep current
@@ -407,7 +408,7 @@ the exact captured proposal, with an unchecked per-proposal acceptance required
for UNVERIFIED results; a blocked plate disables the whole-batch Apply. A successful
Apply persists the confirmed settings and refreshes the editor; a failed/stale Apply
changes nothing and requires replanning. Escape, the panel X or Cancel restores the
live view and prior viewport; closing during planning waits for the worker before
live view and prior viewport; Close during planning waits for the worker before
unlocking inspection. Planning is not live-nest validation or posting consent. The
panel's **Validate Nest (live programs)** mode separately checks current installed
programs across all plates and navigates typed findings on the live canvas; it
@@ -426,11 +427,11 @@ panel is wired in the editor. Non-editor callers still use the shared analyzer.
Cut-offs...` asks first, then removes every applied lead-in (locked and hand-placed
ones too), every cut-off and the plates' recorded cutting settings, keeping part
poses, order and quantities, and replans from scratch. There is no undo.
- `Add automatic scrap cut-offs` runs the automatic cut-off planner (defaults 35 in /
- `Add scrap cut-offs` runs the automatic cut-off planner (defaults 35 in /
889 mm strips, half-strip minimum, 12 in / 304.8 mm tail) on every plate in scope
*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, Cancel/Close or switching to Validate removes them and
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
plans nothing; the panel names the plate and reason.
`Cut-off Settings...` opens the Automatic Scrap Cut-offs dialog in settings mode for
@@ -466,19 +467,22 @@ panel is wired in the editor. Non-editor callers still use the shared analyzer.
`NoSolutionWithinBudget` is retried once with the current part order, and the summary
says the order was kept. Both are allowed 400 expansions per part (at least the
default 20000), because both still plan contour order and entries for every part.
- The docked panel keeps the batch outcome and plate headings above a
read-only, unsorted warning grid with Plate, source Part, With, and full finding text. Every
overlap issue/pair and every planner finding has its own selectable row, including findings
hidden by the compact blocked-plate summary. **Previous**/**Next** and warning selection
navigate the editor to that plate's live source sheet, then display its detached proposal
when available. Warning selection zooms to the affected part extent (or both parts for a
pair), mapping source positions to proposed cutting order. This is not an exact contact
marker: most findings do not carry a geometric witness. If a plate cannot produce a
current, supported proposal preview, its own live sheet remains visible; no unsupported
geometry is cloned or focused. Replanning, cancellation and stale Apply clear old warning
targets.
- The summary lists every plate: ready plates with part counts and rapid travel, others
with their status and findings. Unverified proposals retain every overlap and route
- The docked panel keeps text short: one colored outcome line for the batch, one line for the
shown plate, and one detail line (a remedy for a blocked plate, or what the selected marker
shows). Below them is a read-only, unsorted issue grid with Plate, source Part, With, and full
finding text. Every overlap issue/pair and every planner finding has its own selectable row.
**Previous**/**Next** (shown for more than one plate) and issue selection navigate the editor
to that plate's live source sheet, then display its detached proposal when available.
Overlap pairs and checker findings carry their world-coordinate witness
(`CuttingPlanDiagnosticRow.Witness`, from `CuttingPlanFinding.Witness`), so selection shades
the overlap or draws the rapid and its contacts with the Validate Nest overlay, on the
proposal or on a blocked plate's live sheet. Findings without a witness zoom to the affected
part extent (or both parts for a pair), mapping source positions to proposed cutting order.
If a plate cannot produce a current, supported proposal preview, its own live sheet remains
visible; no unsupported geometry is cloned. Replanning, cancellation and stale Apply clear
old warning targets.
- The plate line gives ready plates' part counts, order change and rapid travel, and other
plates' blocking reason. Unverified proposals retain every overlap and route
warning in the selectable grid before the acceptance checkbox is used; they do
not truncate later parts' warnings. Finding part numbers are the plate's current order, as the
editor numbers them. The panel shows the selected plate detached from the nest (quantity
@@ -486,14 +490,14 @@ panel is wired in the editor. Non-editor callers still use the shared analyzer.
programs. Ready and best-effort proposals can be previewed only while the plate still
matches capture. Best-effort previews are labelled `UNVERIFIED`. Refused program graphs
are never copied, and changed plates require replanning.
- Apply requires usable output for every plate and remains all or nothing. The summary
- Apply requires usable output for every plate and remains all or nothing. The outcome line
counts best-effort proposals as usable when reporting how many plates block Apply;
their warnings still require acceptance. Open nominal contour findings identify the
contour number, endpoint gap in model units (the nest's linear units), and closure
tolerance, so the source can be reviewed in the drawing editor. Reporting never
closes gaps or changes the validation tolerance. A fully ready batch
can apply immediately; an unverified batch requires the unchecked, per-proposal
`Accept UNVERIFIED plan warnings (post consent happens separately)` checkbox. Replanning clears it.
`Accept the unverified plan` checkbox (posting consent is still separate). Replanning clears it.
Both use the same owned-program, freshness and rollback boundary. After it applies, each plate keeps its own copy of the
confirmed settings, which also become the saved defaults. `Stale` keeps the panel open
and asks for a replan; nothing changes.
+4 -2
View File
@@ -9,8 +9,10 @@ in **Validate Nest (live programs)** mode, distinct from the panel's unapplied
missing lead-ins, and rapid crossings in the existing placed programs and cutting
order. Findings identify plate and part sequence numbers in a read-only grid;
incomplete or failed checks are never reported as clear. Select a row to switch
the main plate view to that plate and focus the finding. The scrollable report
retains every result and the selected finding's explanation.
the main plate view to that plate and focus the finding. A colored headline gives
the warning count by kind (or says the check is incomplete or clear), a single line
explains the selected marker, and the grid retains every result. **Plan Cutting**
marks its overlap and route findings with the same overlay.
While a finding is selected, its named parts retain their normal colors; other
parts use muted gray fills and visible gray outlines, lead-ins, and marks. Selecting
another finding updates the emphasis. Clearing the finding or closing validation