feat: engine-specific TrimAxis and rename ShrinkAxis.Height to Length

Make quantity trimming direction-aware: DefaultNestEngine uses TrimAxis
(virtual property on NestEngineBase) so HorizontalRemnantEngine removes
topmost parts instead of rightmost. Rename ShrinkAxis.Height → Length
for consistency with Width/Length naming used throughout the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 19:43:29 -04:00
parent 2f19f47a85
commit 07d6f08e8b
6 changed files with 14 additions and 10 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ namespace OpenNest
var best = context.CurrentBest ?? new List<Part>(); var best = context.CurrentBest ?? new List<Part>();
if (item.Quantity > 0 && best.Count > item.Quantity) if (item.Quantity > 0 && best.Count > item.Quantity)
best = ShrinkFiller.TrimToCount(best, item.Quantity, ShrinkAxis.Width); best = ShrinkFiller.TrimToCount(best, item.Quantity, TrimAxis);
ReportProgress(progress, new ProgressReport ReportProgress(progress, new ProgressReport
{ {
@@ -18,7 +18,7 @@ namespace OpenNest.Engine.Fill
/// <summary> /// <summary>
/// Composes <see cref="RemnantFiller"/> and <see cref="ShrinkFiller"/> with /// Composes <see cref="RemnantFiller"/> and <see cref="ShrinkFiller"/> with
/// dual-direction shrink selection. Wraps the caller's fill function in a /// dual-direction shrink selection. Wraps the caller's fill function in a
/// closure that tries both <see cref="ShrinkAxis.Height"/> and /// closure that tries both <see cref="ShrinkAxis.Length"/> and
/// <see cref="ShrinkAxis.Width"/>, picks the better <see cref="FillScore"/>, /// <see cref="ShrinkAxis.Width"/>, picks the better <see cref="FillScore"/>,
/// and passes the wrapper to <see cref="RemnantFiller.FillItems"/>. /// and passes the wrapper to <see cref="RemnantFiller.FillItems"/>.
/// </summary> /// </summary>
@@ -85,7 +85,7 @@ namespace OpenNest.Engine.Fill
ShrinkResult widthResult = null; ShrinkResult widthResult = null;
Parallel.Invoke( Parallel.Invoke(
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token, () => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Length, token,
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar), targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar),
() => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token, () => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token,
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar) targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar)
+3 -3
View File
@@ -7,7 +7,7 @@ using System.Threading;
namespace OpenNest.Engine.Fill namespace OpenNest.Engine.Fill
{ {
public enum ShrinkAxis { Width, Height } public enum ShrinkAxis { Width, Length }
public class ShrinkResult public class ShrinkResult
{ {
@@ -101,7 +101,7 @@ namespace OpenNest.Engine.Fill
if (bbox.Width <= 0 || bbox.Length <= 0) if (bbox.Width <= 0 || bbox.Length <= 0)
return box; return box;
var maxDim = axis == ShrinkAxis.Height ? box.Length : box.Width; var maxDim = axis == ShrinkAxis.Length ? box.Length : box.Width;
// Use FillBestFit for a fast, accurate rectangle count on the full box. // Use FillBestFit for a fast, accurate rectangle count on the full box.
var bin = new Bin { Size = new Size(box.Width, box.Length) }; var bin = new Bin { Size = new Size(box.Width, box.Length) };
@@ -121,7 +121,7 @@ namespace OpenNest.Engine.Fill
if (estimate <= 0 || estimate >= maxDim) if (estimate <= 0 || estimate >= maxDim)
return box; return box;
return axis == ShrinkAxis.Height return axis == ShrinkAxis.Length
? new Box(box.X, box.Y, box.Width, estimate) ? new Box(box.X, box.Y, box.Width, estimate)
: new Box(box.X, box.Y, estimate, box.Length); : new Box(box.X, box.Y, estimate, box.Length);
} }
@@ -24,6 +24,8 @@ namespace OpenNest
public override NestDirection? PreferredDirection => NestDirection.Vertical; public override NestDirection? PreferredDirection => NestDirection.Vertical;
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
public override List<double> BuildAngles(NestItem item, double bestRotation, Box workArea) public override List<double> BuildAngles(NestItem item, double bestRotation, Box workArea)
{ {
var baseAngles = new List<double> { bestRotation, bestRotation + Angle.HalfPI }; var baseAngles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
+2
View File
@@ -43,6 +43,8 @@ namespace OpenNest
public virtual NestDirection? PreferredDirection => null; public virtual NestDirection? PreferredDirection => null;
public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width;
public virtual List<double> BuildAngles(NestItem item, double bestRotation, Box workArea) public virtual List<double> BuildAngles(NestItem item, double bestRotation, Box workArea)
{ {
return new List<double> { bestRotation, bestRotation + OpenNest.Math.Angle.HalfPI }; return new List<double> { bestRotation, bestRotation + OpenNest.Math.Angle.HalfPI };
+4 -4
View File
@@ -30,7 +30,7 @@ public class ShrinkFillerTests
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None); return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
}; };
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Height); var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Length);
Assert.NotNull(result); Assert.NotNull(result);
Assert.True(result.Parts.Count > 0); Assert.True(result.Parts.Count > 0);
@@ -73,7 +73,7 @@ public class ShrinkFillerTests
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) }; new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0,
ShrinkAxis.Height, token: cts.Token); ShrinkAxis.Length, token: cts.Token);
Assert.NotNull(result); Assert.NotNull(result);
Assert.True(result.Parts.Count > 0); Assert.True(result.Parts.Count > 0);
@@ -97,7 +97,7 @@ public class ShrinkFillerTests
} }
[Fact] [Fact]
public void TrimToCount_Height_KeepsPartsNearestToOrigin() public void TrimToCount_Length_KeepsPartsNearestToOrigin()
{ {
var parts = new List<Part> var parts = new List<Part>
{ {
@@ -107,7 +107,7 @@ public class ShrinkFillerTests
TestHelpers.MakePartAt(0, 30, 5), // Top = 35 TestHelpers.MakePartAt(0, 30, 5), // Top = 35
}; };
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Height); var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Length);
Assert.Equal(2, trimmed.Count); Assert.Equal(2, trimmed.Count);
Assert.True(trimmed.All(p => p.BoundingBox.Top <= 15)); Assert.True(trimmed.All(p => p.BoundingBox.Top <= 15));