refactor(engine): merge the plate fillers into one PlateFiller

DefaultPlateFiller becomes PlateFiller(plate, policy, layout); PlateFillerBase
and StripPlateFiller are gone. PlateLayout.FillThenPack is the old
orchestrator path (Fill and both Remnants); PlateLayout.Strips is the old
StripPlateFiller.Nest, and Strip is Fill policy plus Strips layout.

Lifetimes are unchanged: a strip-layout filler runs every Fill, group Fill
and PackArea on a fresh inner filler and gives each strip fresh remnant
fillers, so learned angles and PlateNumber never leak into them. The
comparer, preferred direction, trim axis and angle recording now read the
policy directly. The base class's unsorted two-angle BuildAngles is dropped:
only DefaultPlateFiller's pipeline called BuildAngles, and Strip never ran it.

Layout-neutral: all 29 FillStackCharacterizationTests 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 9f335ea111
commit 5761eefd37
21 files changed
+286 -360

No files matched your search

@@ -328,7 +328,7 @@ public class FillStackCharacterizationTests
{
string Solve(bool fullSweep)
{
var filler = new DefaultPlateFiller(MakePlate()) { ForceFullAngleSweep = fullSweep };
var filler = new PlateFiller(MakePlate()) { ForceFullAngleSweep = fullSweep };
return Layout(filler.Nest(LearnedAngleItems(), null, CancellationToken.None));
}
@@ -1,8 +1,8 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.Jobs;
@@ -135,7 +135,7 @@ public class NestJobCancellationTests
}
}
private sealed class ReportingFiller(Plate plate) : DefaultPlateFiller(plate)
private sealed class ReportingFiller(Plate plate) : PlateFiller(plate)
{
public override List<Part> Nest(
List<NestItem> items,
@@ -1,9 +1,9 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.Jobs;
@@ -119,34 +119,34 @@ public class NestJobIdentityTests
Assert.Equal(new[] { 0, 0 }, new[] { items[0].Quantity, items[1].Quantity });
}
/// <summary>Orchestrator probe whose fill/pack delegates run the Default filler.</summary>
private sealed class DefaultFillerProbe(Plate plate) : PlateFillerBase(plate)
/// <summary>Orchestrator probe whose fill/pack delegates each run a fresh Fill filler.</summary>
private sealed class DefaultFillerProbe(Plate plate) : PlateFiller(plate)
{
public override List<Part> Fill(
NestItem item,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
) => new DefaultPlateFiller(Plate).Fill(item, workArea, progress, token);
) => new PlateFiller(Plate).Fill(item, workArea, progress, token);
public override List<Part> Fill(
List<Part> groupParts,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
) => new DefaultPlateFiller(Plate).Fill(groupParts, workArea, progress, token);
) => new PlateFiller(Plate).Fill(groupParts, workArea, progress, token);
public override List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
) => new DefaultPlateFiller(Plate).PackArea(box, items, progress, token);
) => new PlateFiller(Plate).PackArea(box, items, progress, token);
}
/// <summary>Places exactly 2 parts from the first multi-quantity item and none from the
/// rest, forcing the orchestrator's deduction to run on an asymmetric placement result.</summary>
private sealed class StarvingProbe(Plate plate) : PlateFillerBase(plate)
private sealed class StarvingProbe(Plate plate) : PlateFiller(plate)
{
private int _first = -1;
@@ -172,5 +172,12 @@ public class NestJobIdentityTests
}
return parts;
}
public override List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
) => new List<Part>();
}
}
@@ -30,10 +30,10 @@ public class PlateFillerContractTests
}
[Fact]
public void DefaultPlateFiller_UsesOverriddenAngleSelection()
public void PlateFiller_UsesOverriddenAngleSelection()
{
var plate = new Plate(new Size(30, 50));
var filler = new AngleProbeDefaultPlateFiller(plate);
var filler = new AngleProbePlateFiller(plate);
var parts = filler.Fill(
new NestItem
@@ -51,12 +51,12 @@ public class PlateFillerContractTests
}
[Fact]
public void DefaultPlateFiller_Nest_UsesOverriddenFillAndPackArea()
public void PlateFiller_Nest_UsesOverriddenFillAndPackArea()
{
var plate = new Plate(new Size(100, 100));
var fillDrawing = new Drawing("fill", TestDrawingFactory.Rectangle(10, 10));
var packDrawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
var filler = new FillAndPackProbeDefaultPlateFiller(plate, fillDrawing, packDrawing);
var filler = new FillAndPackProbePlateFiller(plate, fillDrawing, packDrawing);
var parts = filler.Nest(
new List<NestItem>
@@ -74,7 +74,7 @@ public class PlateFillerContractTests
}
[Fact]
public void StripPlateFiller_Nest_ReturnsPlacedPartsAndDeductsInput()
public void StripLayout_Nest_ReturnsPlacedPartsAndDeductsInput()
{
var plate = new Plate(new Size(30, 50));
var items = new List<NestItem>
@@ -95,7 +95,7 @@ public class PlateFillerContractTests
Quantity = 3,
},
};
var filler = new StripPlateFiller(plate) { PlateNumber = 7 };
var filler = new PlateFiller(plate, PlateFillPolicy.Fill, PlateLayout.Strips) { PlateNumber = 7 };
var parts = filler.Nest(items, null, CancellationToken.None);
@@ -105,11 +105,11 @@ public class PlateFillerContractTests
}
[Fact]
public void StripPlateFiller_Nest_UsesOverriddenPackArea()
public void StripLayout_Nest_UsesOverriddenPackArea()
{
var plate = new Plate(new Size(100, 100));
var drawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
var filler = new PackProbeStripPlateFiller(plate, drawing);
var filler = new PackProbeStripFiller(plate, drawing);
var parts = filler.Nest(
new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } },
@@ -260,12 +260,12 @@ public class PlateFillerContractTests
Assert.Equal(10, item.Quantity);
}
private sealed class FillAndPackProbeDefaultPlateFiller : DefaultPlateFiller
private sealed class FillAndPackProbePlateFiller : PlateFiller
{
private readonly Drawing fillDrawing;
private readonly Drawing packDrawing;
internal FillAndPackProbeDefaultPlateFiller(
internal FillAndPackProbePlateFiller(
Plate plate,
Drawing fillDrawing,
Drawing packDrawing
@@ -308,9 +308,9 @@ public class PlateFillerContractTests
}
}
private sealed class AngleProbeDefaultPlateFiller : DefaultPlateFiller
private sealed class AngleProbePlateFiller : PlateFiller
{
internal AngleProbeDefaultPlateFiller(Plate plate)
internal AngleProbePlateFiller(Plate plate)
: base(plate) { }
internal bool BuildAnglesCalled { get; private set; }
@@ -326,12 +326,12 @@ public class PlateFillerContractTests
}
}
private sealed class PackProbeStripPlateFiller : StripPlateFiller
private sealed class PackProbeStripFiller : PlateFiller
{
private readonly Drawing drawing;
internal PackProbeStripPlateFiller(Plate plate, Drawing drawing)
: base(plate)
internal PackProbeStripFiller(Plate plate, Drawing drawing)
: base(plate, PlateFillPolicy.Fill, PlateLayout.Strips)
{
this.drawing = drawing;
}
@@ -351,11 +351,11 @@ public class PlateFillerContractTests
}
}
private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch
private static PlateFiller CreateFiller(string strategy, Plate plate) => strategy switch
{
"Fill" => new DefaultPlateFiller(plate),
"Vertical Remnant" => new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant),
"Horizontal Remnant" => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant),
"Fill" => new PlateFiller(plate),
"Vertical Remnant" => new PlateFiller(plate, PlateFillPolicy.VerticalRemnant),
"Horizontal Remnant" => new PlateFiller(plate, PlateFillPolicy.HorizontalRemnant),
_ => throw new ArgumentOutOfRangeException(nameof(strategy)),
};
@@ -8,7 +8,7 @@ namespace OpenNest.Engine.Jobs.Placement;
/// <summary>
/// Built-in placement strategy for the whole-job runner. Fills each candidate trial with a
/// <see cref="DefaultPlateFiller"/> on a fresh private plate and owns its own run-scoped
/// <see cref="PlateFiller"/> on a fresh private plate and owns its own run-scoped
/// bookkeeping: remaining demand is read from the request and placement counts are derived from
/// returned placements, so the filler's private <see cref="NestItem.Quantity"/> mutations never
/// feed back into job accounting.
@@ -23,15 +23,15 @@ namespace OpenNest.Engine.Jobs.Placement;
/// </remarks>
public sealed class DefaultPlateNester : IPlateNester
{
private readonly Func<Plate, DefaultPlateFiller> fillerFactory;
private readonly Func<Plate, PlateFiller> fillerFactory;
private readonly OrderedPlateNester restrictedRotationNester = new();
private readonly CandidatePlacementContext context = new();
public DefaultPlateNester()
: this(static plate => new DefaultPlateFiller(plate)) { }
: this(static plate => new PlateFiller(plate)) { }
/// <param name="fillerFactory">Injectable for tests; defaults to <see cref="DefaultPlateFiller"/>.</param>
internal DefaultPlateNester(Func<Plate, DefaultPlateFiller> fillerFactory)
/// <param name="fillerFactory">Injectable for tests; defaults to <see cref="PlateFiller"/>.</param>
internal DefaultPlateNester(Func<Plate, PlateFiller> fillerFactory)
{
this.fillerFactory =
fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory));
@@ -13,22 +13,64 @@ using OpenNest.Math;
namespace OpenNest.Engine.Jobs.Placement.Fillers;
internal class DefaultPlateFiller : PlateFillerBase
/// <summary>How <see cref="PlateFiller.Nest"/> lays out a whole plate.</summary>
internal enum PlateLayout
{
/// <summary>Fill each multi-quantity item, pack the leftovers, then best-fit pairs.</summary>
FillThenPack,
/// <summary>
/// Iterative shrink-fill strips (each strip ranked by a fresh remnant filler), then pack the
/// leftovers into the first remnant.
/// </summary>
Strips,
}
/// <summary>
/// The interactive and whole-job filler behind Fill, Strip and both Remnants. A
/// <see cref="PlateLayout.FillThenPack"/> filler is reused for a whole <see cref="Nest"/> (and
/// across clamp refills), so its learned angles carry over between items. A
/// <see cref="PlateLayout.Strips"/> filler runs every single-area fill and pack on a fresh
/// <see cref="PlateLayout.FillThenPack"/> filler, so nothing it learns or its
/// <see cref="PlateNumber"/> reaches them.
/// </summary>
internal class PlateFiller
{
private readonly AngleCandidateBuilder angleBuilder = new();
private readonly PlateFillPolicy policy;
private readonly PlateLayout layout;
private IFillComparer comparer;
internal DefaultPlateFiller(Plate plate, PlateFillPolicy policy = null)
: base(plate)
internal PlateFiller(
Plate plate,
PlateFillPolicy policy = null,
PlateLayout layout = PlateLayout.FillThenPack
)
{
Plate = plate;
this.policy = policy ?? PlateFillPolicy.Fill;
this.layout = layout;
}
protected override IFillComparer CreateComparer() => policy.CreateComparer();
public Plate Plate { get; }
public override NestDirection? PreferredDirection => policy.PreferredDirection;
public int PlateNumber { get; set; }
public override ShrinkAxis TrimAxis => policy.TrimAxis;
public NestDirection NestDirection { get; set; }
public NestPhase WinnerPhase { get; private set; }
public List<PhaseResult> PhaseResults { get; } = new();
public List<AngleResult> AngleResults { get; } = new();
public NestDirection? PreferredDirection => policy.PreferredDirection;
public ShrinkAxis TrimAxis => policy.TrimAxis;
private IFillComparer Comparer => comparer ??= policy.CreateComparer();
private FillPolicy BuildPolicy() => new(Comparer, PreferredDirection);
internal bool ForceFullAngleSweep
{
@@ -43,7 +85,7 @@ internal class DefaultPlateFiller : PlateFillerBase
/// </summary>
internal bool StopAtQuantity { get; init; }
public override List<double> BuildAngles(
public virtual List<double> BuildAngles(
NestItem item,
ClassificationResult classification,
Box workArea
@@ -52,18 +94,34 @@ internal class DefaultPlateFiller : PlateFillerBase
? angleBuilder.Build(item, classification, workArea)
: policy.BuildFixedAngles(item, classification);
protected override void RecordProductiveAngles(List<AngleResult> angleResults)
internal bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea) =>
Comparer.IsBetter(candidate, current, workArea);
/// <summary>A strip layout's single-area work runs here, never on the strip filler itself.</summary>
private PlateFiller CreateInnerFiller() => new(Plate, policy);
private string BuildProgressSummary()
{
angleBuilder.RecordProductive(angleResults);
if (PhaseResults.Count == 0)
return null;
var parts = new List<string>(PhaseResults.Count);
foreach (var result in PhaseResults)
parts.Add($"{result.Phase.ShortName()}: {result.PartCount}");
return string.Join(" | ", parts);
}
public override List<Part> Fill(
public virtual List<Part> Fill(
NestItem item,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
if (layout == PlateLayout.Strips)
return CreateInnerFiller().Fill(item, workArea, progress, token);
PhaseResults.Clear();
AngleResults.Clear();
@@ -366,13 +424,16 @@ internal class DefaultPlateFiller : PlateFillerBase
return context.CurrentBest ?? new List<Part>();
}
public override List<Part> Fill(
public virtual List<Part> Fill(
List<Part> groupParts,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
if (layout == PlateLayout.Strips)
return CreateInnerFiller().Fill(groupParts, workArea, progress, token);
if (groupParts == null || groupParts.Count == 0)
return new List<Part>();
@@ -410,17 +471,123 @@ internal class DefaultPlateFiller : PlateFillerBase
return best ?? new List<Part>();
}
public override List<Part> PackArea(
public virtual List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
if (layout == PlateLayout.Strips)
return CreateInnerFiller().PackArea(box, items, progress, token);
return AreaPacker.Pack(box, items, Plate.PartSpacing, Comparer, token, Plate.WorkArea());
}
protected virtual void RunPipeline(FillContext context)
public virtual List<Part> Nest(
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
if (layout == PlateLayout.Strips)
return NestStrips(items, progress, token);
return PlateFillOrchestrator.Nest(
Plate,
items,
Comparer,
(item, workArea, sink, cancellation) => Fill(item, workArea, sink, cancellation),
(workArea, packItems, sink, cancellation) =>
PackArea(workArea, packItems, sink, cancellation),
progress,
token
);
}
private List<Part> NestStrips(
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
if (items == null || items.Count == 0)
return new List<Part>();
var workArea = Plate.WorkArea();
var fillItems = items
.Where(item => item.Quantity != 1)
.OrderBy(item => item.Priority)
.ThenByDescending(item => item.Drawing.Area)
.ToList();
var packItems = items.Where(item => item.Quantity == 1).ToList();
var allParts = new List<Part>();
if (fillItems.Count > 0)
{
// Each strip gets fresh remnant fillers: they run in parallel and carry no
// PlateNumber or learned angles.
Func<NestItem, Box, List<Part>> heightFillFunc = (item, box) =>
{
var inner = new PlateFiller(Plate, PlateFillPolicy.HorizontalRemnant);
return inner.Fill(item, box, progress, token);
};
Func<NestItem, Box, List<Part>> widthFillFunc = (item, box) =>
{
var inner = new PlateFiller(Plate, PlateFillPolicy.VerticalRemnant);
return inner.Fill(item, box, progress, token);
};
var shrinkResult = IterativeShrinkFiller.Fill(
fillItems,
workArea,
heightFillFunc,
Plate.PartSpacing,
token,
progress,
PlateNumber,
widthFillFunc
);
allParts.AddRange(shrinkResult.Parts);
Compactor.Settle(allParts, workArea, Plate.PartSpacing);
packItems.AddRange(shrinkResult.Leftovers);
}
packItems = packItems.Where(item => item.Quantity > 0).ToList();
if (packItems.Count > 0 && !token.IsCancellationRequested)
{
var packArea = workArea;
if (allParts.Count > 0)
{
var obstacles = allParts
.Select(part => part.BoundingBox.Offset(Plate.PartSpacing))
.ToList();
var finder = new RemnantFinder(workArea, obstacles);
var remnants = finder.FindRemnants();
packArea = remnants.Count > 0 ? remnants[0] : new Box(0, 0, 0, 0);
}
if (packArea.Width > 0 && packArea.Length > 0)
{
var packParts = PackArea(packArea, packItems, progress, token);
allParts.AddRange(packParts);
}
}
foreach (var item in items)
{
if (item.Quantity <= 0)
continue;
var placed = allParts.Count(part => ReferenceEquals(part.BaseDrawing, item.Drawing));
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
return allParts;
}
private void RunPipeline(FillContext context)
{
var classification = PartClassifier.Classify(context.Item.Drawing);
context.PartType = classification.Type;
@@ -502,7 +669,7 @@ internal class DefaultPlateFiller : PlateFillerBase
}
}
RecordProductiveAngles(context.AngleResults);
angleBuilder.RecordProductive(context.AngleResults);
}
/// <summary>
@@ -1,121 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
namespace OpenNest.Engine.Jobs.Placement.Fillers;
internal abstract class PlateFillerBase
{
private IFillComparer comparer;
protected PlateFillerBase(Plate plate)
{
Plate = plate;
}
public Plate Plate { get; }
public int PlateNumber { get; set; }
public NestDirection NestDirection { get; set; }
public NestPhase WinnerPhase { get; protected set; }
public List<PhaseResult> PhaseResults { get; } = new();
public List<AngleResult> AngleResults { get; } = new();
protected IFillComparer Comparer => comparer ??= CreateComparer();
protected virtual IFillComparer CreateComparer() => new DefaultFillComparer();
public virtual NestDirection? PreferredDirection => null;
public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width;
public virtual List<double> BuildAngles(
NestItem item,
ClassificationResult classification,
Box workArea
)
{
return new List<double>
{
classification.PrimaryAngle,
classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI,
};
}
protected virtual void RecordProductiveAngles(List<AngleResult> angleResults) { }
protected FillPolicy BuildPolicy() => new(Comparer, PreferredDirection);
protected string BuildProgressSummary() => BuildProgressSummary(PhaseResults);
private static string BuildProgressSummary(IReadOnlyList<PhaseResult> phaseResults)
{
if (phaseResults.Count == 0)
return null;
var parts = new List<string>(phaseResults.Count);
foreach (var result in phaseResults)
parts.Add($"{result.Phase.ShortName()}: {result.PartCount}");
return string.Join(" | ", parts);
}
internal bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea) =>
Comparer.IsBetter(candidate, current, workArea);
public virtual List<Part> Fill(
NestItem item,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
return new List<Part>();
}
public virtual List<Part> Fill(
List<Part> groupParts,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
return new List<Part>();
}
public virtual List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
return new List<Part>();
}
public virtual List<Part> Nest(
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
return PlateFillOrchestrator.Nest(
Plate,
items,
Comparer,
(item, workArea, sink, cancellation) => Fill(item, workArea, sink, cancellation),
(workArea, packItems, sink, cancellation) =>
PackArea(workArea, packItems, sink, cancellation),
progress,
token
);
}
}
@@ -1,128 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
namespace OpenNest.Engine.Jobs.Placement.Fillers;
internal class StripPlateFiller : PlateFillerBase
{
internal StripPlateFiller(Plate plate)
: base(plate) { }
public override List<Part> Fill(
NestItem item,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
var inner = new DefaultPlateFiller(Plate);
return inner.Fill(item, workArea, progress, token);
}
public override List<Part> Fill(
List<Part> groupParts,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
var inner = new DefaultPlateFiller(Plate);
return inner.Fill(groupParts, workArea, progress, token);
}
public override List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
var inner = new DefaultPlateFiller(Plate);
return inner.PackArea(box, items, progress, token);
}
public override List<Part> Nest(
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
if (items == null || items.Count == 0)
return new List<Part>();
var workArea = Plate.WorkArea();
var fillItems = items
.Where(item => item.Quantity != 1)
.OrderBy(item => item.Priority)
.ThenByDescending(item => item.Drawing.Area)
.ToList();
var packItems = items.Where(item => item.Quantity == 1).ToList();
var allParts = new List<Part>();
if (fillItems.Count > 0)
{
Func<NestItem, Box, List<Part>> heightFillFunc = (item, box) =>
{
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 DefaultPlateFiller(Plate, PlateFillPolicy.VerticalRemnant);
return inner.Fill(item, box, progress, token);
};
var shrinkResult = IterativeShrinkFiller.Fill(
fillItems,
workArea,
heightFillFunc,
Plate.PartSpacing,
token,
progress,
PlateNumber,
widthFillFunc
);
allParts.AddRange(shrinkResult.Parts);
Compactor.Settle(allParts, workArea, Plate.PartSpacing);
packItems.AddRange(shrinkResult.Leftovers);
}
packItems = packItems.Where(item => item.Quantity > 0).ToList();
if (packItems.Count > 0 && !token.IsCancellationRequested)
{
var packArea = workArea;
if (allParts.Count > 0)
{
var obstacles = allParts
.Select(part => part.BoundingBox.Offset(Plate.PartSpacing))
.ToList();
var finder = new RemnantFinder(workArea, obstacles);
var remnants = finder.FindRemnants();
packArea = remnants.Count > 0 ? remnants[0] : new Box(0, 0, 0, 0);
}
if (packArea.Width > 0 && packArea.Length > 0)
{
var packParts = PackArea(packArea, packItems, progress, token);
allParts.AddRange(packParts);
}
}
foreach (var item in items)
{
if (item.Quantity <= 0)
continue;
var placed = allParts.Count(part => ReferenceEquals(part.BaseDrawing, item.Drawing));
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
return allParts;
}
}
@@ -53,7 +53,7 @@ public static class PlateFillService
demand, 1);
}
private static List<Part> FillDroppingClampHits(PlateFillerBase filler, Plate plate,
private static List<Part> FillDroppingClampHits(PlateFiller filler, Plate plate,
NestItem item, List<NestItem> demand, Box area, IProgress<NestProgress> progress,
CancellationToken token)
{
@@ -81,7 +81,7 @@ public static class PlateFillService
/// spacing would, and keeps the better layout by the filler's own ranking; a tie keeps the
/// first. Previews never fall below the layout already found.
/// </summary>
private static List<Part> FillBesideClamps(PlateFillerBase filler, Plate plate, Box workArea,
private static List<Part> FillBesideClamps(PlateFiller filler, Plate plate, Box workArea,
IProgress<NestProgress> progress, CancellationToken token,
Func<Box, IProgress<NestProgress>, List<Part>> fill, List<NestItem> demand, int groupSize)
{
@@ -428,20 +428,20 @@ public static class PlateFillService
}
/// <summary>Builds the filler for an optional strategy (null/empty = Fill).</summary>
private static PlateFillerBase CreateFiller(string strategy, Plate plate)
private static PlateFiller CreateFiller(string strategy, Plate plate)
{
ArgumentNullException.ThrowIfNull(plate);
return ResolveStrategy(strategy) switch
{
"Fill" => new DefaultPlateFiller(plate),
"Strip" => new StripPlateFiller(plate),
"Vertical Remnant" => new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant),
_ => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant),
"Fill" => new PlateFiller(plate),
"Strip" => new PlateFiller(plate, PlateFillPolicy.Fill, PlateLayout.Strips),
"Vertical Remnant" => new PlateFiller(plate, PlateFillPolicy.VerticalRemnant),
_ => new PlateFiller(plate, PlateFillPolicy.HorizontalRemnant),
};
}
/// <summary>Public operations require an explicit strategy name (null is an argument error).</summary>
private static PlateFillerBase RequireFiller(string strategy, Plate plate)
private static PlateFiller RequireFiller(string strategy, Plate plate)
{
ArgumentNullException.ThrowIfNull(strategy);
// An explicit empty string is an unknown strategy; only ResolveStrategy(string),
@@ -32,7 +32,7 @@ internal static class PrivatePlateFill
fits.AddRange(sorted);
// A positive quantity lets supporting strategies avoid surplus rows. Zero still
// fills the whole area (the cutout lattice caller requires an uncapped lattice).
var filler = new DefaultPlateFiller(plate) { StopAtQuantity = true };
var filler = new PlateFiller(plate) { StopAtQuantity = true };
return filler.Fill(new NestItem
{
Drawing = drawing,
@@ -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="DefaultPlateFiller"/> with the vertical or horizontal remnant <see cref="PlateFillPolicy"/>. Placement is
/// a <see cref="PlateFiller"/> 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, DefaultPlateFiller> fillerFactory)
internal RemnantPlateNester(Func<Plate, PlateFiller> 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 DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant));
new(plate => new PlateFiller(plate, PlateFillPolicy.VerticalRemnant));
/// <summary>Horizontal-remnant policy: minimize Y-extent, prefer vertical placement.</summary>
internal static RemnantPlateNester Horizontal() =>
new(plate => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant));
new(plate => new PlateFiller(plate, PlateFillPolicy.HorizontalRemnant));
public PlateCandidate Place(
PlatePlacementRequest request,
@@ -7,8 +7,9 @@ using OpenNest.Engine.Jobs.Placement.Fillers;
namespace OpenNest.Engine.Jobs.Placement;
/// <summary>
/// Built-in placement strategy for the whole-job runner. Runs the <see cref="StripPlateFiller"/>
/// iterative shrink-fill/pack geometry with the same run-scoped bookkeeping as
/// Built-in placement strategy for the whole-job runner. Runs a <see cref="PlateFiller"/> with the
/// <see cref="PlateLayout.Strips"/> layout (iterative shrink-fill strips, then a pack)
/// with the same run-scoped bookkeeping as
/// <see cref="DefaultPlateNester"/>: remaining demand is read from the request and placement counts
/// are derived from returned placements.
/// </summary>
@@ -20,15 +21,15 @@ namespace OpenNest.Engine.Jobs.Placement;
/// </remarks>
public sealed class StripPlateNester : IPlateNester
{
private readonly Func<Plate, StripPlateFiller> fillerFactory;
private readonly Func<Plate, PlateFiller> fillerFactory;
private readonly OrderedPlateNester restrictedRotationNester = new();
private readonly CandidatePlacementContext context = new();
public StripPlateNester()
: this(static plate => new StripPlateFiller(plate)) { }
: this(static plate => new PlateFiller(plate, PlateFillPolicy.Fill, PlateLayout.Strips)) { }
/// <param name="fillerFactory">Injectable for tests; defaults to <see cref="StripPlateFiller"/>.</param>
internal StripPlateNester(Func<Plate, StripPlateFiller> fillerFactory)
/// <param name="fillerFactory">Injectable for tests; defaults to the strip-layout <see cref="PlateFiller"/>.</param>
internal StripPlateNester(Func<Plate, PlateFiller> fillerFactory)
{
this.fillerFactory =
fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory));
+1 -1
View File
@@ -31,7 +31,7 @@ namespace OpenNest.Engine.ML
bool forceFullAngleSweep = false
)
{
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
filler.ForceFullAngleSweep = forceFullAngleSweep;
var item = new NestItem { Drawing = drawing };
+1 -1
View File
@@ -101,7 +101,7 @@ namespace OpenNest.Engine.Strategies
/// the canonical drawing's baked source angle whenever it's non-zero. No-op when
/// <see cref="OriginalDrawing"/> isn't set. Internal so callers that build their own
/// <see cref="ProgressReport"/> outside <see cref="ReportProgress"/> (e.g. the fallback
/// report in <c>DefaultPlateFiller.RunPipeline</c> for strategies that don't self-report)
/// report in <c>PlateFiller.RunPipeline</c> for strategies that don't self-report)
/// can apply the same rebind before reaching the UI.
/// </summary>
internal List<Part> ToOriginalFrame(List<Part> parts)
@@ -18,10 +18,10 @@ public class EngineRefactorSmokeTests
}
[Fact]
public void DefaultPlateFiller_FillNestItem_ProducesResults()
public void PlateFiller_FillNestItem_ProducesResults()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = filler.Fill(
@@ -31,14 +31,14 @@ public class EngineRefactorSmokeTests
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0, "DefaultPlateFiller should fill parts");
Assert.True(parts.Count > 0, "PlateFiller should fill parts");
}
[Fact]
public void DefaultPlateFiller_FillGroupParts_ProducesResults()
public void PlateFiller_FillGroupParts_ProducesResults()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var drawing = MakeRectDrawing(20, 10);
var groupParts = new List<Part> { new Part(drawing) };
@@ -49,14 +49,14 @@ public class EngineRefactorSmokeTests
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0, "DefaultPlateFiller group fill should produce parts");
Assert.True(parts.Count > 0, "PlateFiller group fill should produce parts");
}
[Fact]
public void DefaultPlateFiller_ForceFullAngleSweep_StillWorks()
public void PlateFiller_ForceFullAngleSweep_StillWorks()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
filler.ForceFullAngleSweep = true;
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
@@ -73,10 +73,10 @@ public class EngineRefactorSmokeTests
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DefaultPlateFiller_StopAtQuantity_StillDeliversExactlyTheQuantity(bool stopAtQuantity)
public void PlateFiller_StopAtQuantity_StillDeliversExactlyTheQuantity(bool stopAtQuantity)
{
var plate = new Plate(60, 120) { PartSpacing = 0.5 };
var filler = new DefaultPlateFiller(plate) { StopAtQuantity = stopAtQuantity };
var filler = new PlateFiller(plate) { StopAtQuantity = stopAtQuantity };
var item = new NestItem { Drawing = MakeRectDrawing(7, 3), Quantity = 23 };
var parts = filler.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
@@ -92,10 +92,10 @@ public class EngineRefactorSmokeTests
}
[Fact]
public void StripPlateFiller_Nest_ProducesResults()
public void StripLayout_Nest_ProducesResults()
{
var plate = new Plate(60, 120);
var filler = new StripPlateFiller(plate);
var filler = new PlateFiller(plate, PlateFillPolicy.Fill, PlateLayout.Strips);
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
@@ -104,14 +104,14 @@ public class EngineRefactorSmokeTests
var parts = filler.Nest(items, null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "StripPlateFiller should nest parts");
Assert.True(parts.Count > 0, "Strip layout should nest parts");
}
[Fact]
public void DefaultPlateFiller_Nest_ProducesResults()
public void PlateFiller_Nest_ProducesResults()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
+2 -2
View File
@@ -38,7 +38,7 @@ public class NestInvarianceTests
private static int RunFillCount(Drawing drawing, Plate plate)
{
BestFitCache.Clear();
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = drawing };
var parts = filler.Fill(
item,
@@ -72,7 +72,7 @@ public class NestInvarianceTests
foreach (var theta in new[] { 0.0, 0.3, 0.8, 1.2 })
{
BestFitCache.Clear();
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeImportedAt(theta) };
var parts = filler.Fill(item, workArea, progress: null, token: CancellationToken.None);
+6 -6
View File
@@ -23,7 +23,7 @@ public class RemnantEngineTests
public void VerticalRemnantPlateFiller_UsesHorizontalPreferredDirection()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant);
var filler = new PlateFiller(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 DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant);
var filler = new PlateFiller(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 DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant);
var filler = new PlateFiller(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 DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant);
var filler = new PlateFiller(plate, PlateFillPolicy.HorizontalRemnant);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = filler.Fill(
@@ -83,8 +83,8 @@ public class RemnantEngineTests
var drawing = MakeRectDrawing(20, 10);
var item = new NestItem { Drawing = drawing };
var defaultFiller = new DefaultPlateFiller(plate);
var remnantFiller = new DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant);
var defaultFiller = new PlateFiller(plate);
var remnantFiller = new PlateFiller(plate, PlateFillPolicy.VerticalRemnant);
var defaultParts = defaultFiller.Fill(
item,
@@ -1,7 +1,7 @@
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Engine;
namespace OpenNest.Tests.Fill;
@@ -55,7 +55,7 @@ public class IterativeShrinkFillerTests
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
@@ -76,7 +76,7 @@ public class IterativeShrinkFillerTests
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
@@ -101,7 +101,7 @@ public class IterativeShrinkFillerTests
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
@@ -123,7 +123,7 @@ public class IterativeShrinkFillerTests
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
+3 -3
View File
@@ -1,7 +1,7 @@
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Engine;
namespace OpenNest.Tests.Fill;
@@ -36,7 +36,7 @@ public class RemnantFillerTests2
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
@@ -60,7 +60,7 @@ public class RemnantFillerTests2
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
+4 -4
View File
@@ -1,7 +1,7 @@
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
using OpenNest.Engine;
namespace OpenNest.Tests.Fill;
@@ -28,7 +28,7 @@ public class ShrinkFillerTests
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
@@ -50,7 +50,7 @@ public class ShrinkFillerTests
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
@@ -151,6 +151,6 @@ public class ShrinkFillerTests
var trimmed = ShrinkFiller.TrimToCount(parts, 1, ShrinkAxis.Width);
Assert.Equal(originalCount, parts.Count);
Assert.Equal(1, trimmed.Count);
Assert.Single(trimmed);
}
}
@@ -26,7 +26,7 @@ public class FillPipelineTests
public void Pipeline_PopulatesPhaseResults()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
filler.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
@@ -41,7 +41,7 @@ public class FillPipelineTests
public void Pipeline_SetsWinnerPhase()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = filler.Fill(
@@ -65,7 +65,7 @@ public class FillPipelineTests
public void Pipeline_RespectsCancellation()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var cts = new System.Threading.CancellationTokenSource();
cts.Cancel();
@@ -81,7 +81,7 @@ public class FillPipelineTests
public void Pipeline_ReportsBoxLayoutBeforeBestFitPairs()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var reports = new List<NestProgress>();
@@ -97,7 +97,7 @@ public class FillPipelineTests
public void Pipeline_ReportsStatusWhileComputingBestFitPairs()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var reports = new List<NestProgress>();
@@ -113,7 +113,7 @@ public class FillPipelineTests
public void Pipeline_AcceptedBeforeBestFitPairs_ReturnsBoxLayout()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
using var cts = new CancellationTokenSource();
NestProgress first = null;
@@ -137,7 +137,7 @@ public class FillPipelineTests
public void Pipeline_ShownLayoutNeverRegressesBelowBoxLayout()
{
var plate = new Plate(60, 120);
var filler = new DefaultPlateFiller(plate);
var filler = new PlateFiller(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var shown = new List<int>();