Files
OpenNest/OpenNest.Tests/Shapes/PlateSizesTests.cs
AJ Isaacs 0e45c13515 feat(shapes): add PlateSizes catalog and wire Ctrl+P to snap-to-standard
PlateSizes holds standard mill sheet sizes (48x96 through 96x240) and
exposes Recommend() which snaps small layouts to an increment and
rounds larger layouts up to the nearest fitting sheet. Plate.SnapToStandardSize
applies the result while preserving long-axis orientation, and the
existing Ctrl+P "Resize to Fit" menu in EditNestForm now calls it
instead of the simple round-up AutoSize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:16:29 -04:00

312 lines
9.3 KiB
C#

using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
using OpenNest.Shapes;
namespace OpenNest.Tests.Shapes;
public class PlateSizesTests
{
[Fact]
public void All_IsNotEmpty()
{
Assert.NotEmpty(PlateSizes.All);
}
[Fact]
public void All_DoesNotContain48x48()
{
// 48x48 is not a standard sheet - it's the default MinSheet threshold only.
Assert.DoesNotContain(PlateSizes.All, e => e.Width == 48 && e.Length == 48);
}
[Fact]
public void All_Smallest_Is48x96()
{
var smallest = PlateSizes.All.OrderBy(e => e.Area).First();
Assert.Equal(48, smallest.Width);
Assert.Equal(96, smallest.Length);
}
[Fact]
public void All_SortedByAreaAscending()
{
for (var i = 1; i < PlateSizes.All.Count; i++)
Assert.True(PlateSizes.All[i].Area >= PlateSizes.All[i - 1].Area);
}
[Fact]
public void All_Entries_AreCanonical_WidthLessOrEqualLength()
{
foreach (var entry in PlateSizes.All)
Assert.True(entry.Width <= entry.Length, $"{entry.Label} not in canonical orientation");
}
[Theory]
[InlineData(40, 40, true)] // small - fits trivially
[InlineData(48, 96, true)] // exact
[InlineData(96, 48, true)] // rotated exact
[InlineData(90, 40, true)] // rotated
[InlineData(49, 97, false)] // just over in both dims
[InlineData(50, 50, false)] // too wide in both orientations
public void Entry_Fits_RespectsRotation(double w, double h, bool expected)
{
var entry = new PlateSizes.Entry("48x96", 48, 96);
Assert.Equal(expected, entry.Fits(w, h));
}
[Fact]
public void TryGet_KnownLabel_ReturnsEntry()
{
Assert.True(PlateSizes.TryGet("48x96", out var entry));
Assert.Equal(48, entry.Width);
Assert.Equal(96, entry.Length);
}
[Fact]
public void TryGet_IsCaseInsensitive()
{
Assert.True(PlateSizes.TryGet("48X96", out var entry));
Assert.Equal(48, entry.Width);
Assert.Equal(96, entry.Length);
}
[Fact]
public void TryGet_UnknownLabel_ReturnsFalse()
{
Assert.False(PlateSizes.TryGet("bogus", out _));
}
[Fact]
public void Recommend_BelowMin_SnapsToDefaultIncrementOfOne()
{
var bbox = new Box(0, 0, 10.3, 20.7);
var result = PlateSizes.Recommend(bbox);
Assert.Equal(11, result.Width);
Assert.Equal(21, result.Length);
Assert.Null(result.MatchedLabel);
}
[Fact]
public void Recommend_BelowMin_UsesCustomIncrement()
{
var bbox = new Box(0, 0, 10.3, 20.7);
var options = new PlateSizeOptions { SnapIncrement = 0.25 };
var result = PlateSizes.Recommend(bbox, options);
Assert.Equal(10.5, result.Width, 4);
Assert.Equal(20.75, result.Length, 4);
Assert.Null(result.MatchedLabel);
}
[Fact]
public void Recommend_ExactlyAtMin_Snaps()
{
var bbox = new Box(0, 0, 48, 48);
var result = PlateSizes.Recommend(bbox);
Assert.Equal(48, result.Width);
Assert.Equal(48, result.Length);
Assert.Null(result.MatchedLabel);
}
[Fact]
public void Recommend_AboveMin_PicksSmallestContainingStandardSheet()
{
var bbox = new Box(0, 0, 40, 90);
var result = PlateSizes.Recommend(bbox);
Assert.Equal(48, result.Width);
Assert.Equal(96, result.Length);
Assert.Equal("48x96", result.MatchedLabel);
}
[Fact]
public void Recommend_AboveMin_WithRotation_PicksSmallestSheet()
{
var bbox = new Box(0, 0, 90, 40);
var result = PlateSizes.Recommend(bbox);
Assert.Equal("48x96", result.MatchedLabel);
}
[Fact]
public void Recommend_JustOver48_PicksNextStandardSize()
{
var bbox = new Box(0, 0, 50, 100);
var result = PlateSizes.Recommend(bbox);
Assert.Equal(60, result.Width);
Assert.Equal(120, result.Length);
Assert.Equal("60x120", result.MatchedLabel);
}
[Fact]
public void Recommend_MarginIsAppliedPerSide()
{
// 46 + 2*1 = 48 (fits exactly), 94 + 2*1 = 96 (fits exactly)
var bbox = new Box(0, 0, 46, 94);
var options = new PlateSizeOptions { Margin = 1 };
var result = PlateSizes.Recommend(bbox, options);
Assert.Equal("48x96", result.MatchedLabel);
}
[Fact]
public void Recommend_MarginPushesToNextSheet()
{
// 47 + 2 = 49 > 48, so 48x96 no longer fits -> next standard
var bbox = new Box(0, 0, 47, 95);
var options = new PlateSizeOptions { Margin = 1 };
var result = PlateSizes.Recommend(bbox, options);
Assert.NotEqual("48x96", result.MatchedLabel);
Assert.True(result.Width >= 49);
Assert.True(result.Length >= 97);
}
[Fact]
public void Recommend_AllowedSizes_StandardLabelWhitelist()
{
// 60x120 is the only option; 50x50 is above min so it routes to standard
var bbox = new Box(0, 0, 50, 50);
var options = new PlateSizeOptions { AllowedSizes = new[] { "60x120" } };
var result = PlateSizes.Recommend(bbox, options);
Assert.Equal("60x120", result.MatchedLabel);
}
[Fact]
public void Recommend_AllowedSizes_ArbitraryWxHString()
{
// 50x100 isn't in the standard catalog but is valid as an ad-hoc entry.
// bbox 49x99 doesn't fit 48x96 or 48x120, does fit 50x100 and 60x120,
// but only 50x100 is allowed.
var bbox = new Box(0, 0, 49, 99);
var options = new PlateSizeOptions { AllowedSizes = new[] { "50x100" } };
var result = PlateSizes.Recommend(bbox, options);
Assert.Equal(50, result.Width);
Assert.Equal(100, result.Length);
Assert.Equal("50x100", result.MatchedLabel);
}
[Fact]
public void Recommend_NothingFits_FallsBackToSnapUp()
{
// Larger than any catalog sheet
var bbox = new Box(0, 0, 100, 300);
var result = PlateSizes.Recommend(bbox);
Assert.Equal(100, result.Width);
Assert.Equal(300, result.Length);
Assert.Null(result.MatchedLabel);
}
[Fact]
public void Recommend_NothingFitsInAllowedList_FallsBackToSnapUp()
{
// Only 48x96 allowed, but bbox is too big for it
var bbox = new Box(0, 0, 50, 100);
var options = new PlateSizeOptions { AllowedSizes = new[] { "48x96" } };
var result = PlateSizes.Recommend(bbox, options);
Assert.Equal(50, result.Width);
Assert.Equal(100, result.Length);
Assert.Null(result.MatchedLabel);
}
[Fact]
public void Recommend_BoxEnumerable_CombinesIntoEnvelope()
{
// Two boxes that together span 0..40 x 0..90 -> fits 48x96
var boxes = new[]
{
new Box(0, 0, 40, 50),
new Box(0, 40, 30, 50),
};
var result = PlateSizes.Recommend(boxes);
Assert.Equal("48x96", result.MatchedLabel);
}
[Fact]
public void Recommend_BoxEnumerable_Empty_Throws()
{
Assert.Throws<System.ArgumentException>(
() => PlateSizes.Recommend(System.Array.Empty<Box>()));
}
[Fact]
public void PlateSizeOptions_Defaults()
{
var options = new PlateSizeOptions();
Assert.Equal(48, options.MinSheetWidth);
Assert.Equal(48, options.MinSheetLength);
Assert.Equal(1.0, options.SnapIncrement);
Assert.Equal(0, options.Margin);
Assert.Null(options.AllowedSizes);
Assert.Equal(PlateSizeSelection.SmallestArea, options.Selection);
}
[Fact]
public void Recommend_NarrowestFirst_PicksNarrowerSheetOverSmallerArea()
{
// Hypothetical: bbox (47, 47) fits both 48x96 (area 4608) and some narrower option.
// With SmallestArea: picks 48x96 (it's already the smallest 48-wide).
// With NarrowestFirst: also picks 48x96 since that's the narrowest.
// Better test: AllowedSizes = ["60x120", "48x120"] with bbox that fits both.
// 48x120 (area 5760) is narrower; 60x120 (area 7200) has more area.
// SmallestArea picks 48x120; NarrowestFirst also picks 48x120. Both pick the same.
//
// Real divergence: AllowedSizes = ["60x120", "72x120"] with bbox 55x100.
// 60x120 has narrower width (60) AND smaller area (7200 vs 8640), so both agree.
//
// To force divergence: AllowedSizes = ["60x96", "48x144"] with bbox 47x95.
// 60x96 area = 5760, 48x144 area = 6912. SmallestArea -> 60x96.
// NarrowestFirst width 48 < 60 -> 48x144.
var bbox = new Box(0, 0, 47, 95);
var options = new PlateSizeOptions
{
AllowedSizes = new[] { "60x96", "48x144" },
Selection = PlateSizeSelection.NarrowestFirst,
};
var result = PlateSizes.Recommend(bbox, options);
Assert.Equal(48, result.Width);
Assert.Equal(144, result.Length);
}
[Fact]
public void Recommend_SmallestArea_PicksSmallerAreaOverNarrowerWidth()
{
var bbox = new Box(0, 0, 47, 95);
var options = new PlateSizeOptions
{
AllowedSizes = new[] { "60x96", "48x144" },
Selection = PlateSizeSelection.SmallestArea,
};
var result = PlateSizes.Recommend(bbox, options);
Assert.Equal(60, result.Width);
Assert.Equal(96, result.Length);
}
}