mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 15:01:07 -04:00
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:
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);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user