Files
OpenNest/OpenNest.Tests/Data/ClampRailEditorTests.cs
T
aj 87849e5326 feat(desktop): show clamps to pull and their time
Slice 3b task 3 (D9 + display). OpenNest.Core: TimingInfo gains
ClampRemovalMinutes (included in +, -, * so every aggregation stays
consistent); Timing.GetTimingInfo(Plate) adds plate.ClampRemovalMinutes
once per physical sheet, before the quantity multiply so it scales
with everything else cut on that sheet; CalculateTime adds it to the
estimate as a flat number of minutes regardless of the cut parameters'
unit. A plate with nothing removed is unaffected (zero).

OpenNest.Data: ClampSettings and ClampRailEditor carry RemovalMinutes
through the same three boundaries as clamp size - the rail/constructor,
the settings file (missing key falls back to
ClampRail.DefaultRemovalMinutes; present but negative/non-numeric/null
is ignored and reported Partial, same as width/depth), and
ToMachineDefaults - except it is a time, not a length, so
ConvertedTo never scales it between inches and millimeters.

Desktop: ClampsForm gets a 'Minutes to remove and replace one clamp'
field wired to the editor; PlateRenderer.DrawRemovedClamps draws each
clamp Plate.RemovedClamps lists (resolved from the full rail, since a
removed clamp is absent from Plate.ClampZones) as a dashed grey
outline with a small 'remove' marker, distinct from an active clamp's
solid hatch and still display-only - no hit test, selection or
sequencing; PlateView calls it right after the active-zone pass.
MainForm's Auto Nest completion flow lists every committed sheet with
a removal as 'Sheet N: remove clamp-X (+5 min)', using
Plate.RemovedClamps/ClampRemovalMinutes from Slice 3b tasks 1-2.

Tests: 10 new Data (ClampSettingsTests, ClampRailEditorTests) covering
round-trip/default/invalid-value handling and the never-scaled-by-units
rule; 4 new Core (TimingTests) covering the per-sheet add, the quantity
scaling, CalculateTime, and nest-level aggregation across plates.
Desktop changes (dialog field, renderer, Auto Nest summary) are
compile-verified only (-p:EnableWindowsTargeting=true), matching the
project's existing WinForms testing bar - no automated UI test exists
for the sibling DrawClampZones either.

Gates: Release OpenNest.Tests 3898 passed/31 skipped, OpenNest.IO.Tests
105 passed, OpenNest.Engine.Tests 878 passed/2 skipped; desktop and
OpenNest.WinForms.Tests cross-builds clean; scoped dotnet format
--verify-no-changes clean.

This completes Slice 3b (D8/D9) on top of Slice 3's rail-following
plates. Not published; master and other feature/clamp-zones-* branches
untouched. Windows runtime/visual acceptance not claimed - the plan's
click-through checklist (after slices 3-5) still applies.
2026-10-13 18:15:49 -04:00

215 lines
7.7 KiB
C#

using OpenNest.Data;
using OpenNest.Geometry;
namespace OpenNest.Tests.Data;
/// <summary>
/// Decision logic of the desktop Clamps dialog: edits one nest's rail in its units, keeps
/// "apply to this nest" (OK) separate from "save as machine defaults" (immediate write).
/// </summary>
public class ClampRailEditorTests
{
private static readonly double[] MachineCenters = { 6, 21, 36, 51, 66, 81, 96, 111 };
[Fact]
public void NestWithoutARailStartsEmptyAtTheDefaultClampSize()
{
var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: false);
Assert.Equal(1.5, editor.Width);
Assert.Equal(0.625, editor.Depth);
Assert.Empty(editor.Centers);
Assert.Null(editor.BuildRail());
var metric = new ClampRailEditor(null, Units.Millimeters, applyToNewNests: false);
Assert.Equal(1.5 * 25.4, metric.Width, 9);
}
[Fact]
public void NestRailIsEditedInPlace()
{
var rail = new ClampRail(1.5, 0.625, MachineCenters);
var editor = new ClampRailEditor(rail, Units.Inches, applyToNewNests: true);
Assert.Equal(MachineCenters, editor.Centers);
Assert.Equal(rail, editor.BuildRail());
Assert.True(editor.ApplyToNewNests);
}
[Fact]
public void CentersStaySortedAndRefuseInvalidOrDuplicateValues()
{
var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: false);
Assert.True(editor.TryAddCenter(21, out _));
Assert.True(editor.TryAddCenter(6, out var none));
Assert.Null(none);
Assert.False(editor.TryAddCenter(21, out var duplicate));
Assert.Contains("already", duplicate);
Assert.False(editor.TryAddCenter(-1, out var negative));
Assert.Contains("origin", negative);
Assert.False(editor.TryAddCenter(double.NaN, out _));
Assert.Equal(new[] { 6.0, 21 }, editor.Centers);
editor.RemoveCenterAt(0);
editor.RemoveCenterAt(5); // out of range: ignored
Assert.Equal(new[] { 21.0 }, editor.Centers);
}
[Fact]
public void ApplyingToTheNestReResolvesEveryPlate()
{
var nest = new Nest();
nest.PlateDefaults.Size = new Size(48, 120);
var full = nest.CreatePlate();
var shorter = nest.CreatePlate();
shorter.Size = new Size(48, 60);
var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: false);
foreach (var center in MachineCenters)
Assert.True(editor.TryAddCenter(center, out _));
editor.ApplyTo(nest);
Assert.Equal(8, full.ClampZones.Count);
Assert.Equal(4, shorter.ClampZones.Count);
Assert.Equal(8, nest.CreatePlate().ClampZones.Count);
// Removing every clamp and applying removes them from the nest.
while (editor.Centers.Count > 0)
editor.RemoveCenterAt(0);
editor.ApplyTo(nest);
Assert.Null(nest.PlateDefaults.ClampRail);
Assert.All(nest.Plates, p => Assert.Empty(p.ClampZones));
}
[Fact]
public void InvalidClampSizeIsRefusedBeforeApplyingOrSaving()
{
var nest = new Nest();
var plate = nest.CreatePlate();
var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: true);
editor.TryAddCenter(6, out _);
editor.Width = 0;
Assert.Throws<ArgumentOutOfRangeException>(() => editor.ApplyTo(nest));
Assert.Throws<ArgumentOutOfRangeException>(() => editor.ToMachineDefaults());
Assert.Empty(plate.ClampZones);
Assert.Null(nest.PlateDefaults.ClampRail);
}
[Fact]
public void MachineDefaultsCaptureTheEditedValuesInTheNestsUnits()
{
var editor = new ClampRailEditor(null, Units.Millimeters, applyToNewNests: true);
editor.Width = 40;
editor.Depth = 16;
editor.TryAddCenter(150, out _);
var settings = editor.ToMachineDefaults();
Assert.True(settings.Enabled);
Assert.Equal(Units.Millimeters, settings.Units);
Assert.Equal(40, settings.Width);
Assert.Equal(16, settings.Depth);
Assert.Equal(new[] { 150.0 }, settings.Centers);
// Without any clamp listed, saved defaults never enable clamps for new nests.
editor.RemoveCenterAt(0);
Assert.False(editor.ToMachineDefaults().Enabled);
editor.ApplyToNewNests = false;
editor.TryAddCenter(150, out _);
Assert.False(editor.ToMachineDefaults().Enabled);
}
[Fact]
public void LoadingMachineDefaultsOnlyFillsTheEditorInTheNestsUnits()
{
var nest = new Nest { Units = Units.Millimeters };
var plate = nest.CreatePlate();
var defaults = new ClampSettings
{
Enabled = true,
Units = Units.Inches,
Width = 1.5,
Depth = 0.625,
Centers = new[] { 21.0, 6 },
};
var editor = new ClampRailEditor(null, Units.Millimeters, applyToNewNests: false);
editor.LoadMachineDefaults(defaults);
Assert.Equal(1.5 * 25.4, editor.Width, 9);
Assert.Equal(0.625 * 25.4, editor.Depth, 9);
Assert.Equal(new[] { 6 * 25.4, 21 * 25.4 }, editor.Centers);
Assert.True(editor.ApplyToNewNests);
Assert.Empty(plate.ClampZones); // nothing applied until OK
Assert.Null(nest.PlateDefaults.ClampRail);
// Disabled defaults still load their values but do not ask to enable new nests.
defaults.Enabled = false;
editor.LoadMachineDefaults(defaults);
Assert.False(editor.ApplyToNewNests);
Assert.Equal(2, editor.Centers.Count);
}
[Fact]
public void ConvertedSettingsKeepEnabledAndScaleEveryLength()
{
var inches = new ClampSettings { Enabled = true, Width = 1.5, Depth = 0.625, Centers = new[] { 6.0 } };
var metric = inches.ConvertedTo(Units.Millimeters);
Assert.True(metric.Enabled);
Assert.Equal(Units.Millimeters, metric.Units);
Assert.Equal(38.1, metric.Width, 9);
Assert.Equal(15.875, metric.Depth, 9);
Assert.Equal(152.4, metric.Centers[0], 9);
Assert.Equal(1.5, inches.Width); // the source is not modified
Assert.Equal(1.5, inches.ConvertedTo(Units.Inches).Width);
}
// D9: the editor carries removal minutes through the same three boundaries as size.
[Fact]
public void RemovalMinutesStartsFromTheRailOrMachineDefault()
{
var rail = new ClampRail(1.5, 0.625, MachineCenters, removalMinutes: 7);
Assert.Equal(7, new ClampRailEditor(rail, Units.Inches, false).RemovalMinutes);
Assert.Equal(ClampRail.DefaultRemovalMinutes,
new ClampRailEditor(null, Units.Inches, false).RemovalMinutes);
}
[Fact]
public void RemovalMinutesIsNeverConvertedBetweenUnitsOnLoadOrBuild()
{
var editor = new ClampRailEditor(null, Units.Millimeters, applyToNewNests: true);
editor.RemovalMinutes = 8;
editor.TryAddCenter(150, out _);
Assert.Equal(8, editor.ToMachineDefaults().RemovalMinutes);
Assert.Equal(8, editor.BuildRail()!.RemovalMinutes);
var defaults = new ClampSettings
{
Enabled = true,
Units = Units.Inches,
RemovalMinutes = 9,
Centers = new[] { 6.0 },
};
editor.LoadMachineDefaults(defaults);
Assert.Equal(9, editor.RemovalMinutes); // a time, so unaffected by the unit change
}
[Fact]
public void InvalidRemovalMinutesIsRefusedBeforeSaving()
{
var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: true);
editor.TryAddCenter(6, out _);
editor.RemovalMinutes = -1;
Assert.Throws<ArgumentOutOfRangeException>(() => editor.ToMachineDefaults());
Assert.Throws<ArgumentOutOfRangeException>(() => editor.BuildRail());
}
}