From 9f335ea111d7cd60bb6dac5c9445ad4788339249 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 25 Oct 2026 22:53:28 -0400 Subject: [PATCH] refactor(engine): fold remnant fill settings into one filler policy Fill, Vertical Remnant and Horizontal Remnant are now three values of an internal PlateFillPolicy that DefaultPlateFiller takes (Fill by default), replacing RemnantPlateFiller and RemnantFillPolicy. The policy holds the comparer factory, preferred direction, trim axis and angle mode; the remnant angle mode still replaces the angle builder outright, and the public FillPolicy that strategies read is unchanged. Layout-neutral: the fill-stack characterization pins pass unchanged. Co-Authored-By: Claude Opus 5.5 --- .../Jobs/PlateFillerContractTests.cs | 4 +- .../Placement/Fillers/DefaultPlateFiller.cs | 19 ++++-- ...mnantPlateFiller.cs => PlateFillPolicy.cs} | 63 ++++++++++--------- .../Placement/Fillers/StripPlateFiller.cs | 4 +- .../Jobs/Placement/PlateFillService.cs | 4 +- .../Jobs/Placement/RemnantPlateNester.cs | 8 +-- OpenNest.Tests/Engine/RemnantEngineTests.cs | 10 +-- 7 files changed, 62 insertions(+), 50 deletions(-) rename OpenNest.Engine/Jobs/Placement/Fillers/{RemnantPlateFiller.cs => PlateFillPolicy.cs} (54%) diff --git a/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs b/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs index cfd71629..d83196e8 100644 --- a/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs +++ b/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs @@ -354,8 +354,8 @@ public class PlateFillerContractTests private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch { "Fill" => new DefaultPlateFiller(plate), - "Vertical Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical), - "Horizontal Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal), + "Vertical Remnant" => new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant), + "Horizontal Remnant" => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant), _ => throw new ArgumentOutOfRangeException(nameof(strategy)), }; diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs index a8f7115e..d1db0a5f 100644 --- a/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs +++ b/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs @@ -16,11 +16,19 @@ namespace OpenNest.Engine.Jobs.Placement.Fillers; internal class DefaultPlateFiller : PlateFillerBase { private readonly AngleCandidateBuilder angleBuilder = new(); + private readonly PlateFillPolicy policy; - internal DefaultPlateFiller(Plate plate) - : base(plate) { } + internal DefaultPlateFiller(Plate plate, PlateFillPolicy policy = null) + : base(plate) + { + this.policy = policy ?? PlateFillPolicy.Fill; + } - protected override IFillComparer CreateComparer() => new DefaultFillComparer(); + protected override IFillComparer CreateComparer() => policy.CreateComparer(); + + public override NestDirection? PreferredDirection => policy.PreferredDirection; + + public override ShrinkAxis TrimAxis => policy.TrimAxis; internal bool ForceFullAngleSweep { @@ -39,7 +47,10 @@ internal class DefaultPlateFiller : PlateFillerBase NestItem item, ClassificationResult classification, Box workArea - ) => angleBuilder.Build(item, classification, workArea); + ) => + policy.UsesAngleBuilder + ? angleBuilder.Build(item, classification, workArea) + : policy.BuildFixedAngles(item, classification); protected override void RecordProductiveAngles(List angleResults) { diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/RemnantPlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillPolicy.cs similarity index 54% rename from OpenNest.Engine/Jobs/Placement/Fillers/RemnantPlateFiller.cs rename to OpenNest.Engine/Jobs/Placement/Fillers/PlateFillPolicy.cs index f34b13c9..2d874574 100644 --- a/OpenNest.Engine/Jobs/Placement/Fillers/RemnantPlateFiller.cs +++ b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillPolicy.cs @@ -2,42 +2,23 @@ using System; using System.Collections.Generic; using OpenNest.Engine; using OpenNest.Engine.Fill; -using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Engine.Jobs.Placement.Fillers; -internal class RemnantPlateFiller : DefaultPlateFiller -{ - private readonly RemnantFillPolicy policy; - - internal RemnantPlateFiller(Plate plate, RemnantFillPolicy policy) - : base(plate) - { - this.policy = policy; - } - - protected override IFillComparer CreateComparer() => policy.CreateComparer(); - - public override NestDirection? PreferredDirection => policy.PreferredDirection; - - public override ShrinkAxis TrimAxis => policy.TrimAxis; - - public override List BuildAngles( - NestItem item, - ClassificationResult classification, - Box workArea - ) => policy.BuildAngles(item, classification); -} - -internal sealed class RemnantFillPolicy +/// +/// Filler-only settings that distinguish Fill from the remnant fills: how candidates are ranked, +/// the preferred direction, the axis surplus parts are trimmed from, and how angles are chosen. +/// Strategies see only the the filler builds from it. +/// +internal sealed class PlateFillPolicy { private readonly Func comparerFactory; private readonly Func angleExtent; - private RemnantFillPolicy( + private PlateFillPolicy( Func comparerFactory, - NestDirection preferredDirection, + NestDirection? preferredDirection, ShrinkAxis trimAxis, Func angleExtent ) @@ -48,27 +29,47 @@ internal sealed class RemnantFillPolicy this.angleExtent = angleExtent; } - internal static RemnantFillPolicy Vertical { get; } = new( + /// Plain fill: default ranking and the filler's learned angle candidates. + internal static PlateFillPolicy Fill { get; } = new( + () => new DefaultFillComparer(), + null, + ShrinkAxis.Width, + null + ); + + /// Vertical remnant: minimize X-extent, prefer horizontal placement. + internal static PlateFillPolicy VerticalRemnant { get; } = new( () => new VerticalRemnantComparer(), NestDirection.Horizontal, ShrinkAxis.Width, RotatedWidth ); - internal static RemnantFillPolicy Horizontal { get; } = new( + /// Horizontal remnant: minimize Y-extent, prefer vertical placement. + internal static PlateFillPolicy HorizontalRemnant { get; } = new( () => new HorizontalRemnantComparer(), NestDirection.Vertical, ShrinkAxis.Length, RotatedHeight ); - internal NestDirection PreferredDirection { get; } + internal NestDirection? PreferredDirection { get; } internal ShrinkAxis TrimAxis { get; } + /// + /// True when the filler's chooses the angles; false when + /// replaces it entirely. + /// + internal bool UsesAngleBuilder => angleExtent == null; + internal IFillComparer CreateComparer() => comparerFactory(); - internal List BuildAngles(NestItem item, ClassificationResult classification) + /// + /// The remnant angles: the primary angle and its quarter turn, narrowest extent first. No + /// circle case, sweep or learned angles, and the item's rotation range is ignored. + /// + internal List BuildFixedAngles(NestItem item, ClassificationResult classification) { var baseAngles = new List { diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs index 4a589dd2..d031b6d0 100644 --- a/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs +++ b/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs @@ -68,12 +68,12 @@ internal class StripPlateFiller : PlateFillerBase { Func> heightFillFunc = (item, box) => { - var inner = new RemnantPlateFiller(Plate, RemnantFillPolicy.Horizontal); + var inner = new DefaultPlateFiller(Plate, PlateFillPolicy.HorizontalRemnant); return inner.Fill(item, box, progress, token); }; Func> widthFillFunc = (item, box) => { - var inner = new RemnantPlateFiller(Plate, RemnantFillPolicy.Vertical); + var inner = new DefaultPlateFiller(Plate, PlateFillPolicy.VerticalRemnant); return inner.Fill(item, box, progress, token); }; diff --git a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs index f4ddcb7c..82da30c9 100644 --- a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs +++ b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs @@ -435,8 +435,8 @@ public static class PlateFillService { "Fill" => new DefaultPlateFiller(plate), "Strip" => new StripPlateFiller(plate), - "Vertical Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical), - _ => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal), + "Vertical Remnant" => new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant), + _ => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant), }; } diff --git a/OpenNest.Engine/Jobs/Placement/RemnantPlateNester.cs b/OpenNest.Engine/Jobs/Placement/RemnantPlateNester.cs index 876eb8c8..3b3bc6ec 100644 --- a/OpenNest.Engine/Jobs/Placement/RemnantPlateNester.cs +++ b/OpenNest.Engine/Jobs/Placement/RemnantPlateNester.cs @@ -6,7 +6,7 @@ namespace OpenNest.Engine.Jobs.Placement; /// /// Built-in remnant placement strategy for the whole-job runner. Fills one candidate per trial with -/// a for the vertical or horizontal remnant policy. Placement is +/// a with the vertical or horizontal remnant . Placement is /// 's with that filler: the same fresh private plate per trial and /// the same run-scoped identity, progress, and accounting boundaries. /// @@ -18,7 +18,7 @@ public sealed class RemnantPlateNester : IPlateNester { private readonly DefaultPlateNester nester; - internal RemnantPlateNester(Func fillerFactory) + internal RemnantPlateNester(Func fillerFactory) { nester = new DefaultPlateNester( fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory))); @@ -26,11 +26,11 @@ public sealed class RemnantPlateNester : IPlateNester /// Vertical-remnant policy: minimize X-extent, prefer horizontal placement. internal static RemnantPlateNester Vertical() => - new(plate => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical)); + new(plate => new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant)); /// Horizontal-remnant policy: minimize Y-extent, prefer vertical placement. internal static RemnantPlateNester Horizontal() => - new(plate => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal)); + new(plate => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant)); public PlateCandidate Place( PlatePlacementRequest request, diff --git a/OpenNest.Tests/Engine/RemnantEngineTests.cs b/OpenNest.Tests/Engine/RemnantEngineTests.cs index dd0167e9..d685cbfa 100644 --- a/OpenNest.Tests/Engine/RemnantEngineTests.cs +++ b/OpenNest.Tests/Engine/RemnantEngineTests.cs @@ -23,7 +23,7 @@ public class RemnantEngineTests public void VerticalRemnantPlateFiller_UsesHorizontalPreferredDirection() { var plate = new Plate(60, 120); - var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical); + var filler = new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant); Assert.Equal(NestDirection.Horizontal, filler.PreferredDirection); } @@ -31,7 +31,7 @@ public class RemnantEngineTests public void HorizontalRemnantPlateFiller_UsesVerticalPreferredDirection() { var plate = new Plate(60, 120); - var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal); + var filler = new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant); Assert.Equal(NestDirection.Vertical, filler.PreferredDirection); } @@ -39,7 +39,7 @@ public class RemnantEngineTests public void VerticalRemnantPlateFiller_Fill_ProducesResults() { var plate = new Plate(60, 120); - var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical); + var filler = new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant); var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; var parts = filler.Fill( @@ -56,7 +56,7 @@ public class RemnantEngineTests public void HorizontalRemnantPlateFiller_Fill_ProducesResults() { var plate = new Plate(60, 120); - var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal); + var filler = new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant); var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; var parts = filler.Fill( @@ -84,7 +84,7 @@ public class RemnantEngineTests var item = new NestItem { Drawing = drawing }; var defaultFiller = new DefaultPlateFiller(plate); - var remnantFiller = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical); + var remnantFiller = new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant); var defaultParts = defaultFiller.Fill( item,