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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-04 20:45:33 -05:00
1 parent 7ea21f3afe
commit 4dae54c629
8 files changed
+109 -651

No files matched your search

-60
View File
@@ -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
);
}
/// <summary>
/// Sizes the plate using the <see cref="PlateSizes"/> 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.
/// </summary>
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;
}
/// <summary>
/// Gets the area of the top surface of the plate: the material inside its
/// <see cref="Outline"/> when it has one, otherwise the whole rectangle.
+1 -226
View File
@@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Shapes
{
/// <summary>
/// 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.
/// </summary>
@@ -62,227 +59,5 @@ namespace OpenNest.Shapes
entry = default;
return false;
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Recommends a plate size for the envelope of the given boxes.
/// </summary>
public static PlateSizeResult Recommend(
IEnumerable<Box> 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);
}
/// <summary>
/// Recommends a plate size for a (width, length) pair.
/// Inputs are treated as orientation-independent.
/// </summary>
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<Entry> BuildCatalog(IReadOnlyList<string> allowedSizes)
{
if (allowedSizes == null || allowedSizes.Count == 0)
return All;
var result = new List<Entry>(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<Entry> 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
{
/// <summary>
/// If the margin-adjusted bounding box fits within MinSheetWidth x MinSheetLength
/// the result is snapped to <see cref="SnapIncrement"/> instead of routed to a
/// standard sheet. Default 48" x 48".
/// </summary>
public double MinSheetWidth { get; set; } = 48;
public double MinSheetLength { get; set; } = 48;
/// <summary>
/// Increment used for below-threshold rounding and oversize fallback. Default 1".
/// </summary>
public double SnapIncrement { get; set; } = 1.0;
/// <summary>
/// Extra clearance added to each side of the bounding box before matching.
/// </summary>
public double Margin { get; set; } = 0;
/// <summary>
/// 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").
/// </summary>
public IReadOnlyList<string> AllowedSizes { get; set; }
/// <summary>
/// Tiebreaker when multiple sheets can contain the bounding box.
/// </summary>
public PlateSizeSelection Selection { get; set; } = PlateSizeSelection.SmallestArea;
}
public enum PlateSizeSelection
{
/// <summary>Pick the cheapest sheet that contains the bbox (smallest area).</summary>
SmallestArea,
/// <summary>Prefer narrower-width sheets (e.g. 48-wide before 60-wide).</summary>
NarrowestFirst,
}
}
+106
View File
@@ -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
}
}
@@ -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
}
}
-238
View File
@@ -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<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);
}
}
-2
View File
@@ -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]
+1 -6
View File
@@ -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();
+1 -1
View File
@@ -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 |