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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-25 23:44:51 -04:00
1 parent 0c7ad5aefb
commit 9f335ea111
7 files changed
+62 -50

No files matched your search

@@ -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)),
};
@@ -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<AngleResult> angleResults)
{
@@ -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<double> BuildAngles(
NestItem item,
ClassificationResult classification,
Box workArea
) => policy.BuildAngles(item, classification);
}
internal sealed class RemnantFillPolicy
/// <summary>
/// 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 <see cref="Strategies.FillPolicy"/> the filler builds from it.
/// </summary>
internal sealed class PlateFillPolicy
{
private readonly Func<IFillComparer> comparerFactory;
private readonly Func<NestItem, double, double> angleExtent;
private RemnantFillPolicy(
private PlateFillPolicy(
Func<IFillComparer> comparerFactory,
NestDirection preferredDirection,
NestDirection? preferredDirection,
ShrinkAxis trimAxis,
Func<NestItem, double, double> angleExtent
)
@@ -48,27 +29,47 @@ internal sealed class RemnantFillPolicy
this.angleExtent = angleExtent;
}
internal static RemnantFillPolicy Vertical { get; } = new(
/// <summary>Plain fill: default ranking and the filler's learned angle candidates.</summary>
internal static PlateFillPolicy Fill { get; } = new(
() => new DefaultFillComparer(),
null,
ShrinkAxis.Width,
null
);
/// <summary>Vertical remnant: minimize X-extent, prefer horizontal placement.</summary>
internal static PlateFillPolicy VerticalRemnant { get; } = new(
() => new VerticalRemnantComparer(),
NestDirection.Horizontal,
ShrinkAxis.Width,
RotatedWidth
);
internal static RemnantFillPolicy Horizontal { get; } = new(
/// <summary>Horizontal remnant: minimize Y-extent, prefer vertical placement.</summary>
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; }
/// <summary>
/// True when the filler's <see cref="AngleCandidateBuilder"/> chooses the angles; false when
/// <see cref="BuildFixedAngles"/> replaces it entirely.
/// </summary>
internal bool UsesAngleBuilder => angleExtent == null;
internal IFillComparer CreateComparer() => comparerFactory();
internal List<double> BuildAngles(NestItem item, ClassificationResult classification)
/// <summary>
/// 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.
/// </summary>
internal List<double> BuildFixedAngles(NestItem item, ClassificationResult classification)
{
var baseAngles = new List<double>
{
@@ -68,12 +68,12 @@ internal class StripPlateFiller : PlateFillerBase
{
Func<NestItem, Box, List<Part>> 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<NestItem, Box, List<Part>> widthFillFunc = (item, box) =>
{
var inner = new RemnantPlateFiller(Plate, RemnantFillPolicy.Vertical);
var inner = new DefaultPlateFiller(Plate, PlateFillPolicy.VerticalRemnant);
return inner.Fill(item, box, progress, token);
};
@@ -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),
};
}
@@ -6,7 +6,7 @@ namespace OpenNest.Engine.Jobs.Placement;
/// <summary>
/// Built-in remnant placement strategy for the whole-job runner. Fills one candidate per trial with
/// a <see cref="RemnantPlateFiller"/> for the vertical or horizontal remnant policy. Placement is
/// a <see cref="DefaultPlateFiller"/> with the vertical or horizontal remnant <see cref="PlateFillPolicy"/>. Placement is
/// <see cref="DefaultPlateNester"/>'s with that filler: the same fresh private plate per trial and
/// the same run-scoped identity, progress, and accounting boundaries.
/// </summary>
@@ -18,7 +18,7 @@ public sealed class RemnantPlateNester : IPlateNester
{
private readonly DefaultPlateNester nester;
internal RemnantPlateNester(Func<Plate, RemnantPlateFiller> fillerFactory)
internal RemnantPlateNester(Func<Plate, DefaultPlateFiller> fillerFactory)
{
nester = new DefaultPlateNester(
fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory)));
@@ -26,11 +26,11 @@ public sealed class RemnantPlateNester : IPlateNester
/// <summary>Vertical-remnant policy: minimize X-extent, prefer horizontal placement.</summary>
internal static RemnantPlateNester Vertical() =>
new(plate => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical));
new(plate => new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant));
/// <summary>Horizontal-remnant policy: minimize Y-extent, prefer vertical placement.</summary>
internal static RemnantPlateNester Horizontal() =>
new(plate => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal));
new(plate => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant));
public PlateCandidate Place(
PlatePlacementRequest request,
+5 -5
View File
@@ -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,