refactor(engine): extract strip plate filler
This commit is contained in:
@@ -97,6 +97,74 @@ public class PlateFillerContractTests
|
|||||||
Assert.Equal(11, parts.Count);
|
Assert.Equal(11, parts.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StripPlateFiller_Nest_MatchesCompatibilityFacade()
|
||||||
|
{
|
||||||
|
var directPlate = new Plate(new Size(30, 50));
|
||||||
|
var facadePlate = new Plate(new Size(30, 50));
|
||||||
|
var directItems = new List<NestItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Drawing = new Drawing("rect-a", TestDrawingFactory.Rectangle(6, 4)),
|
||||||
|
Quantity = 5,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Drawing = new Drawing("rect-b", TestDrawingFactory.Rectangle(4, 3)),
|
||||||
|
Quantity = 4,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Drawing = new Drawing("rect-c", TestDrawingFactory.Rectangle(2, 7)),
|
||||||
|
Quantity = 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var facadeItems = new List<NestItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Drawing = new Drawing("rect-a", TestDrawingFactory.Rectangle(6, 4)),
|
||||||
|
Quantity = 5,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Drawing = new Drawing("rect-b", TestDrawingFactory.Rectangle(4, 3)),
|
||||||
|
Quantity = 4,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Drawing = new Drawing("rect-c", TestDrawingFactory.Rectangle(2, 7)),
|
||||||
|
Quantity = 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var directFiller = new StripPlateFiller(directPlate) { PlateNumber = 7 };
|
||||||
|
var facade = new StripNestEngine(facadePlate) { PlateNumber = 7 };
|
||||||
|
|
||||||
|
var directParts = directFiller.Nest(directItems, null, CancellationToken.None);
|
||||||
|
var facadeParts = facade.Nest(facadeItems, null, CancellationToken.None);
|
||||||
|
|
||||||
|
AssertEquivalentLayouts(facadeParts, directParts);
|
||||||
|
Assert.Equal(facadeItems.Select(item => item.Quantity), directItems.Select(item => item.Quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompatibilityStripFacade_Nest_UsesOverriddenPackArea()
|
||||||
|
{
|
||||||
|
var plate = new Plate(new Size(100, 100));
|
||||||
|
var drawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
|
||||||
|
var engine = new PackProbeStripNestEngine(plate, drawing);
|
||||||
|
|
||||||
|
var parts = engine.Nest(
|
||||||
|
new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } },
|
||||||
|
null,
|
||||||
|
CancellationToken.None
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Equal(1, engine.PackAreaCalls);
|
||||||
|
Assert.Single(parts);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NestProgressReporter_Report_ClonesPartsAndPreservesReportFields()
|
public void NestProgressReporter_Report_ClonesPartsAndPreservesReportFields()
|
||||||
{
|
{
|
||||||
@@ -302,6 +370,56 @@ public class PlateFillerContractTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class PackProbeStripNestEngine : StripNestEngine
|
||||||
|
{
|
||||||
|
private readonly Drawing drawing;
|
||||||
|
|
||||||
|
internal PackProbeStripNestEngine(Plate plate, Drawing drawing)
|
||||||
|
: base(plate)
|
||||||
|
{
|
||||||
|
this.drawing = drawing;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int PackAreaCalls { get; private set; }
|
||||||
|
|
||||||
|
public override List<Part> PackArea(
|
||||||
|
Box box,
|
||||||
|
List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
|
{
|
||||||
|
PackAreaCalls++;
|
||||||
|
Assert.Same(drawing, Assert.Single(items).Drawing);
|
||||||
|
return new List<Part> { new(drawing, new Vector(0, 0)) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertEquivalentLayouts(List<Part> expected, List<Part> actual)
|
||||||
|
{
|
||||||
|
var expectedParts = expected
|
||||||
|
.OrderBy(part => part.BaseDrawing.Name)
|
||||||
|
.ThenBy(part => part.Location.X)
|
||||||
|
.ThenBy(part => part.Location.Y)
|
||||||
|
.ThenBy(part => part.Rotation)
|
||||||
|
.ToList();
|
||||||
|
var actualParts = actual
|
||||||
|
.OrderBy(part => part.BaseDrawing.Name)
|
||||||
|
.ThenBy(part => part.Location.X)
|
||||||
|
.ThenBy(part => part.Location.Y)
|
||||||
|
.ThenBy(part => part.Rotation)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.Equal(expectedParts.Count, actualParts.Count);
|
||||||
|
for (var i = 0; i < expectedParts.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedParts[i].BaseDrawing.Name, actualParts[i].BaseDrawing.Name);
|
||||||
|
Assert.Equal(expectedParts[i].Location.X, actualParts[i].Location.X, 9);
|
||||||
|
Assert.Equal(expectedParts[i].Location.Y, actualParts[i].Location.Y, 9);
|
||||||
|
Assert.Equal(expectedParts[i].Rotation, actualParts[i].Rotation, 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch
|
private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch
|
||||||
{
|
{
|
||||||
"Default" => new DefaultPlateFiller(plate),
|
"Default" => new DefaultPlateFiller(plate),
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ internal abstract class PlateFillerBase
|
|||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Part> Nest(
|
public virtual List<Part> Nest(
|
||||||
List<NestItem> items,
|
List<NestItem> items,
|
||||||
IProgress<NestProgress> progress,
|
IProgress<NestProgress> progress,
|
||||||
CancellationToken token
|
CancellationToken token
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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
|
||||||
|
) => PackAreaCore(box, items, progress, token);
|
||||||
|
|
||||||
|
internal List<Part> PackAreaCore(
|
||||||
|
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 RemnantPlateFiller(Plate, RemnantFillPolicy.Horizontal);
|
||||||
|
return inner.Fill(item, box, progress, token);
|
||||||
|
};
|
||||||
|
Func<NestItem, Box, List<Part>> widthFillFunc = (item, box) =>
|
||||||
|
{
|
||||||
|
var inner = new RemnantPlateFiller(Plate, RemnantFillPolicy.Vertical);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Engine
|
namespace OpenNest.Engine
|
||||||
{
|
{
|
||||||
public class StripNestEngine : NestEngineBase
|
public class StripNestEngine : NestEngineBase
|
||||||
{
|
{
|
||||||
|
private StripPlateFiller filler;
|
||||||
|
|
||||||
public StripNestEngine(Plate plate)
|
public StripNestEngine(Plate plate)
|
||||||
: base(plate) { }
|
: base(plate) { }
|
||||||
|
|
||||||
@@ -17,9 +18,24 @@ namespace OpenNest.Engine
|
|||||||
public override string Description =>
|
public override string Description =>
|
||||||
"Iterative shrink-fill nesting for mixed-drawing layouts";
|
"Iterative shrink-fill nesting for mixed-drawing layouts";
|
||||||
|
|
||||||
/// <summary>
|
private StripPlateFiller Filler
|
||||||
/// Single-item fill delegates to DefaultNestEngine.
|
{
|
||||||
/// </summary>
|
get
|
||||||
|
{
|
||||||
|
if (filler == null || !ReferenceEquals(filler.Plate, Plate))
|
||||||
|
filler = new LegacyStripPlateFiller(this, Plate);
|
||||||
|
return filler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StripPlateFiller PrepareFiller()
|
||||||
|
{
|
||||||
|
var current = Filler;
|
||||||
|
current.PlateNumber = PlateNumber;
|
||||||
|
current.NestDirection = NestDirection;
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
public override List<Part> Fill(
|
public override List<Part> Fill(
|
||||||
NestItem item,
|
NestItem item,
|
||||||
Box workArea,
|
Box workArea,
|
||||||
@@ -27,13 +43,10 @@ namespace OpenNest.Engine
|
|||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var inner = new DefaultNestEngine(Plate);
|
var current = PrepareFiller();
|
||||||
return inner.Fill(item, workArea, progress, token);
|
return current.Fill(item, workArea, progress, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Group-parts fill delegates to DefaultNestEngine.
|
|
||||||
/// </summary>
|
|
||||||
public override List<Part> Fill(
|
public override List<Part> Fill(
|
||||||
List<Part> groupParts,
|
List<Part> groupParts,
|
||||||
Box workArea,
|
Box workArea,
|
||||||
@@ -41,13 +54,10 @@ namespace OpenNest.Engine
|
|||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var inner = new DefaultNestEngine(Plate);
|
var current = PrepareFiller();
|
||||||
return inner.Fill(groupParts, workArea, progress, token);
|
return current.Fill(groupParts, workArea, progress, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pack delegates to DefaultNestEngine.
|
|
||||||
/// </summary>
|
|
||||||
public override List<Part> PackArea(
|
public override List<Part> PackArea(
|
||||||
Box box,
|
Box box,
|
||||||
List<NestItem> items,
|
List<NestItem> items,
|
||||||
@@ -55,110 +65,36 @@ namespace OpenNest.Engine
|
|||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var inner = new DefaultNestEngine(Plate);
|
var current = PrepareFiller();
|
||||||
return inner.PackArea(box, items, progress, token);
|
return current.PackAreaCore(box, items, progress, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multi-drawing iterative shrink-fill strategy.
|
|
||||||
/// Each multi-quantity drawing gets shrink-filled into the tightest
|
|
||||||
/// sub-region using dual-direction selection. Singles and leftovers
|
|
||||||
/// are packed at the end.
|
|
||||||
/// </summary>
|
|
||||||
public override List<Part> Nest(
|
public override List<Part> Nest(
|
||||||
List<NestItem> items,
|
List<NestItem> items,
|
||||||
IProgress<NestProgress> progress,
|
IProgress<NestProgress> progress,
|
||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (items == null || items.Count == 0)
|
var current = PrepareFiller();
|
||||||
return new List<Part>();
|
return current.Nest(items, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
var workArea = Plate.WorkArea();
|
private sealed class LegacyStripPlateFiller : StripPlateFiller
|
||||||
|
{
|
||||||
|
private readonly StripNestEngine engine;
|
||||||
|
|
||||||
// Separate multi-quantity from singles.
|
internal LegacyStripPlateFiller(StripNestEngine engine, Plate plate)
|
||||||
var fillItems = items
|
: base(plate)
|
||||||
.Where(i => i.Quantity != 1)
|
|
||||||
.OrderBy(i => i.Priority)
|
|
||||||
.ThenByDescending(i => i.Drawing.Area)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var packItems = items.Where(i => i.Quantity == 1).ToList();
|
|
||||||
|
|
||||||
var allParts = new List<Part>();
|
|
||||||
|
|
||||||
// Phase 1: Iterative shrink-fill for multi-quantity items.
|
|
||||||
if (fillItems.Count > 0)
|
|
||||||
{
|
{
|
||||||
// Use direction-specific engines: height shrink benefits from
|
this.engine = engine;
|
||||||
// minimizing Y-extent, width shrink from minimizing X-extent.
|
|
||||||
Func<NestItem, Box, List<Part>> heightFillFunc = (ni, b) =>
|
|
||||||
{
|
|
||||||
var inner = new HorizontalRemnantEngine(Plate);
|
|
||||||
return inner.Fill(ni, b, progress, token);
|
|
||||||
};
|
|
||||||
|
|
||||||
Func<NestItem, Box, List<Part>> widthFillFunc = (ni, b) =>
|
|
||||||
{
|
|
||||||
var inner = new VerticalRemnantEngine(Plate);
|
|
||||||
return inner.Fill(ni, b, progress, token);
|
|
||||||
};
|
|
||||||
|
|
||||||
var shrinkResult = IterativeShrinkFiller.Fill(
|
|
||||||
fillItems,
|
|
||||||
workArea,
|
|
||||||
heightFillFunc,
|
|
||||||
Plate.PartSpacing,
|
|
||||||
token,
|
|
||||||
progress,
|
|
||||||
PlateNumber,
|
|
||||||
widthFillFunc
|
|
||||||
);
|
|
||||||
|
|
||||||
allParts.AddRange(shrinkResult.Parts);
|
|
||||||
|
|
||||||
// Compact placed parts toward the origin to close gaps.
|
|
||||||
Compactor.Settle(allParts, workArea, Plate.PartSpacing);
|
|
||||||
|
|
||||||
// Add unfilled items to pack list.
|
|
||||||
packItems.AddRange(shrinkResult.Leftovers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Pack singles + leftovers into remaining space.
|
public override List<Part> PackArea(
|
||||||
packItems = packItems.Where(i => i.Quantity > 0).ToList();
|
Box box,
|
||||||
|
List<NestItem> items,
|
||||||
if (packItems.Count > 0 && !token.IsCancellationRequested)
|
IProgress<NestProgress> progress,
|
||||||
{
|
CancellationToken token
|
||||||
// Reconstruct remaining area from placed parts.
|
) => engine.PackArea(box, items, progress, token);
|
||||||
var packArea = workArea;
|
|
||||||
if (allParts.Count > 0)
|
|
||||||
{
|
|
||||||
var obstacles = allParts
|
|
||||||
.Select(p => p.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduct placed quantities from original items by drawing reference.
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
if (item.Quantity <= 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var placed = allParts.Count(p => ReferenceEquals(p.BaseDrawing, item.Drawing));
|
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return allParts;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user