diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index 8e58ea9..e5a3714 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -1,6 +1,7 @@ using OpenNest.Collections; using OpenNest.Geometry; using OpenNest.Math; +using OpenNest.Shapes; using System; using System.Collections.Generic; using System.Linq; @@ -548,6 +549,65 @@ namespace OpenNest Rounding.RoundUpToNearest(xExtent, roundingFactor)); } + /// + /// Sizes the plate using the catalog: small + /// layouts snap to an increment, larger ones round up to the next + /// standard mill sheet. The plate's long-axis orientation (X vs Y) + /// is preserved. Does nothing if the plate has no parts. + /// + public PlateSizeResult SnapToStandardSize(PlateSizeOptions options = null) + { + if (Parts.Count == 0) + return default; + + var bounds = Parts.GetBoundingBox(); + + // Quadrant-aware extents relative to the plate origin, matching AutoSize. + double xExtent; + double yExtent; + + switch (Quadrant) + { + case 1: + xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; + yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; + break; + + case 2: + xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; + yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top; + break; + + case 3: + xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left; + yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; + break; + + case 4: + xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right; + yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom; + break; + + default: + return default; + } + + // PlateSizes.Recommend takes (short, long); canonicalize then map + // the result back so the plate's long axis stays aligned with the + // parts' long axis. + var shortDim = System.Math.Min(xExtent, yExtent); + var longDim = System.Math.Max(xExtent, yExtent); + var result = PlateSizes.Recommend(shortDim, longDim, options); + + // Plate convention: Length = X axis, Width = Y axis. + if (xExtent >= yExtent) + Size = new Size(result.Width, result.Length); // X is the long axis + else + Size = new Size(result.Length, result.Width); // Y is the long axis + + return result; + } + /// /// Gets the area of the top surface of the plate. /// diff --git a/OpenNest.Core/Shapes/PlateSizes.cs b/OpenNest.Core/Shapes/PlateSizes.cs new file mode 100644 index 0000000..08af25d --- /dev/null +++ b/OpenNest.Core/Shapes/PlateSizes.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenNest.Geometry; + +namespace OpenNest.Shapes +{ + /// + /// Catalog of standard mill sheet sizes (inches) with helpers for matching + /// a bounding box to a recommended plate size. Uses the project-wide + /// (Width, Length) convention where Width is the short dimension and + /// Length is the long dimension. + /// + public static class PlateSizes + { + public readonly record struct Entry(string Label, double Width, double Length) + { + public double Area => Width * Length; + + /// + /// Returns true if a part of the given dimensions fits within this entry + /// in either orientation. + /// + public bool Fits(double width, double length) => + (width <= Width && length <= Length) || (width <= Length && length <= Width); + } + + /// + /// Standard mill sheet sizes (inches), sorted by area ascending. + /// Canonical orientation: Width <= Length. + /// + public static IReadOnlyList All { get; } = new[] + { + new Entry("48x96", 48, 96), // 4608 + new Entry("48x120", 48, 120), // 5760 + new Entry("48x144", 48, 144), // 6912 + new Entry("60x120", 60, 120), // 7200 + new Entry("60x144", 60, 144), // 8640 + new Entry("72x120", 72, 120), // 8640 + new Entry("72x144", 72, 144), // 10368 + new Entry("96x240", 96, 240), // 23040 + }; + + /// + /// Looks up a standard size by label. Case-insensitive. + /// + public static bool TryGet(string label, out Entry entry) + { + if (!string.IsNullOrWhiteSpace(label)) + { + foreach (var candidate in All) + { + if (string.Equals(candidate.Label, label, StringComparison.OrdinalIgnoreCase)) + { + entry = candidate; + return true; + } + } + } + + entry = default; + return false; + } + + /// + /// Recommends a plate size for the given bounding box. The box's + /// spatial axes are normalized to (short, long) so neither the bbox + /// orientation nor Box's internal Length/Width naming matters. + /// + public static PlateSizeResult Recommend(Box bbox, PlateSizeOptions options = null) + { + var a = bbox.Width; + var b = bbox.Length; + return Recommend(System.Math.Min(a, b), System.Math.Max(a, b), options); + } + + /// + /// Recommends a plate size for the envelope of the given boxes. + /// + public static PlateSizeResult Recommend(IEnumerable boxes, PlateSizeOptions options = null) + { + if (boxes == null) + throw new ArgumentNullException(nameof(boxes)); + + var hasAny = false; + var minX = double.PositiveInfinity; + var minY = double.PositiveInfinity; + var maxX = double.NegativeInfinity; + var maxY = double.NegativeInfinity; + + foreach (var box in boxes) + { + hasAny = true; + if (box.Left < minX) minX = box.Left; + if (box.Bottom < minY) minY = box.Bottom; + if (box.Right > maxX) maxX = box.Right; + if (box.Top > maxY) maxY = box.Top; + } + + if (!hasAny) + throw new ArgumentException("At least one box is required.", nameof(boxes)); + + var b = maxX - minX; + var a = maxY - minY; + return Recommend(System.Math.Min(a, b), System.Math.Max(a, b), options); + } + + /// + /// Recommends a plate size for a (width, length) pair. + /// Inputs are treated as orientation-independent. + /// + public static PlateSizeResult Recommend(double width, double length, PlateSizeOptions options = null) + { + options ??= new PlateSizeOptions(); + + var w = width + 2 * options.Margin; + var l = length + 2 * options.Margin; + + // Canonicalize (short, long) — Fits handles rotation anyway, but + // normalizing lets the below-min comparison use the narrower + // MinSheet dimensions consistently. + if (w > l) + (w, l) = (l, w); + + // Below full-sheet threshold: snap each dimension up to the nearest increment. + if (w <= options.MinSheetWidth && l <= options.MinSheetLength) + return SnapResult(w, l, options.SnapIncrement); + + var catalog = BuildCatalog(options.AllowedSizes); + + var best = PickBest(catalog, w, l, options.Selection); + if (best.HasValue) + return new PlateSizeResult(best.Value.Width, best.Value.Length, best.Value.Label); + + // Nothing in the catalog fits - fall back to snap-up (ad-hoc oversize sheet). + return SnapResult(w, l, options.SnapIncrement); + } + + private static PlateSizeResult SnapResult(double width, double length, double increment) + { + if (increment <= 0) + return new PlateSizeResult(width, length, null); + + return new PlateSizeResult(SnapUp(width, increment), SnapUp(length, increment), null); + } + + private static double SnapUp(double value, double increment) + { + var steps = System.Math.Ceiling(value / increment); + return steps * increment; + } + + private static IReadOnlyList BuildCatalog(IReadOnlyList allowedSizes) + { + if (allowedSizes == null || allowedSizes.Count == 0) + return All; + + var result = new List(allowedSizes.Count); + foreach (var label in allowedSizes) + { + if (TryParseEntry(label, out var entry)) + result.Add(entry); + } + + return result; + } + + private static bool TryParseEntry(string label, out Entry entry) + { + if (TryGet(label, out entry)) + return true; + + // Accept ad-hoc "WxL" strings (e.g. "50x100", "50 x 100"). + if (!string.IsNullOrWhiteSpace(label)) + { + var parts = label.Split(new[] { 'x', 'X' }, 2); + if (parts.Length == 2 + && double.TryParse(parts[0].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var a) + && double.TryParse(parts[1].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var b) + && a > 0 && b > 0) + { + var width = System.Math.Min(a, b); + var length = System.Math.Max(a, b); + entry = new Entry(label.Trim(), width, length); + return true; + } + } + + entry = default; + return false; + } + + private static Entry? PickBest(IReadOnlyList catalog, double width, double length, PlateSizeSelection selection) + { + var fitting = catalog.Where(e => e.Fits(width, length)); + + fitting = selection switch + { + PlateSizeSelection.NarrowestFirst => fitting.OrderBy(e => e.Width).ThenBy(e => e.Area), + _ => fitting.OrderBy(e => e.Area).ThenBy(e => e.Width), + }; + + foreach (var candidate in fitting) + return candidate; + + return null; + } + } + + public readonly record struct PlateSizeResult(double Width, double Length, string MatchedLabel) + { + public bool IsStandard => MatchedLabel != null; + } + + public sealed class PlateSizeOptions + { + /// + /// If the margin-adjusted bounding box fits within MinSheetWidth x MinSheetLength + /// the result is snapped to instead of routed to a + /// standard sheet. Default 48" x 48". + /// + public double MinSheetWidth { get; set; } = 48; + public double MinSheetLength { get; set; } = 48; + + /// + /// Increment used for below-threshold rounding and oversize fallback. Default 1". + /// + public double SnapIncrement { get; set; } = 1.0; + + /// + /// Extra clearance added to each side of the bounding box before matching. + /// + public double Margin { get; set; } = 0; + + /// + /// Optional whitelist. When non-empty, only these sizes are considered. + /// Entries may be standard catalog labels (e.g. "48x96") or arbitrary + /// "WxL" strings (e.g. "50x100"). + /// + public IReadOnlyList AllowedSizes { get; set; } + + /// + /// Tiebreaker when multiple sheets can contain the bounding box. + /// + public PlateSizeSelection Selection { get; set; } = PlateSizeSelection.SmallestArea; + } + + public enum PlateSizeSelection + { + /// Pick the cheapest sheet that contains the bbox (smallest area). + SmallestArea, + /// Prefer narrower-width sheets (e.g. 48-wide before 60-wide). + NarrowestFirst, + } +} diff --git a/OpenNest.Tests/PlateSnapToStandardSizeTests.cs b/OpenNest.Tests/PlateSnapToStandardSizeTests.cs new file mode 100644 index 0000000..b4a9cd1 --- /dev/null +++ b/OpenNest.Tests/PlateSnapToStandardSizeTests.cs @@ -0,0 +1,118 @@ +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.Shapes; + +namespace OpenNest.Tests; + +public class PlateSnapToStandardSizeTests +{ + private static Part MakeRectPart(double x, double y, double length, double width) + { + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(length, 0))); + pgm.Codes.Add(new LinearMove(new Vector(length, width))); + pgm.Codes.Add(new LinearMove(new Vector(0, width))); + pgm.Codes.Add(new LinearMove(new Vector(0, 0))); + var drawing = new Drawing("test", pgm); + var part = new Part(drawing); + part.Offset(x, y); + return part; + } + + [Fact] + public void SnapToStandardSize_SmallParts_SnapsToIncrement() + { + var plate = new Plate(200, 200); // oversized starting size + plate.Parts.Add(MakeRectPart(0, 0, 10, 20)); + + var result = plate.SnapToStandardSize(); + + // 10x20 is well below 48x48 MinSheet -> snap to integer increment. + Assert.Null(result.MatchedLabel); + Assert.Equal(10, plate.Size.Length); // X axis + Assert.Equal(20, plate.Size.Width); // Y axis + } + + [Fact] + public void SnapToStandardSize_SmallPartsWithFractionalIncrement_UsesIncrement() + { + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 10.3, 20.7)); + + var result = plate.SnapToStandardSize(new PlateSizeOptions { SnapIncrement = 0.25 }); + + Assert.Null(result.MatchedLabel); + Assert.Equal(10.5, plate.Size.Length, 4); + Assert.Equal(20.75, plate.Size.Width, 4); + } + + [Fact] + public void SnapToStandardSize_40x90Part_SnapsToStandard48x96_XLong() + { + // Part is 90 long (X) x 40 wide (Y) -> X is the long axis. + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 90, 40)); + + var result = plate.SnapToStandardSize(); + + Assert.Equal("48x96", result.MatchedLabel); + Assert.Equal(96, plate.Size.Length); // X axis = long + Assert.Equal(48, plate.Size.Width); // Y axis = short + } + + [Fact] + public void SnapToStandardSize_90TallPart_SnapsToStandard48x96_YLong() + { + // Part is 40 long (X) x 90 wide (Y) -> Y is the long axis. + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 40, 90)); + + var result = plate.SnapToStandardSize(); + + Assert.Equal("48x96", result.MatchedLabel); + Assert.Equal(48, plate.Size.Length); // X axis = short + Assert.Equal(96, plate.Size.Width); // Y axis = long + } + + [Fact] + public void SnapToStandardSize_JustOver48_PicksNextStandardSize() + { + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 100, 50)); + + var result = plate.SnapToStandardSize(); + + Assert.Equal("60x120", result.MatchedLabel); + Assert.Equal(120, plate.Size.Length); // X long + Assert.Equal(60, plate.Size.Width); + } + + [Fact] + public void SnapToStandardSize_EmptyPlate_DoesNotModifySize() + { + var plate = new Plate(60, 120); + + var result = plate.SnapToStandardSize(); + + Assert.Null(result.MatchedLabel); + Assert.Equal(60, plate.Size.Width); + Assert.Equal(120, plate.Size.Length); + } + + [Fact] + public void SnapToStandardSize_MultipleParts_UsesCombinedEnvelope() + { + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 30, 40)); + plate.Parts.Add(MakeRectPart(30, 0, 30, 40)); // combined X-extent = 60 + plate.Parts.Add(MakeRectPart(0, 40, 60, 60)); // combined extent = 60 x 100 + + var result = plate.SnapToStandardSize(); + + // 60 x 100 fits 60x120 standard sheet, Y is the long axis. + Assert.Equal("60x120", result.MatchedLabel); + Assert.Equal(60, plate.Size.Length); // X + Assert.Equal(120, plate.Size.Width); // Y long + } +} diff --git a/OpenNest.Tests/Shapes/PlateSizesTests.cs b/OpenNest.Tests/Shapes/PlateSizesTests.cs new file mode 100644 index 0000000..7b3e0a7 --- /dev/null +++ b/OpenNest.Tests/Shapes/PlateSizesTests.cs @@ -0,0 +1,311 @@ +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( + () => PlateSizes.Recommend(System.Array.Empty())); + } + + [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); + } +} diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index 845ea22..f26abcc 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -7,6 +7,7 @@ using OpenNest.Engine.Sequencing; using OpenNest.IO; using OpenNest.Math; using OpenNest.Properties; +using OpenNest.Shapes; using System; using System.ComponentModel; using System.Diagnostics; @@ -453,7 +454,11 @@ namespace OpenNest.Forms public void ResizePlateToFitParts() { - PlateView.Plate.AutoSize(Settings.Default.AutoSizePlateFactor); + var options = new PlateSizeOptions + { + SnapIncrement = Settings.Default.AutoSizePlateFactor, + }; + PlateView.Plate.SnapToStandardSize(options); PlateView.ZoomToPlate(); PlateView.Refresh(); UpdatePlateList();