From 4dae54c62920aaec4d5c2222e5b587ec07a86bfe Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 4 Nov 2026 20:45:33 -0500 Subject: [PATCH] feat(plates): resize plate to fit parts at any size Resize to Fit snapped layouts larger than 48x48 to a hardcoded list of inch mill sheets, so a plate already on a full sheet could not shrink to its parts, and the cutoff ignored nest units and the machine's stock sizes. It now rounds the parts' extent up to the auto-size plate factor at every size through Plate.AutoSize. Removes Plate.SnapToStandardSize and PlateSizes.Recommend with its options; the sheet list and label lookup stay for Machine Config. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Core/Plate.cs | 60 ----- OpenNest.Core/Shapes/PlateSizes.cs | 227 +---------------- OpenNest.Tests/PlateAutoSizeTests.cs | 106 ++++++++ .../PlateSnapToStandardSizeTests.cs | 118 --------- OpenNest.Tests/Shapes/PlateSizesTests.cs | 238 ------------------ OpenNest.Tests/SheetOutlineTests.cs | 2 - OpenNest/Forms/EditNestForm.cs | 7 +- docs/irregular-stock.md | 2 +- 8 files changed, 109 insertions(+), 651 deletions(-) create mode 100644 OpenNest.Tests/PlateAutoSizeTests.cs delete mode 100644 OpenNest.Tests/PlateSnapToStandardSizeTests.cs diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index 5d55624e..378470b0 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -5,7 +5,6 @@ using System.Linq; using OpenNest.Collections; using OpenNest.Geometry; using OpenNest.Math; -using OpenNest.Shapes; using OpenNest.Support; namespace OpenNest @@ -985,65 +984,6 @@ namespace OpenNest ); } - /// - /// 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 || Outline != null) - 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: the material inside its /// when it has one, otherwise the whole rectangle. diff --git a/OpenNest.Core/Shapes/PlateSizes.cs b/OpenNest.Core/Shapes/PlateSizes.cs index d8e1cbb9..fc8a7165 100644 --- a/OpenNest.Core/Shapes/PlateSizes.cs +++ b/OpenNest.Core/Shapes/PlateSizes.cs @@ -1,13 +1,10 @@ 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 + /// Catalog of standard mill sheet sizes (inches). Uses the project-wide /// (Width, Length) convention where Width is the short dimension and /// Length is the long dimension. /// @@ -62,227 +59,5 @@ namespace OpenNest.Shapes 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/PlateAutoSizeTests.cs b/OpenNest.Tests/PlateAutoSizeTests.cs new file mode 100644 index 00000000..85c14686 --- /dev/null +++ b/OpenNest.Tests/PlateAutoSizeTests.cs @@ -0,0 +1,106 @@ +using OpenNest.CNC; +using OpenNest.Geometry; + +namespace OpenNest.Tests; + +public class PlateAutoSizeTests +{ + 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 AutoSize_SmallParts_RoundsUpToFactor() + { + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 10.3, 20.7)); + + plate.AutoSize(0.25); + + Assert.Equal(10.5, plate.Size.Length, 4); // X axis + Assert.Equal(20.75, plate.Size.Width, 4); // Y axis + } + + [Fact] + public void AutoSize_ZeroFactor_FitsExactly() + { + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 10.3, 20.7)); + + plate.AutoSize(0); + + Assert.Equal(10.3, plate.Size.Length, 4); + Assert.Equal(20.7, plate.Size.Width, 4); + } + + [Fact] + public void AutoSize_LargerThanFullSheet_RoundsUpInsteadOfPickingAStandardSheet() + { + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 100, 50)); + + plate.AutoSize(); + + Assert.Equal(100, plate.Size.Length); + Assert.Equal(50, plate.Size.Width); + } + + [Fact] + public void AutoSize_FullSheet_ShrinksToParts() + { + var plate = new Plate(60, 120); + plate.Parts.Add(MakeRectPart(0, 0, 100.2, 55.6)); + + plate.AutoSize(); + + Assert.Equal(101, plate.Size.Length); + Assert.Equal(56, plate.Size.Width); + } + + [Fact] + public void AutoSize_IncludesEdgeSpacing() + { + var plate = new Plate(200, 200) { EdgeSpacing = new Spacing(0, 0, 0.5, 0.5) }; + plate.Parts.Add(MakeRectPart(0, 0, 10, 20)); + + plate.AutoSize(0.25); + + Assert.Equal(10.5, plate.Size.Length, 4); + Assert.Equal(20.5, plate.Size.Width, 4); + } + + [Fact] + public void AutoSize_EmptyPlate_DoesNotModifySize() + { + var plate = new Plate(60, 120); + + plate.AutoSize(); + + Assert.Equal(60, plate.Size.Width); + Assert.Equal(120, plate.Size.Length); + } + + [Fact] + public void AutoSize_MultipleParts_UsesCombinedEnvelope() + { + var plate = new Plate(200, 200); + plate.Parts.Add(MakeRectPart(0, 0, 30, 40)); + plate.Parts.Add(MakeRectPart(30, 0, 30, 40)); + plate.Parts.Add(MakeRectPart(0, 40, 60, 60)); // combined extent = 60 x 100 + + plate.AutoSize(); + + Assert.Equal(60, plate.Size.Length); // X + Assert.Equal(100, plate.Size.Width); // Y + } +} diff --git a/OpenNest.Tests/PlateSnapToStandardSizeTests.cs b/OpenNest.Tests/PlateSnapToStandardSizeTests.cs deleted file mode 100644 index 7112c0f5..00000000 --- a/OpenNest.Tests/PlateSnapToStandardSizeTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -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 index e05675c2..4b8691c2 100644 --- a/OpenNest.Tests/Shapes/PlateSizesTests.cs +++ b/OpenNest.Tests/Shapes/PlateSizesTests.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Linq; -using OpenNest.Geometry; using OpenNest.Shapes; namespace OpenNest.Tests.Shapes; @@ -13,13 +11,6 @@ public class PlateSizesTests 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() { @@ -76,233 +67,4 @@ public class PlateSizesTests { 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.Tests/SheetOutlineTests.cs b/OpenNest.Tests/SheetOutlineTests.cs index c3986dc8..25aed606 100644 --- a/OpenNest.Tests/SheetOutlineTests.cs +++ b/OpenNest.Tests/SheetOutlineTests.cs @@ -179,8 +179,6 @@ public class SheetOutlineTests plate.AutoSize(); Assert.Equal(96, plate.Size.Width); Assert.NotNull(plate.Outline); - Assert.Equal(default, plate.SnapToStandardSize()); - Assert.NotNull(plate.Outline); } [Fact] diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index 7903ba98..57215c39 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -21,7 +21,6 @@ using OpenNest.IO; using OpenNest.Math; using OpenNest.Properties; using OpenNest.Reporting; -using OpenNest.Shapes; using Timer = System.Timers.Timer; namespace OpenNest.Forms @@ -962,11 +961,7 @@ namespace OpenNest.Forms public void ResizePlateToFitParts() { - var options = new PlateSizeOptions - { - SnapIncrement = Settings.Default.AutoSizePlateFactor, - }; - RecordChange("Resize Plate", () => PlateView.Plate.SnapToStandardSize(options)); + RecordChange("Resize Plate", () => PlateView.Plate.AutoSize(Settings.Default.AutoSizePlateFactor)); PlateView.ZoomToPlate(); PlateView.Refresh(); UpdatePlateList(); diff --git a/docs/irregular-stock.md b/docs/irregular-stock.md index eff7e57d..ad1f8ce1 100644 --- a/docs/irregular-stock.md +++ b/docs/irregular-stock.md @@ -38,7 +38,7 @@ Drawing an outline in the plate view and taking the leftover of a cut plate are | Salvage and remnants | No salvage credit and no `expectedRemnant`: no rectangular edge strip is guaranteed inside an outline | | Manual cut-offs | A line is cut only where it crosses material: each crossing is a segment of its own, started at the material's edge and overtraveled past the far one, within the user's limits and the usual part clearance. The head-locked overcut applies only to a span that reaches the bounding box's far edge | | Automatic cut-offs | Refused: `AutomaticCutOffPlanner` rejects an outlined plate ("Automatic cut-offs need a rectangular sheet") | -| AutoSize, Snap to Standard Size | Leave an outlined plate unchanged: the outline is a particular piece of material | +| AutoSize (Resize Plate to Fit Parts) | Leaves an outlined plate unchanged: the outline is a particular piece of material | | Rotate 90/180 | Turn the outline with the sheet | | Repeated layouts, clamp-removal recovery, stock matching | An outlined stock is never the same stock as its rectangle |