feat(ui): add Parts tab listing pass/fail status for every part

Lists every part in the cutting inspection panel's scope, including ones
with no finding, so an all-clear nest doesn't just look like an empty
grid. Selecting a part jumps to its first issue or zooms to it when clear.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Sonnet 5 committed 2026-10-17 20:54:24 -04:00
1 parent 6f555345e3
commit 9d6c10c563
3 files changed
+192 -2

No files matched your search

@@ -215,6 +215,38 @@ public class CuttingInspectionPanelTests
Assert.Null(view.ValidationOverlay.Finding);
});
[Fact]
public void PartsTabListsEveryPartAndFlagsOnlyThoseWithIssues() => 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);
var issues = Field<DataGridView>(panel, "warningGrid");
var parts = Field<DataGridView>(panel, "partsGrid");
Assert.Equal(plate.Parts.Count, parts.Rows.Count);
var flaggedPartNumbers = issues.Rows.Cast<DataGridViewRow>()
.SelectMany(r => r.Tag is OpenNest.Engine.CuttingPlanning.CuttingPlanDiagnosticRow d
? new[] { d.PartNumber, d.OtherPartNumber }.OfType<int>() : [])
.ToHashSet();
Assert.NotEmpty(flaggedPartNumbers);
foreach (DataGridViewRow row in parts.Rows)
{
var partNumber = (int)row.Cells[1].Value!;
var status = (string)row.Cells[2].Value!;
Assert.Equal(!flaggedPartNumbers.Contains(partNumber), status == "Passed");
}
var flaggedRow = parts.Rows.Cast<DataGridViewRow>().First(r => flaggedPartNumbers.Contains((int)r.Cells[1].Value!));
parts.ClearSelection();
flaggedRow.Selected = true; // Cross-selects the matching Issues row and its marker.
Assert.NotNull(editor.PlateView.ValidationOverlay.Finding?.Overlap);
panel.RequestClose();
});
[Fact]
public void PlannedCutOffsKeepTheCutOffLineStyleOnTheProposal() => RunSta(() =>
{
@@ -70,6 +70,28 @@ public class CuttingValidationPanelTests
Assert.Equal(programs, editor.Nest.Plates.SelectMany(p => p.Parts).Select(p => p.Program));
});
[Fact]
public void PartsTabCoversWholeNestAndSelectionCrossesToItsIssue() => RunSta(() =>
{
using var editor = RapidEditor();
editor.Show();
editor.OpenValidationInspection();
var panel = editor.CuttingInspection!;
Pump(() => panel.ValidationReport != null && !panel.IsPlanning);
var issues = Field<DataGridView>(panel, "warningGrid");
var parts = Field<DataGridView>(panel, "partsGrid");
Assert.Equal(editor.Nest.Plates.Sum(p => p.Parts.Count), parts.Rows.Count);
var flaggedRow = parts.Rows.Cast<DataGridViewRow>()
.Single(r => (int)r.Cells[0].Value! == 2 && (int)r.Cells[1].Value! == 3);
Assert.NotEqual("Passed", (string)flaggedRow.Cells[2].Value!);
parts.ClearSelection();
flaggedRow.Selected = true; // Real SelectionChanged precedes CurrentCell assignment.
var selected = Assert.Single(issues.SelectedRows.Cast<DataGridViewRow>());
Assert.True(selected.Tag is PostVerificationFinding
{ Kind: PostVerificationKind.RapidCrossing, PlateNumber: 2, PartNumber: 3 });
panel.RequestClose();
});
[Theory]
[InlineData(false)]
[InlineData(true)]
+138 -2
View File
@@ -120,6 +120,19 @@ internal sealed class CuttingInspectionPanel : UserControl
RowHeadersVisible = false,
AutoGenerateColumns = false
});
// Every part in scope, including ones with no finding, so "no rows" never reads as "all clear".
private readonly DataGridView partsGrid = GridStyle.Apply(new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
MultiSelect = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
RowHeadersVisible = false,
AutoGenerateColumns = false
});
private readonly TabControl resultTabs = new() { Dock = DockStyle.Fill };
private ValidationInspectionScope scope;
private bool closed;
private int generation;
@@ -181,7 +194,12 @@ internal sealed class CuttingInspectionPanel : UserControl
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
layout.Controls.Add(statusBanner, 0, 0);
layout.Controls.Add(header, 0, 1);
layout.Controls.Add(warningGrid, 0, 2);
var issuesTab = new TabPage("Issues");
issuesTab.Controls.Add(warningGrid);
var partsTab = new TabPage("Parts");
partsTab.Controls.Add(partsGrid);
resultTabs.TabPages.AddRange([issuesTab, partsTab]);
layout.Controls.Add(resultTabs, 0, 2);
Controls.Add(layout);
foreach (var name in new[] { "Plate", "Part", "With", "Issue" })
warningGrid.Columns.Add(new DataGridViewTextBoxColumn
@@ -191,6 +209,14 @@ internal sealed class CuttingInspectionPanel : UserControl
SortMode = DataGridViewColumnSortMode.NotSortable,
AutoSizeMode = name == "Issue" ? DataGridViewAutoSizeColumnMode.Fill : DataGridViewAutoSizeColumnMode.None
});
foreach (var name in new[] { "Plate", "Part", "Status" })
partsGrid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = name,
Width = 48,
SortMode = DataGridViewColumnSortMode.NotSortable,
AutoSizeMode = name == "Status" ? DataGridViewAutoSizeColumnMode.Fill : DataGridViewAutoSizeColumnMode.None
});
Padding = new Padding(10);
planButton.Click += (_, _) => StartCurrentMode();
cancelButton.Click += (_, _) => RequestClose();
@@ -204,6 +230,7 @@ internal sealed class CuttingInspectionPanel : UserControl
previousButton.Click += (_, _) => ShowPlate(selectedPlate - 1);
nextButton.Click += (_, _) => ShowPlate(selectedPlate + 1);
warningGrid.SelectionChanged += (_, _) => NavigateFinding();
partsGrid.SelectionChanged += (_, _) => NavigatePart();
ShowSettings();
}
@@ -440,6 +467,7 @@ internal sealed class CuttingInspectionPanel : UserControl
warningGrid.Rows[index].Tag = finding;
}
warningGrid.ClearSelection();
PopulatePartsGrid();
}
finally { populatingWarnings = false; }
}
@@ -654,6 +682,7 @@ internal sealed class CuttingInspectionPanel : UserControl
warningGrid.Rows[index].Tag = row;
}
warningGrid.ClearSelection();
PopulatePartsGrid();
}
finally { populatingWarnings = false; }
ShowPlanStatus();
@@ -762,7 +791,7 @@ internal sealed class CuttingInspectionPanel : UserControl
previewLabel.Text = "No current preview.";
SetStatus(Tone.Neutral, string.Empty);
populatingWarnings = true;
try { warningGrid.Rows.Clear(); }
try { warningGrid.Rows.Clear(); partsGrid.Rows.Clear(); }
finally { populatingWarnings = false; }
}
@@ -893,6 +922,113 @@ internal sealed class CuttingInspectionPanel : UserControl
SetDetail("Zoomed to the affected part; this issue has no exact location.");
}
/// <summary>
/// Every part in scope, so a part with nothing wrong still shows as Passed instead of
/// simply not appearing. Validate always covers the whole nest (matching the Issues tab);
/// Plan covers only the plates this instance planned.
/// </summary>
private void PopulatePartsGrid()
{
partsGrid.Rows.Clear();
if (Mode == CuttingInspectionMode.Validate)
{
var all = editor.Nest.Plates;
for (var i = 0; i < all.Count; i++)
AddPartRows(i + 1, all[i].Parts.Count);
}
else
for (var i = 0; i < plates.Length; i++)
AddPartRows(plateNumbers[i], plates[i].Parts.Count);
}
private void AddPartRows(int plateNumber, int partCount)
{
for (var partIndex = 0; partIndex < partCount; partIndex++)
{
var partNumber = partIndex + 1;
var tone = Tone.Good;
var matches = 0;
foreach (DataGridViewRow row in warningGrid.Rows)
{
if (!Refers(row.Tag, plateNumber, partNumber)) continue;
matches++;
if (KindOf(row.Tag) == PostVerificationKind.Incomplete) tone = Tone.Bad;
else if (tone != Tone.Bad) tone = Tone.Warning;
}
var status = tone switch
{
Tone.Bad => "Not checked",
Tone.Warning => Count(matches, "issue"),
_ => "Passed",
};
var addedRow = partsGrid.Rows[partsGrid.Rows.Add(plateNumber, partNumber, status)];
addedRow.Tag = (plateNumber, partNumber);
addedRow.DefaultCellStyle.ForeColor = StatusInk(tone);
}
}
private static Color StatusInk(Tone tone) => tone switch
{
Tone.Good => Color.FromArgb(28, 88, 28),
Tone.Warning => Color.FromArgb(118, 78, 0),
Tone.Bad => Color.FromArgb(132, 20, 20),
_ => SystemColors.ControlText,
};
/// <summary>Whether a warning/issue row's tag names this plate and part, either as the subject or the other part.</summary>
private static bool Refers(object tag, int plateNumber, int partNumber) => tag switch
{
PostVerificationFinding f => f.PlateNumber == plateNumber && (f.PartNumber == partNumber || f.OtherPartNumber == partNumber),
CuttingPlanDiagnosticRow r => r.PlateNumber == plateNumber && (r.PartNumber == partNumber || r.OtherPartNumber == partNumber),
_ => false,
};
private static PostVerificationKind? KindOf(object tag) => tag switch
{
PostVerificationFinding f => f.Kind,
CuttingPlanDiagnosticRow { Witness: { } witness } => witness.Kind,
_ => null,
};
/// <summary>Selecting a part jumps to its first issue (reusing that marker) or just zooms to it when clear.</summary>
private void NavigatePart()
{
if (closed || closeRequested || pendingMode != null || populatingWarnings || IsPlanning || editor.IsDisposed
|| partsGrid.SelectedRows.Count == 0 || partsGrid.SelectedRows[0].Tag is not ValueTuple<int, int> target)
return;
var (plateNumber, partNumber) = target;
foreach (DataGridViewRow row in warningGrid.Rows)
if (Refers(row.Tag, plateNumber, partNumber))
{
warningGrid.ClearSelection();
row.Selected = true;
warningGrid.CurrentCell = row.Cells[0];
return; // warningGrid.SelectionChanged reuses NavigateFinding/NavigateValidationFinding.
}
view.ClearValidationFinding();
if (Mode == CuttingInspectionMode.Validate)
{
if (plateNumber < 1 || plateNumber > editor.Nest.Plates.Count) return;
if (editor.PlateManager.CurrentIndex != plateNumber - 1)
editor.PlateManager.LoadAt(plateNumber - 1);
if (partNumber > 0 && partNumber <= view.Plate.Parts.Count
&& view.Plate.Parts[partNumber - 1]?.BoundingBox is { } box
&& double.IsFinite(box.X) && double.IsFinite(box.Y) && double.IsFinite(box.Length) && double.IsFinite(box.Width))
{
var pad = System.Math.Max(0.5, System.Math.Max(box.Length, box.Width) * 0.12);
view.ZoomToArea(box.X - pad, box.Y - pad, box.Length + 2 * pad, box.Width + 2 * pad);
}
else
view.ZoomToFit();
SetDetail("No issue found for this part.");
}
else
{
var index = Array.IndexOf(plateNumbers, plateNumber);
if (index >= 0) ShowPlate(index);
}
}
internal CuttingCommitResult CommitResult { get; private set; }
private void Apply()