diff --git a/OpenNest.Engine/HorizontalRemnantEngine.cs b/OpenNest.Engine/HorizontalRemnantEngine.cs
new file mode 100644
index 0000000..1cadd19
--- /dev/null
+++ b/OpenNest.Engine/HorizontalRemnantEngine.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using OpenNest.Engine;
+using OpenNest.Engine.Fill;
+using OpenNest.Geometry;
+using OpenNest.Math;
+
+namespace OpenNest
+{
+ ///
+ /// Optimizes for the largest top-side horizontal drop.
+ /// Scores by count first, then minimizes Y-extent.
+ /// Prefers vertical nest direction and angles that keep parts narrow in Y.
+ ///
+ public class HorizontalRemnantEngine : DefaultNestEngine
+ {
+ public HorizontalRemnantEngine(Plate plate) : base(plate) { }
+
+ public override string Name => "Horizontal Remnant";
+
+ public override string Description => "Optimizes for largest top-side horizontal drop";
+
+ protected override IFillComparer CreateComparer() => new HorizontalRemnantComparer();
+
+ public override NestDirection? PreferredDirection => NestDirection.Vertical;
+
+ public override List BuildAngles(NestItem item, double bestRotation, Box workArea)
+ {
+ var baseAngles = new List { bestRotation, bestRotation + Angle.HalfPI };
+ baseAngles.Sort((a, b) => RotatedHeight(item, a).CompareTo(RotatedHeight(item, b)));
+ return baseAngles;
+ }
+
+ private static double RotatedHeight(NestItem item, double angle)
+ {
+ var bb = item.Drawing.Program.BoundingBox();
+ var cos = System.Math.Abs(System.Math.Cos(angle));
+ var sin = System.Math.Abs(System.Math.Sin(angle));
+ return bb.Length * cos + bb.Width * sin;
+ }
+ }
+}
diff --git a/OpenNest.Engine/NestEngineRegistry.cs b/OpenNest.Engine/NestEngineRegistry.cs
index 2a7e9da..8363646 100644
--- a/OpenNest.Engine/NestEngineRegistry.cs
+++ b/OpenNest.Engine/NestEngineRegistry.cs
@@ -24,6 +24,14 @@ namespace OpenNest
Register("NFP",
"NFP-based mixed-part nesting with simulated annealing",
plate => new NfpNestEngine(plate));
+
+ Register("Vertical Remnant",
+ "Optimizes for largest right-side vertical drop",
+ plate => new VerticalRemnantEngine(plate));
+
+ Register("Horizontal Remnant",
+ "Optimizes for largest top-side horizontal drop",
+ plate => new HorizontalRemnantEngine(plate));
}
public static IReadOnlyList AvailableEngines => engines;
diff --git a/OpenNest.Engine/VerticalRemnantEngine.cs b/OpenNest.Engine/VerticalRemnantEngine.cs
new file mode 100644
index 0000000..1be136b
--- /dev/null
+++ b/OpenNest.Engine/VerticalRemnantEngine.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using OpenNest.Engine;
+using OpenNest.Engine.Fill;
+using OpenNest.Geometry;
+using OpenNest.Math;
+
+namespace OpenNest
+{
+ ///
+ /// Optimizes for the largest right-side vertical drop.
+ /// Scores by count first, then minimizes X-extent.
+ /// Prefers horizontal nest direction and angles that keep parts narrow in X.
+ ///
+ public class VerticalRemnantEngine : DefaultNestEngine
+ {
+ public VerticalRemnantEngine(Plate plate) : base(plate) { }
+
+ public override string Name => "Vertical Remnant";
+
+ public override string Description => "Optimizes for largest right-side vertical drop";
+
+ protected override IFillComparer CreateComparer() => new VerticalRemnantComparer();
+
+ public override NestDirection? PreferredDirection => NestDirection.Horizontal;
+
+ public override List BuildAngles(NestItem item, double bestRotation, Box workArea)
+ {
+ var baseAngles = new List { bestRotation, bestRotation + Angle.HalfPI };
+ baseAngles.Sort((a, b) => RotatedWidth(item, a).CompareTo(RotatedWidth(item, b)));
+ return baseAngles;
+ }
+
+ private static double RotatedWidth(NestItem item, double angle)
+ {
+ var bb = item.Drawing.Program.BoundingBox();
+ var cos = System.Math.Abs(System.Math.Cos(angle));
+ var sin = System.Math.Abs(System.Math.Sin(angle));
+ return bb.Width * cos + bb.Length * sin;
+ }
+ }
+}
diff --git a/OpenNest.Tests/RemnantEngineTests.cs b/OpenNest.Tests/RemnantEngineTests.cs
new file mode 100644
index 0000000..208a6a2
--- /dev/null
+++ b/OpenNest.Tests/RemnantEngineTests.cs
@@ -0,0 +1,92 @@
+using OpenNest.Engine;
+using OpenNest.Engine.Fill;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests;
+
+public class RemnantEngineTests
+{
+ private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
+ {
+ var pgm = new OpenNest.CNC.Program();
+ pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
+ pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
+ pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
+ pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
+ pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
+ return new Drawing(name, pgm);
+ }
+
+ [Fact]
+ public void VerticalRemnantEngine_UsesVerticalRemnantComparer()
+ {
+ var plate = new Plate(60, 120);
+ var engine = new VerticalRemnantEngine(plate);
+ Assert.Equal("Vertical Remnant", engine.Name);
+ Assert.Equal(NestDirection.Horizontal, engine.PreferredDirection);
+ }
+
+ [Fact]
+ public void HorizontalRemnantEngine_UsesHorizontalRemnantComparer()
+ {
+ var plate = new Plate(60, 120);
+ var engine = new HorizontalRemnantEngine(plate);
+ Assert.Equal("Horizontal Remnant", engine.Name);
+ Assert.Equal(NestDirection.Vertical, engine.PreferredDirection);
+ }
+
+ [Fact]
+ public void VerticalRemnantEngine_Fill_ProducesResults()
+ {
+ var plate = new Plate(60, 120);
+ var engine = new VerticalRemnantEngine(plate);
+ var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
+
+ var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
+
+ Assert.True(parts.Count > 0, "VerticalRemnantEngine should fill parts");
+ }
+
+ [Fact]
+ public void HorizontalRemnantEngine_Fill_ProducesResults()
+ {
+ var plate = new Plate(60, 120);
+ var engine = new HorizontalRemnantEngine(plate);
+ var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
+
+ var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
+
+ Assert.True(parts.Count > 0, "HorizontalRemnantEngine should fill parts");
+ }
+
+ [Fact]
+ public void Registry_ContainsBothRemnantEngines()
+ {
+ var names = NestEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
+ Assert.Contains("Vertical Remnant", names);
+ Assert.Contains("Horizontal Remnant", names);
+ }
+
+ [Fact]
+ public void VerticalRemnantEngine_ProducesTighterXExtent_ThanDefault()
+ {
+ var plate = new Plate(60, 120);
+ var drawing = MakeRectDrawing(20, 10);
+ var item = new NestItem { Drawing = drawing };
+
+ var defaultEngine = new DefaultNestEngine(plate);
+ var remnantEngine = new VerticalRemnantEngine(plate);
+
+ var defaultParts = defaultEngine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
+ var remnantParts = remnantEngine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
+
+ Assert.True(defaultParts.Count > 0);
+ Assert.True(remnantParts.Count > 0);
+
+ var defaultXExtent = defaultParts.Max(p => p.BoundingBox.Right) - defaultParts.Min(p => p.BoundingBox.Left);
+ var remnantXExtent = remnantParts.Max(p => p.BoundingBox.Right) - remnantParts.Min(p => p.BoundingBox.Left);
+
+ Assert.True(remnantXExtent <= defaultXExtent + 0.01,
+ $"Remnant X-extent ({remnantXExtent:F1}) should be <= default ({defaultXExtent:F1})");
+ }
+}