mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
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:
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)]
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user