diff --git a/OpenNest.Engine.Tests/Jobs/FillStackCharacterizationTests.cs b/OpenNest.Engine.Tests/Jobs/FillStackCharacterizationTests.cs index e2a61bde..dac42564 100644 --- a/OpenNest.Engine.Tests/Jobs/FillStackCharacterizationTests.cs +++ b/OpenNest.Engine.Tests/Jobs/FillStackCharacterizationTests.cs @@ -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)); } diff --git a/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs index c8580a5c..5ee7f2a1 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs @@ -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 Nest( List items, diff --git a/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs index a551f4e7..68572c4c 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs @@ -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 }); } - /// Orchestrator probe whose fill/pack delegates run the Default filler. - private sealed class DefaultFillerProbe(Plate plate) : PlateFillerBase(plate) + /// Orchestrator probe whose fill/pack delegates each run a fresh Fill filler. + private sealed class DefaultFillerProbe(Plate plate) : PlateFiller(plate) { public override List Fill( NestItem item, Box workArea, IProgress progress, CancellationToken token - ) => new DefaultPlateFiller(Plate).Fill(item, workArea, progress, token); + ) => new PlateFiller(Plate).Fill(item, workArea, progress, token); public override List Fill( List groupParts, Box workArea, IProgress progress, CancellationToken token - ) => new DefaultPlateFiller(Plate).Fill(groupParts, workArea, progress, token); + ) => new PlateFiller(Plate).Fill(groupParts, workArea, progress, token); public override List PackArea( Box box, List items, IProgress progress, CancellationToken token - ) => new DefaultPlateFiller(Plate).PackArea(box, items, progress, token); + ) => new PlateFiller(Plate).PackArea(box, items, progress, token); } /// 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. - 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 PackArea( + Box box, + List items, + IProgress progress, + CancellationToken token + ) => new List(); } } diff --git a/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs b/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs index d83196e8..a75c2b59 100644 --- a/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs +++ b/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs @@ -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 @@ -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 @@ -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 { 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)), }; diff --git a/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs b/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs index 9b855828..da56c09d 100644 --- a/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs +++ b/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs @@ -8,7 +8,7 @@ namespace OpenNest.Engine.Jobs.Placement; /// /// Built-in placement strategy for the whole-job runner. Fills each candidate trial with a -/// on a fresh private plate and owns its own run-scoped +/// 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 mutations never /// feed back into job accounting. @@ -23,15 +23,15 @@ namespace OpenNest.Engine.Jobs.Placement; /// public sealed class DefaultPlateNester : IPlateNester { - private readonly Func fillerFactory; + private readonly Func 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)) { } - /// Injectable for tests; defaults to . - internal DefaultPlateNester(Func fillerFactory) + /// Injectable for tests; defaults to . + internal DefaultPlateNester(Func fillerFactory) { this.fillerFactory = fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory)); diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFiller.cs similarity index 73% rename from OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs rename to OpenNest.Engine/Jobs/Placement/Fillers/PlateFiller.cs index d1db0a5f..a5dc50a2 100644 --- a/OpenNest.Engine/Jobs/Placement/Fillers/DefaultPlateFiller.cs +++ b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFiller.cs @@ -13,22 +13,64 @@ using OpenNest.Math; namespace OpenNest.Engine.Jobs.Placement.Fillers; -internal class DefaultPlateFiller : PlateFillerBase +/// How lays out a whole plate. +internal enum PlateLayout +{ + /// Fill each multi-quantity item, pack the leftovers, then best-fit pairs. + FillThenPack, + + /// + /// Iterative shrink-fill strips (each strip ranked by a fresh remnant filler), then pack the + /// leftovers into the first remnant. + /// + Strips, +} + +/// +/// The interactive and whole-job filler behind Fill, Strip and both Remnants. A +/// filler is reused for a whole (and +/// across clamp refills), so its learned angles carry over between items. A +/// filler runs every single-area fill and pack on a fresh +/// filler, so nothing it learns or its +/// reaches them. +/// +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 PhaseResults { get; } = new(); + + public List 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 /// internal bool StopAtQuantity { get; init; } - public override List BuildAngles( + public virtual List 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 angleResults) + internal bool IsBetterFill(List candidate, List current, Box workArea) => + Comparer.IsBetter(candidate, current, workArea); + + /// A strip layout's single-area work runs here, never on the strip filler itself. + private PlateFiller CreateInnerFiller() => new(Plate, policy); + + private string BuildProgressSummary() { - angleBuilder.RecordProductive(angleResults); + if (PhaseResults.Count == 0) + return null; + + var parts = new List(PhaseResults.Count); + foreach (var result in PhaseResults) + parts.Add($"{result.Phase.ShortName()}: {result.PartCount}"); + + return string.Join(" | ", parts); } - public override List Fill( + public virtual List Fill( NestItem item, Box workArea, IProgress 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(); } - public override List Fill( + public virtual List Fill( List groupParts, Box workArea, IProgress progress, CancellationToken token ) { + if (layout == PlateLayout.Strips) + return CreateInnerFiller().Fill(groupParts, workArea, progress, token); + if (groupParts == null || groupParts.Count == 0) return new List(); @@ -410,17 +471,123 @@ internal class DefaultPlateFiller : PlateFillerBase return best ?? new List(); } - public override List PackArea( + public virtual List PackArea( Box box, List items, IProgress 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 Nest( + List items, + IProgress 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 NestStrips( + List items, + IProgress progress, + CancellationToken token + ) + { + if (items == null || items.Count == 0) + return new List(); + + 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(); + + if (fillItems.Count > 0) + { + // Each strip gets fresh remnant fillers: they run in parallel and carry no + // PlateNumber or learned angles. + Func> heightFillFunc = (item, box) => + { + var inner = new PlateFiller(Plate, PlateFillPolicy.HorizontalRemnant); + return inner.Fill(item, box, progress, token); + }; + Func> 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); } /// diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillerBase.cs b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillerBase.cs deleted file mode 100644 index 8de79730..00000000 --- a/OpenNest.Engine/Jobs/Placement/Fillers/PlateFillerBase.cs +++ /dev/null @@ -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 PhaseResults { get; } = new(); - - public List 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 BuildAngles( - NestItem item, - ClassificationResult classification, - Box workArea - ) - { - return new List - { - classification.PrimaryAngle, - classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI, - }; - } - - protected virtual void RecordProductiveAngles(List angleResults) { } - - protected FillPolicy BuildPolicy() => new(Comparer, PreferredDirection); - - protected string BuildProgressSummary() => BuildProgressSummary(PhaseResults); - - private static string BuildProgressSummary(IReadOnlyList phaseResults) - { - if (phaseResults.Count == 0) - return null; - - var parts = new List(phaseResults.Count); - foreach (var result in phaseResults) - parts.Add($"{result.Phase.ShortName()}: {result.PartCount}"); - - return string.Join(" | ", parts); - } - - internal bool IsBetterFill(List candidate, List current, Box workArea) => - Comparer.IsBetter(candidate, current, workArea); - - public virtual List Fill( - NestItem item, - Box workArea, - IProgress progress, - CancellationToken token - ) - { - return new List(); - } - - public virtual List Fill( - List groupParts, - Box workArea, - IProgress progress, - CancellationToken token - ) - { - return new List(); - } - - public virtual List PackArea( - Box box, - List items, - IProgress progress, - CancellationToken token - ) - { - return new List(); - } - - public virtual List Nest( - List items, - IProgress 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 - ); - } -} diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs deleted file mode 100644 index d031b6d0..00000000 --- a/OpenNest.Engine/Jobs/Placement/Fillers/StripPlateFiller.cs +++ /dev/null @@ -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 Fill( - NestItem item, - Box workArea, - IProgress progress, - CancellationToken token - ) - { - var inner = new DefaultPlateFiller(Plate); - return inner.Fill(item, workArea, progress, token); - } - - public override List Fill( - List groupParts, - Box workArea, - IProgress progress, - CancellationToken token - ) - { - var inner = new DefaultPlateFiller(Plate); - return inner.Fill(groupParts, workArea, progress, token); - } - - public override List PackArea( - Box box, - List items, - IProgress progress, - CancellationToken token - ) - { - var inner = new DefaultPlateFiller(Plate); - return inner.PackArea(box, items, progress, token); - } - - public override List Nest( - List items, - IProgress progress, - CancellationToken token - ) - { - if (items == null || items.Count == 0) - return new List(); - - 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(); - - if (fillItems.Count > 0) - { - Func> heightFillFunc = (item, box) => - { - var inner = new DefaultPlateFiller(Plate, PlateFillPolicy.HorizontalRemnant); - return inner.Fill(item, box, progress, token); - }; - Func> 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; - } -} diff --git a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs index 82da30c9..778850ab 100644 --- a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs +++ b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs @@ -53,7 +53,7 @@ public static class PlateFillService demand, 1); } - private static List FillDroppingClampHits(PlateFillerBase filler, Plate plate, + private static List FillDroppingClampHits(PlateFiller filler, Plate plate, NestItem item, List demand, Box area, IProgress 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. /// - private static List FillBesideClamps(PlateFillerBase filler, Plate plate, Box workArea, + private static List FillBesideClamps(PlateFiller filler, Plate plate, Box workArea, IProgress progress, CancellationToken token, Func, List> fill, List demand, int groupSize) { @@ -428,20 +428,20 @@ public static class PlateFillService } /// Builds the filler for an optional strategy (null/empty = Fill). - 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), }; } /// Public operations require an explicit strategy name (null is an argument error). - 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), diff --git a/OpenNest.Engine/Jobs/Placement/PrivatePlateFill.cs b/OpenNest.Engine/Jobs/Placement/PrivatePlateFill.cs index 9b41d0e8..6138eb9e 100644 --- a/OpenNest.Engine/Jobs/Placement/PrivatePlateFill.cs +++ b/OpenNest.Engine/Jobs/Placement/PrivatePlateFill.cs @@ -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, diff --git a/OpenNest.Engine/Jobs/Placement/RemnantPlateNester.cs b/OpenNest.Engine/Jobs/Placement/RemnantPlateNester.cs index 3b3bc6ec..b8375bf6 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 with the vertical or horizontal remnant . 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 DefaultPlateFiller(plate, PlateFillPolicy.VerticalRemnant)); + new(plate => new PlateFiller(plate, PlateFillPolicy.VerticalRemnant)); /// Horizontal-remnant policy: minimize Y-extent, prefer vertical placement. internal static RemnantPlateNester Horizontal() => - new(plate => new DefaultPlateFiller(plate, PlateFillPolicy.HorizontalRemnant)); + new(plate => new PlateFiller(plate, PlateFillPolicy.HorizontalRemnant)); public PlateCandidate Place( PlatePlacementRequest request, diff --git a/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs b/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs index 5b2aaf2b..9ba318a9 100644 --- a/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs +++ b/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs @@ -7,8 +7,9 @@ using OpenNest.Engine.Jobs.Placement.Fillers; namespace OpenNest.Engine.Jobs.Placement; /// -/// Built-in placement strategy for the whole-job runner. Runs the -/// iterative shrink-fill/pack geometry with the same run-scoped bookkeeping as +/// Built-in placement strategy for the whole-job runner. Runs a with the +/// layout (iterative shrink-fill strips, then a pack) +/// with the same run-scoped bookkeeping as /// : remaining demand is read from the request and placement counts /// are derived from returned placements. /// @@ -20,15 +21,15 @@ namespace OpenNest.Engine.Jobs.Placement; /// public sealed class StripPlateNester : IPlateNester { - private readonly Func fillerFactory; + private readonly Func 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)) { } - /// Injectable for tests; defaults to . - internal StripPlateNester(Func fillerFactory) + /// Injectable for tests; defaults to the strip-layout . + internal StripPlateNester(Func fillerFactory) { this.fillerFactory = fillerFactory ?? throw new ArgumentNullException(nameof(fillerFactory)); diff --git a/OpenNest.Engine/ML/BruteForceRunner.cs b/OpenNest.Engine/ML/BruteForceRunner.cs index 37aa9067..db77cd1d 100644 --- a/OpenNest.Engine/ML/BruteForceRunner.cs +++ b/OpenNest.Engine/ML/BruteForceRunner.cs @@ -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 }; diff --git a/OpenNest.Engine/Strategies/FillContext.cs b/OpenNest.Engine/Strategies/FillContext.cs index e4e39ea0..4c46a377 100644 --- a/OpenNest.Engine/Strategies/FillContext.cs +++ b/OpenNest.Engine/Strategies/FillContext.cs @@ -101,7 +101,7 @@ namespace OpenNest.Engine.Strategies /// the canonical drawing's baked source angle whenever it's non-zero. No-op when /// isn't set. Internal so callers that build their own /// outside (e.g. the fallback - /// report in DefaultPlateFiller.RunPipeline for strategies that don't self-report) + /// report in PlateFiller.RunPipeline for strategies that don't self-report) /// can apply the same rebind before reaching the UI. /// internal List ToOriginalFrame(List parts) diff --git a/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs b/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs index 4ee8a82b..df5eaff5 100644 --- a/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs +++ b/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs @@ -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 { 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 { 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 { new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 }, diff --git a/OpenNest.Tests/Engine/NestInvarianceTests.cs b/OpenNest.Tests/Engine/NestInvarianceTests.cs index 8d790095..af092a60 100644 --- a/OpenNest.Tests/Engine/NestInvarianceTests.cs +++ b/OpenNest.Tests/Engine/NestInvarianceTests.cs @@ -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); diff --git a/OpenNest.Tests/Engine/RemnantEngineTests.cs b/OpenNest.Tests/Engine/RemnantEngineTests.cs index d685cbfa..e67895fc 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 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, diff --git a/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs b/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs index e35a04b7..08b0df22 100644 --- a/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs +++ b/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs @@ -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> 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> 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> 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> 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); }; diff --git a/OpenNest.Tests/Fill/RemnantFillerTests2.cs b/OpenNest.Tests/Fill/RemnantFillerTests2.cs index fc7c00c6..5f5744ca 100644 --- a/OpenNest.Tests/Fill/RemnantFillerTests2.cs +++ b/OpenNest.Tests/Fill/RemnantFillerTests2.cs @@ -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> 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> 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); }; diff --git a/OpenNest.Tests/Fill/ShrinkFillerTests.cs b/OpenNest.Tests/Fill/ShrinkFillerTests.cs index cbdba86c..e2472f90 100644 --- a/OpenNest.Tests/Fill/ShrinkFillerTests.cs +++ b/OpenNest.Tests/Fill/ShrinkFillerTests.cs @@ -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> 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> 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); } } diff --git a/OpenNest.Tests/Strategies/FillPipelineTests.cs b/OpenNest.Tests/Strategies/FillPipelineTests.cs index c74f3e59..66a43e31 100644 --- a/OpenNest.Tests/Strategies/FillPipelineTests.cs +++ b/OpenNest.Tests/Strategies/FillPipelineTests.cs @@ -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(); @@ -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(); @@ -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();