refactor(ui): make the automatic cutoff dialog scannable at a glance

The dialog buried its verdict in a scrollable text box that restated the
same numbers per plate behind a paragraph-length warning. Replace it
with:

- a bold colored status line stating exactly what Apply will do
  (Ready / Blocked / No new cut-offs / Error with the reason);
- the occupied span, used span, separator X and retained tail as
  compact caption+value cells with tooltips instead of four sentences;
- the warning paragraph reduced to one amber line with the full text
  in its tooltip;
- a per-plate table (cut-offs, spans, tail, Ready/Blocked/No change)
  for the All Plates command, replacing repeated text blocks;
- the details box now lists only issues and notices, prefixed by plate
  number when planning a nest, instead of echoing every measurement.

Planning, preview, apply and rollback behavior is unchanged; tests and
the cutoffs document follow the new presentation.
This commit is contained in:
aj committed 2026-10-13 13:54:21 -04:00
1 parent 311178c15a
commit b66af8658b
4 files changed
+300 -108

No files matched your search

@@ -35,11 +35,17 @@ public class AutomaticCutOffFormTests
var originalParts = view.Plate.Parts.ToArray();
using (var form = new AutomaticCutOffForm(view, Units.Inches))
{
Assert.False(Control<ListView>(form, "plateGrid").Visible);
Invoke(form, "PreviewButton_Click", null, EventArgs.Empty);
Assert.NotEmpty(PreviewParts(view));
Assert.Empty(view.Plate.CutOffs);
Assert.Equal(originalParts, view.Plate.Parts.ToArray());
Assert.Contains("36 in width", Control<Label>(form, "tailLabel").Text);
var expected = AutomaticCutOffPlanner.Create(view.Plate,
new AutomaticCutOffOptions { Spacing = 35, MinimumTailLength = 12 }, view.CutOffSettings);
Assert.True(expected.HasSeparatedTail);
Assert.Equal($"{expected.TailLength:0.####} in", Control<Label>(form, "tailLabel").Text);
Assert.Contains("Ready", Control<Label>(form, "statusLabel").Text);
Assert.Equal("No issues detected.", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.True(Control<Button>(form, "applyButton").Enabled);
}
Assert.Empty(PreviewParts(view));
@@ -137,6 +143,7 @@ public class AutomaticCutOffFormTests
Assert.Empty(PreviewParts(view));
Assert.False(Control<Button>(form, "applyButton").Enabled);
Assert.Contains("BLOCKING", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.Contains("Blocked", Control<Label>(form, "statusLabel").Text);
});
}
@@ -152,7 +159,8 @@ public class AutomaticCutOffFormTests
Assert.NotEqual(DialogResult.OK, form.DialogResult);
Assert.Empty(view.Plate.CutOffs);
Assert.Empty(view.Plate.Parts);
Assert.Contains("No new usable", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.Contains("No new cut-offs", Control<Label>(form, "statusLabel").Text);
Assert.Equal("No issues detected.", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.False(Control<Button>(form, "applyButton").Enabled);
});
}
@@ -209,8 +217,18 @@ public class AutomaticCutOffFormTests
using var form = new AutomaticCutOffForm(view, nest);
Invoke(form, "PreviewButton_Click", null, EventArgs.Empty);
Assert.Contains("All 3 plates", Control<Label>(form, "sheetLabel").Text);
Assert.Contains("Plate 2", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.Contains("Plate 3", Control<TextBox>(form, "diagnosticsBox").Text);
var grid = Control<ListView>(form, "plateGrid");
Assert.Equal(3, grid.Items.Count);
Assert.Equal("1 (active)", grid.Items[0].Text);
Assert.Equal("2", grid.Items[1].Text);
Assert.Equal("3", grid.Items[2].Text);
Assert.Equal("Ready", grid.Items[0].SubItems[5].Text);
Assert.Equal("Ready", grid.Items[1].SubItems[5].Text);
Assert.Equal("No change", grid.Items[2].SubItems[5].Text);
Assert.Equal("—", grid.Items[2].SubItems[1].Text);
Assert.Contains("Ready", Control<Label>(form, "statusLabel").Text);
Assert.Contains("across 2 of 3 plates", Control<Label>(form, "statusLabel").Text);
Assert.DoesNotContain("BLOCKING", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.NotEmpty(PreviewParts(view));
Assert.All(nest.Plates, p => Assert.Empty(p.CutOffs));
@@ -247,6 +265,10 @@ public class AutomaticCutOffFormTests
Assert.False(Control<Button>(form, "applyButton").Enabled);
Assert.Contains("Plate 2", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.Contains("BLOCKING", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.Contains("Blocked", Control<Label>(form, "statusLabel").Text);
var grid = Control<ListView>(form, "plateGrid");
Assert.Equal("Ready", grid.Items[0].SubItems[5].Text);
Assert.Equal("Blocked", grid.Items[1].SubItems[5].Text);
Assert.Empty(PreviewParts(view));
});
}
+178 -56
View File
@@ -19,6 +19,7 @@ partial class AutomaticCutOffForm
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
layout = new TableLayoutPanel();
sheetLabel = new Label();
spacingPanel = new TableLayoutPanel();
@@ -26,11 +27,25 @@ partial class AutomaticCutOffForm
spacingBox = new TextBox();
minimumTailLabel = new Label();
minimumTailBox = new TextBox();
warningLabel = new Label();
statusLabel = new Label();
metricsPanel = new TableLayoutPanel();
occupiedCaption = new Label();
usedCaption = new Label();
separatorCaption = new Label();
tailCaption = new Label();
occupiedLabel = new Label();
usedLabel = new Label();
separatorLabel = new Label();
tailLabel = new Label();
warningLabel = new Label();
toolTip = new ToolTip(components);
plateGrid = new ListView();
plateColumn = new ColumnHeader();
cutoffsColumn = new ColumnHeader();
occupiedColumn = new ColumnHeader();
usedColumn = new ColumnHeader();
tailColumn = new ColumnHeader();
statusColumn = new ColumnHeader();
diagnosticsLabel = new Label();
diagnosticsBox = new TextBox();
buttonsPanel = new FlowLayoutPanel();
@@ -39,6 +54,7 @@ partial class AutomaticCutOffForm
cancelButton = new Button();
layout.SuspendLayout();
spacingPanel.SuspendLayout();
metricsPanel.SuspendLayout();
buttonsPanel.SuspendLayout();
SuspendLayout();
//
@@ -48,24 +64,23 @@ partial class AutomaticCutOffForm
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layout.Controls.Add(sheetLabel, 0, 0);
layout.Controls.Add(spacingPanel, 0, 1);
layout.Controls.Add(warningLabel, 0, 2);
layout.Controls.Add(occupiedLabel, 0, 3);
layout.Controls.Add(usedLabel, 0, 4);
layout.Controls.Add(separatorLabel, 0, 5);
layout.Controls.Add(tailLabel, 0, 6);
layout.Controls.Add(diagnosticsLabel, 0, 7);
layout.Controls.Add(diagnosticsBox, 0, 8);
layout.Controls.Add(buttonsPanel, 0, 9);
layout.Controls.Add(statusLabel, 0, 2);
layout.Controls.Add(metricsPanel, 0, 3);
layout.Controls.Add(warningLabel, 0, 4);
layout.Controls.Add(plateGrid, 0, 5);
layout.Controls.Add(diagnosticsLabel, 0, 6);
layout.Controls.Add(diagnosticsBox, 0, 7);
layout.Controls.Add(buttonsPanel, 0, 8);
layout.Dock = DockStyle.Fill;
layout.Name = "layout";
layout.RowCount = 10;
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowCount = 9;
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
plateGridRow = new RowStyle(SizeType.Absolute, 112F);
layout.RowStyles.Add(plateGridRow);
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
@@ -128,59 +143,149 @@ partial class AutomaticCutOffForm
minimumTailBox.TabIndex = 3;
minimumTailBox.TextChanged += SettingsBox_TextChanged;
//
// statusLabel
//
statusLabel.AutoSize = true;
statusLabel.Dock = DockStyle.Fill;
statusLabel.Font = new Font("Segoe UI", 11.25F, FontStyle.Bold, GraphicsUnit.Point);
statusLabel.Margin = new Padding(3, 0, 3, 8);
statusLabel.Name = "statusLabel";
statusLabel.TabIndex = 2;
statusLabel.Text = "Preparing preview…";
//
// metricsPanel
//
metricsPanel.AutoSize = true;
metricsPanel.ColumnCount = 4;
metricsPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25F));
metricsPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25F));
metricsPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25F));
metricsPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25F));
metricsPanel.Controls.Add(occupiedCaption, 0, 0);
metricsPanel.Controls.Add(usedCaption, 1, 0);
metricsPanel.Controls.Add(separatorCaption, 2, 0);
metricsPanel.Controls.Add(tailCaption, 3, 0);
metricsPanel.Controls.Add(occupiedLabel, 0, 1);
metricsPanel.Controls.Add(usedLabel, 1, 1);
metricsPanel.Controls.Add(separatorLabel, 2, 1);
metricsPanel.Controls.Add(tailLabel, 3, 1);
metricsPanel.Dock = DockStyle.Fill;
metricsPanel.Margin = new Padding(0, 0, 0, 6);
metricsPanel.Name = "metricsPanel";
metricsPanel.RowCount = 2;
metricsPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
metricsPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
metricsPanel.TabIndex = 3;
//
// metric captions
//
occupiedCaption.AutoSize = true;
occupiedCaption.Dock = DockStyle.Fill;
occupiedCaption.ForeColor = SystemColors.GrayText;
occupiedCaption.Margin = new Padding(3, 0, 6, 0);
occupiedCaption.Name = "occupiedCaption";
occupiedCaption.Text = "Occupied span";
usedCaption.AutoSize = true;
usedCaption.Dock = DockStyle.Fill;
usedCaption.ForeColor = SystemColors.GrayText;
usedCaption.Margin = new Padding(3, 0, 6, 0);
usedCaption.Name = "usedCaption";
usedCaption.Text = "Used span";
separatorCaption.AutoSize = true;
separatorCaption.Dock = DockStyle.Fill;
separatorCaption.ForeColor = SystemColors.GrayText;
separatorCaption.Margin = new Padding(3, 0, 6, 0);
separatorCaption.Name = "separatorCaption";
separatorCaption.Text = "Separator X";
tailCaption.AutoSize = true;
tailCaption.Dock = DockStyle.Fill;
tailCaption.ForeColor = SystemColors.GrayText;
tailCaption.Margin = new Padding(3, 0, 3, 0);
tailCaption.Name = "tailCaption";
tailCaption.Text = "Retained tail";
//
// metric values
//
var valueFont = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point);
occupiedLabel.AutoSize = true;
occupiedLabel.Dock = DockStyle.Fill;
occupiedLabel.Font = valueFont;
occupiedLabel.Margin = new Padding(3, 0, 6, 0);
occupiedLabel.Name = "occupiedLabel";
occupiedLabel.Text = "—";
usedLabel.AutoSize = true;
usedLabel.Dock = DockStyle.Fill;
usedLabel.Font = valueFont;
usedLabel.Margin = new Padding(3, 0, 6, 0);
usedLabel.Name = "usedLabel";
usedLabel.Text = "—";
separatorLabel.AutoSize = true;
separatorLabel.Dock = DockStyle.Fill;
separatorLabel.Font = valueFont;
separatorLabel.Margin = new Padding(3, 0, 6, 0);
separatorLabel.Name = "separatorLabel";
separatorLabel.Text = "—";
tailLabel.AutoSize = true;
tailLabel.Dock = DockStyle.Fill;
tailLabel.Font = valueFont;
tailLabel.Margin = new Padding(3, 0, 3, 0);
tailLabel.Name = "tailLabel";
tailLabel.Text = "—";
//
// warningLabel
//
warningLabel.AutoSize = true;
warningLabel.Dock = DockStyle.Fill;
warningLabel.Margin = new Padding(3, 3, 3, 10);
warningLabel.ForeColor = WarningColor;
warningLabel.Margin = new Padding(3, 0, 3, 8);
warningLabel.Name = "warningLabel";
warningLabel.TabIndex = 2;
warningLabel.Text = "Nominal spacing differs from cut-line length. Clearances and omitted segments can leave bridges. "
+ "Confirm disconnected scrap and hopper fit before relying on it. Tail dimensions describe a proposed program; verify the physical separation.";
warningLabel.TabIndex = 4;
warningLabel.Text = "⚠ Nominal spacing is not the cut-line length — verify scrap separation and hopper fit.";
toolTip.SetToolTip(warningLabel,
"Nominal spacing differs from cut-line length. Clearances and omitted segments can leave bridges. "
+ "Confirm disconnected scrap and hopper fit before relying on it. Tail dimensions describe a proposed "
+ "program; verify the physical separation.");
toolTip.SetToolTip(occupiedCaption, "Distance from the sheet origin to the furthest real material.");
toolTip.SetToolTip(usedCaption, "Span through the proposed used area, from the sheet origin.");
toolTip.SetToolTip(separatorCaption,
"Signed X of the verified separator cut. A negative value means the separator is on the origin side.");
toolTip.SetToolTip(tailCaption,
"Length along X of the reusable tail beyond the verified separator, at full sheet width.");
//
// occupiedLabel
// plateGrid
//
occupiedLabel.AutoSize = true;
occupiedLabel.Dock = DockStyle.Fill;
occupiedLabel.Margin = new Padding(3, 3, 3, 5);
occupiedLabel.Name = "occupiedLabel";
occupiedLabel.TabIndex = 3;
occupiedLabel.Text = "Occupied span from origin: —";
//
// usedLabel
//
usedLabel.AutoSize = true;
usedLabel.Dock = DockStyle.Fill;
usedLabel.Margin = new Padding(3, 3, 3, 5);
usedLabel.Name = "usedLabel";
usedLabel.TabIndex = 4;
usedLabel.Text = "Proposed used span from origin: —";
//
// separatorLabel
//
separatorLabel.AutoSize = true;
separatorLabel.Dock = DockStyle.Fill;
separatorLabel.Margin = new Padding(3, 3, 3, 5);
separatorLabel.Name = "separatorLabel";
separatorLabel.TabIndex = 5;
separatorLabel.Text = "Verified separator program: —";
//
// tailLabel
//
tailLabel.AutoSize = true;
tailLabel.Dock = DockStyle.Fill;
tailLabel.Margin = new Padding(3, 3, 3, 8);
tailLabel.Name = "tailLabel";
tailLabel.TabIndex = 6;
tailLabel.Text = "Proposed retained tail: —";
plateGrid.Columns.AddRange(new ColumnHeader[] {
plateColumn, cutoffsColumn, occupiedColumn, usedColumn, tailColumn, statusColumn });
plateGrid.Dock = DockStyle.Fill;
plateGrid.FullRowSelect = true;
plateGrid.GridLines = true;
plateGrid.HeaderStyle = ColumnHeaderStyle.Nonclickable;
plateGrid.HideSelection = false;
plateGrid.MultiSelect = false;
plateGrid.Name = "plateGrid";
plateGrid.TabIndex = 5;
plateGrid.UseCompatibleStateImageBehavior = false;
plateGrid.View = View.Details;
plateColumn.Text = "Plate";
plateColumn.Width = 80;
cutoffsColumn.Text = "Cut-offs";
cutoffsColumn.Width = 65;
occupiedColumn.Text = "Occupied";
occupiedColumn.Width = 90;
usedColumn.Text = "Used";
usedColumn.Width = 90;
tailColumn.Text = "Tail";
tailColumn.Width = 90;
statusColumn.Text = "Status";
statusColumn.Width = 150;
//
// diagnosticsLabel
//
diagnosticsLabel.AutoSize = true;
diagnosticsLabel.Dock = DockStyle.Fill;
diagnosticsLabel.Name = "diagnosticsLabel";
diagnosticsLabel.TabIndex = 7;
diagnosticsLabel.Text = "Diagnostics / plan status:";
diagnosticsLabel.TabIndex = 6;
diagnosticsLabel.Text = "Details — issues and notices:";
//
// diagnosticsBox
//
@@ -189,7 +294,7 @@ partial class AutomaticCutOffForm
diagnosticsBox.Name = "diagnosticsBox";
diagnosticsBox.ReadOnly = true;
diagnosticsBox.ScrollBars = ScrollBars.Vertical;
diagnosticsBox.TabIndex = 8;
diagnosticsBox.TabIndex = 7;
//
// buttonsPanel
//
@@ -201,7 +306,7 @@ partial class AutomaticCutOffForm
buttonsPanel.FlowDirection = FlowDirection.RightToLeft;
buttonsPanel.Margin = new Padding(0, 8, 0, 0);
buttonsPanel.Name = "buttonsPanel";
buttonsPanel.TabIndex = 9;
buttonsPanel.TabIndex = 8;
buttonsPanel.WrapContents = false;
//
// previewButton
@@ -241,7 +346,7 @@ partial class AutomaticCutOffForm
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = cancelButton;
ClientSize = new Size(660, 520);
ClientSize = new Size(660, 560);
Controls.Add(layout);
Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
FormBorderStyle = FormBorderStyle.FixedDialog;
@@ -257,23 +362,40 @@ partial class AutomaticCutOffForm
layout.PerformLayout();
spacingPanel.ResumeLayout(false);
spacingPanel.PerformLayout();
metricsPanel.ResumeLayout(false);
metricsPanel.PerformLayout();
buttonsPanel.ResumeLayout(false);
buttonsPanel.PerformLayout();
ResumeLayout(false);
}
private TableLayoutPanel layout;
private RowStyle plateGridRow;
private Label sheetLabel;
private TableLayoutPanel spacingPanel;
private Label spacingLabel;
private TextBox spacingBox;
private Label minimumTailLabel;
private TextBox minimumTailBox;
private Label warningLabel;
private Label statusLabel;
private TableLayoutPanel metricsPanel;
private Label occupiedCaption;
private Label usedCaption;
private Label separatorCaption;
private Label tailCaption;
private Label occupiedLabel;
private Label usedLabel;
private Label separatorLabel;
private Label tailLabel;
private Label warningLabel;
private ToolTip toolTip;
private ListView plateGrid;
private ColumnHeader plateColumn;
private ColumnHeader cutoffsColumn;
private ColumnHeader occupiedColumn;
private ColumnHeader usedColumn;
private ColumnHeader tailColumn;
private ColumnHeader statusColumn;
private Label diagnosticsLabel;
private TextBox diagnosticsBox;
private FlowLayoutPanel buttonsPanel;
+94 -46
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Windows.Forms;
@@ -9,6 +10,11 @@ namespace OpenNest.Forms;
public partial class AutomaticCutOffForm : Form
{
private static readonly Color ReadyColor = Color.FromArgb(0, 102, 0);
private static readonly Color BlockedColor = Color.FromArgb(192, 0, 0);
private static readonly Color NeutralColor = SystemColors.ControlText;
internal static readonly Color WarningColor = Color.FromArgb(120, 70, 0);
private readonly PlateView plateView;
private readonly Plate plate;
private readonly Nest nest;
@@ -39,6 +45,12 @@ public partial class AutomaticCutOffForm : Form
Text = "Automatic Scrap Cutoffs — All Plates";
applyButton.Text = "&Apply to All Plates";
sheetLabel.Text = $"All {nest.Plates.Count} plates — active sheet shown below";
diagnosticsLabel.Text = "Details — issues and notices (the table above lists every plate):";
}
else
{
plateGrid.Visible = false;
plateGridRow.Height = 0F;
}
}
@@ -89,68 +101,102 @@ public partial class AutomaticCutOffForm : Form
return (plates, plans);
}
private IEnumerable<string> Diagnostics(AutomaticCutOffPlan plan) => plan.Diagnostics.Select(d =>
$"{(d.IsBlocking ? "BLOCKING" : "Notice")} [{d.Code}]"
+ (d.X.HasValue ? $" X = {SignedPosition(d.X.Value)}" : string.Empty)
+ $": {d.Message}");
private string DiagnosticLine(AutomaticCutOffPlan plan) => string.Join(Environment.NewLine,
plan.Diagnostics.Select(d =>
$"{(d.IsBlocking ? "BLOCKING" : "Notice")} [{d.Code}]"
+ (d.X.HasValue ? $" X = {SignedPosition(d.X.Value)}" : string.Empty)
+ $": {d.Message}"));
private static string PlanStatus(AutomaticCutOffPlan plan) => plan.HasBlockingDiagnostics
? "Blocked: resolve the diagnostics before applying."
: plan.Definitions.Count == 0
? "No new usable cut-offs to apply."
: $"{plan.Definitions.Count} new cut-off definition(s) ready to apply.";
private void SetStatus(string message, Color color)
{
statusLabel.Text = message;
statusLabel.ForeColor = color;
}
private void DisplayPlans(Plate[] plates, IReadOnlyList<AutomaticCutOffPlan> plans)
{
var currentIndex = Array.IndexOf(plates, plate);
var plan = plans[currentIndex];
occupiedLabel.Text = $"Occupied span from origin: {Measure(plan.OccupiedSpan)}";
usedLabel.Text = $"Proposed used span from origin: {Measure(plan.UsedSpan)}";
separatorLabel.Text = plan.HasSeparatedTail
? $"Verified separator program at signed X: {SignedPosition(plan.TailSeparatorX.Value)}"
: "Verified separator program: none";
tailLabel.Text = plan.HasSeparatedTail
? $"Proposed retained tail: {Measure(plan.TailLength)} length × {Measure(plate.Size.Width)} width"
: "Proposed retained tail: none verified";
occupiedLabel.Text = Measure(plan.OccupiedSpan);
usedLabel.Text = Measure(plan.UsedSpan);
separatorLabel.Text = plan.HasSeparatedTail ? SignedPosition(plan.TailSeparatorX.Value) : "none";
tailLabel.Text = plan.HasSeparatedTail ? Measure(plan.TailLength) : "none";
var blocked = plans.Any(p => p.HasBlockingDiagnostics);
var definitionCount = plans.Sum(p => p.Definitions.Count);
applyButton.Enabled = !blocked && definitionCount > 0;
if (nest == null)
var issues = new List<string>();
for (var index = 0; index < plans.Count; index++)
{
diagnosticsBox.Text = string.Join(Environment.NewLine + Environment.NewLine,
new[] { PlanStatus(plan) }.Concat(Diagnostics(plan)));
return;
var lines = DiagnosticLine(plans[index]);
if (lines.Length == 0)
continue;
issues.Add(nest == null ? lines : $"Plate {index + 1}: {lines}");
}
diagnosticsBox.Text = issues.Count == 0 ? "No issues detected." : string.Join(Environment.NewLine, issues);
if (blocked)
{
var issueCount = plans.Sum(p => p.Diagnostics.Count(d => d.IsBlocking));
SetStatus($"Blocked — {issueCount} blocking issue{(issueCount == 1 ? "" : "s")}; nothing will be applied.", BlockedColor);
}
else if (definitionCount == 0)
{
SetStatus("No new cut-offs to apply.", NeutralColor);
}
else if (nest == null)
{
SetStatus($"Ready — {definitionCount} new cut-off{(definitionCount == 1 ? "" : "s")} will be added.", ReadyColor);
}
else
{
var touched = plans.Count(p => p.Definitions.Count > 0);
SetStatus($"Ready — {definitionCount} cut-off{(definitionCount == 1 ? "" : "s")} across {touched} of {plans.Count} plates. Unchanged plates stay untouched.", ReadyColor);
}
sheetLabel.Text = $"All {plates.Length} plates — active plate {currentIndex + 1}: "
+ $"{Measure(plate.Size.Length)} length × {Measure(plate.Size.Width)} width";
var status = blocked
? "BLOCKED: no plates will be changed until every blocking issue is resolved."
: $"{definitionCount} new cut-off definition(s) across {plans.Count(p => p.Definitions.Count > 0)} plate(s). "
+ "Empty/unchanged plates will be left untouched.";
var summaries = plans.Select((p, index) =>
if (nest == null)
return;
sheetLabel.Text = $"All {plates.Length} plates — preview shows plate {currentIndex + 1} "
+ $"({Measure(plate.Size.Length)} length × {Measure(plate.Size.Width)} width)";
FillPlateGrid(plans, currentIndex);
}
private void FillPlateGrid(IReadOnlyList<AutomaticCutOffPlan> plans, int activeIndex)
{
plateGrid.BeginUpdate();
plateGrid.Items.Clear();
for (var index = 0; index < plans.Count; index++)
{
var sheet = plates[index];
var tail = p.HasSeparatedTail
? $"Retained tail: {Measure(p.TailLength)} length × {Measure(sheet.Size.Width)} width; "
+ $"separator X = {SignedPosition(p.TailSeparatorX.Value)}."
: "Retained tail: none verified.";
return string.Join(Environment.NewLine, new[]
{
$"Plate {index + 1} ({Measure(sheet.Size.Length)} length × {Measure(sheet.Size.Width)} width): {PlanStatus(p)}",
$"Occupied: {Measure(p.OccupiedSpan)}; used span: {Measure(p.UsedSpan)}. {tail}",
}.Concat(Diagnostics(p)));
});
diagnosticsBox.Text = string.Join(Environment.NewLine + Environment.NewLine, new[] { status }.Concat(summaries));
var plan = plans[index];
var label = (index + 1).ToString(CultureInfo.CurrentCulture);
if (index == activeIndex)
label += " (active)";
var row = new ListViewItem(label);
row.SubItems.Add(plan.Definitions.Count == 0 ? "—" : plan.Definitions.Count.ToString(CultureInfo.CurrentCulture));
row.SubItems.Add(Measure(plan.OccupiedSpan));
row.SubItems.Add(Measure(plan.UsedSpan));
row.SubItems.Add(plan.HasSeparatedTail ? Measure(plan.TailLength) : "—");
row.SubItems.Add(plan.HasBlockingDiagnostics ? "Blocked" : plan.Definitions.Count > 0 ? "Ready" : "No change");
if (plan.HasBlockingDiagnostics)
row.ForeColor = BlockedColor;
else if (plan.Definitions.Count > 0)
row.ForeColor = ReadyColor;
else
row.ForeColor = SystemColors.GrayText;
plateGrid.Items.Add(row);
}
plateGrid.EndUpdate();
}
private void ResetSummary()
{
occupiedLabel.Text = "Occupied span from origin: —";
usedLabel.Text = "Proposed used span from origin: —";
separatorLabel.Text = "Verified separator program: —";
tailLabel.Text = "Proposed retained tail: —";
occupiedLabel.Text = "—";
usedLabel.Text = "—";
separatorLabel.Text = "—";
tailLabel.Text = "—";
plateGrid.Items.Clear();
}
private void ShowError(Exception error)
@@ -158,14 +204,16 @@ public partial class AutomaticCutOffForm : Form
ClearPreview();
ResetSummary();
applyButton.Enabled = false;
diagnosticsBox.Text = $"Unable to complete the cut-off operation: {error.Message}";
SetStatus($"Error — {error.Message}", BlockedColor);
diagnosticsBox.Text = error.Message;
}
private void SettingsBox_TextChanged(object sender, EventArgs e)
{
ClearPreview();
ResetSummary();
diagnosticsBox.Text = "Settings changed. Preview or Apply will calculate a fresh plan.";
SetStatus("Settings changed — press Preview.", NeutralColor);
diagnosticsBox.Text = string.Empty;
applyButton.Enabled = true;
}
+2 -2
View File
@@ -6,10 +6,10 @@ Finish nesting, then choose **Nest > Automatic Scrap Cutoffs (All Plates)** to p
1. Finish or cancel any active nesting/fill operation first, and wait for it to finish stopping; closing its progress window alone does not finish the operation. Enter strip spacing along the sheet's length. The initial value is **35 in**, or **889 mm** for a metric nest. This is the distance between nominal cut lines, not the length of a cut.
2. Set **Minimum tail to keep**, initially **12 in** or **304.8 mm**. This measures the reusable tail's length along X, after separator clearance, not its area. A shorter tail does not get a new final separator; a tail at or above the minimum can be retained. Zero allows any positive tail.
3. Opening the modal dialog previews the initial proposal. After changing either setting, choose **Preview** to refresh it. Cuts run across the active sheet's actual width, advancing along X from the origin-side edge in the plate's quadrant. Review the used span, proposed separator, retained tail dimensions, and diagnostics; the sheet cannot be edited while the dialog is open.
3. Opening the modal dialog previews the initial proposal. After changing either setting, choose **Preview** to refresh it. Cuts run across the active sheet's actual width, advancing along X from the origin-side edge in the plate's quadrant. A bold green or red status line states what **Apply** will do; the occupied span, used span, separator X, and retained tail appear as measurements, and the details list reports only issues and notices. The sheet cannot be edited while the dialog is open.
4. Choose **Apply** (or **Apply to All Plates**) to recalculate against the current parts/settings and add ordinary cutoff definitions at the end of each plate's cutting sequence. Blocked or empty proposals add nothing. Existing parts and manual cutoff definitions are not moved or reordered. **Cancel**, Escape, and closing the dialog discard the detached preview.
For the Nest command, the on-sheet overlay and upper measurements show the active plate; the scrollable diagnostics list reports every plate's proposed cuts, occupied/used spans, retained tail and warnings. Each plate uses its own dimensions, quadrant and part spacing with the dialog's common spacing/minimum-tail settings and current cutoff clearance/direction settings. No plate navigation or per-plate confirmation is required. Empty or unchanged plates are left untouched. A blocking conflict or invalid plate prevents changes to **all** plates, with the plate number reported; resolve it before retrying. If application fails after changes begin, all touched plates' original cutoff programs and sequence are restored. An explicit incomplete-recovery warning requires manual review before saving or cutting.
For the Nest command, the on-sheet overlay and upper measurements show the active plate; a per-plate table lists each plate's proposed cut-off count, occupied and used spans, retained tail, and readiness (Ready, Blocked, or No change), while the details list reports only issues and notices, each prefixed with its plate number. Each plate uses its own dimensions, quadrant and part spacing with the dialog's common spacing/minimum-tail settings and current cutoff clearance/direction settings. No plate navigation or per-plate confirmation is required. Empty or unchanged plates are left untouched. A blocking conflict or invalid plate prevents changes to **all** plates, with the plate number reported; resolve it before retrying. If application fails after changes begin, all touched plates' original cutoff programs and sequence are restored. An explicit incomplete-recovery warning requires manual review before saving or cutting.
An empty sheet is left untouched. Invalid input or out-of-sheet part geometry prevents automatic planning. Excessively small spacing is rejected rather than allocating an unbounded number of cuts. Small bounded discrepancies between converted material and cached bounds—including amplified roundoff in fitted near-tangent arcs—are accepted without changing the part or its program. The same conservative numerical padding applies to cutoff pruning and exclusions. Their broad-phase envelope uses the farther edge of the cached box and converted perimeter; a cutoff within the narrow uncertainty band at a cached edge gets a full-envelope exclusion instead of relying on a near-tangent contour intersection. Elsewhere the contour is retained for precise trimming. Larger arc-center discrepancies still prevent automatic planning rather than risking a cut through material; repair the geometry before retrying.