refactor(engine): rename the Default engine and fill strategy to Fill

The multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder) is
no longer meant to be the engine used by default, so it gets a name for what
it does. The registry lists it as Fill and maps the old name Default to it;
PlateFillService, PlateNesterFactory and NestJobOptions use Fill, and every
fill-strategy caller still accepts Default. Console --autonest now validates
engine names through NestingEngineRegistry.ResolveName so renamed names work.

Layouts are unchanged: Default and Fill resolve to the same fillers and the
golden layouts pass under the new name.
This commit is contained in:
aj committed 2026-10-06 12:11:03 -04:00
1 parent 6b4078aeb2
commit 0a1d021bf7
35 files changed
+178 -84

No files matched your search

+1 -1
View File
@@ -83,7 +83,7 @@ namespace OpenNest.Benchmark
parts,
stock,
new NestJobOptions(
"Default",
"Fill",
maxPlates,
salvageRate ?? SalvageRate,
minimumSalvageDimension ?? 0
+4 -6
View File
@@ -79,10 +79,8 @@ static class NestConsole
// single-plate placement strategy. Unknown names exit with the valid choices.
if (options.AutoNest)
{
var isJobsEngine = NestingEngineRegistry.AvailableEngines.Any(e =>
e.Name.Equals(options.Engine, StringComparison.OrdinalIgnoreCase)
);
if (!isJobsEngine)
// ResolveName also accepts renamed engines' old names (for example Default).
if (NestingEngineRegistry.ResolveName(options.Engine) == null)
{
Console.Error.WriteLine(
$"Error: unknown engine '{options.Engine}'. Jobs engines: {string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))}"
@@ -764,8 +762,8 @@ static class NestConsole
--template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)
--autonest Validated whole-job nesting onto one sheet; replaces only after acceptance
--allow-invalid Explicitly keep representable invalid autonest layouts (default: reject, exit 2)
--engine <name> With --autonest: jobs engine (default: Default; also StockLadder, Strip, ...).
Without --autonest: fill strategy (Default, Strip, Vertical Remnant, Horizontal Remnant)
--engine <name> With --autonest: jobs engine (default: Default; also Irregular, Rectangles, Fill, ...).
Without --autonest: fill strategy (Fill (default), Strip, Vertical Remnant, Horizontal Remnant)
--keep-parts Don't clear existing parts before filling
--check-overlaps Run overlap detection after fill (exit code 1 if found)
--no-save Skip saving output file
@@ -0,0 +1,81 @@
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.Tests.Jobs;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests;
/// <summary>
/// The fill engine and placement strategy are named "Fill"; the old name "Default" remains a
/// hidden alias so saved selections, scripts and API requests keep their behavior.
/// </summary>
public class FillStrategyNameTests
{
[Fact]
public void FillIsListedAndDefaultIsNot()
{
var engines = NestingEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
Assert.Contains("Fill", engines);
Assert.DoesNotContain("Default", engines, StringComparer.OrdinalIgnoreCase);
Assert.Equal(new[] { "Fill", "Strip", "Vertical Remnant", "Horizontal Remnant" },
PlateFillService.BuiltInStrategies);
}
[Theory]
[InlineData("Default")]
[InlineData(" default ")]
public void LegacyDefaultEngineNameResolvesToFill(string name)
{
Assert.Equal("Fill", NestingEngineRegistry.ResolveName(name));
Assert.IsType<FixedStrategyNestingEngine>(NestingEngineRegistry.Create(name));
Assert.Equal(NestJobStatus.Complete, NestingEngineRegistry.Create(name).Solve(FiniteStockJobTests.Job(1)).Status);
}
[Fact]
public void PlugInNamedDefaultCannotShadowFill()
{
var before = NestingEngineRegistry.AvailableEngines.Count;
NestingEngineRegistry.Register("Default", "stale plug-in", () => new FixedStrategyNestingEngine("Strip"));
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
Assert.Equal("Fill", NestingEngineRegistry.ResolveName("Default"));
}
[Theory]
[InlineData(null, "Fill")]
[InlineData("", "Fill")]
[InlineData("fill", "Fill")]
[InlineData("Default", "Fill")]
[InlineData("DEFAULT", "Fill")]
public void PlacementStrategyResolvesFillAndItsLegacyName(string? strategy, string expected)
{
Assert.Equal(expected, PlateFillService.ResolveStrategy(strategy!));
}
[Fact]
public void LegacyStrategyNameStillFillsAPlate()
{
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
var parts = PlateFillService.FillItem("Default", plate,
new NestItem { Drawing = drawing, Quantity = 6 }, plate.WorkArea(), null, CancellationToken.None);
Assert.Equal(6, parts.Count);
}
[Fact]
public void WholeJobRunnerAcceptsFillAndItsLegacyName()
{
Assert.IsType<DefaultPlateNester>(PlateNesterFactory.Create("Fill"));
Assert.IsType<DefaultPlateNester>(PlateNesterFactory.Create("Default"));
Assert.Equal("Fill", new NestJobOptions().PlacementStrategy);
var job = FiniteStockJobTests.Job(1);
var legacy = new NestJobRunner(PlateNesterFactory.Create)
.Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Default")));
Assert.Equal(NestJobStatus.Complete, legacy.Status);
}
}
@@ -54,7 +54,7 @@ public class CutoutLatticeFillTests
}
// Both cases fail intermittently; unskip when Project Memory opennest issue c98c21bd is fixed.
private const string FillNondeterminism = "Open defect (Project Memory opennest c98c21bd): Default Fill can "
private const string FillNondeterminism = "Open defect (Project Memory opennest c98c21bd): Fill can "
+ "return different, equally scored lattices between identical calls.";
[Theory]
@@ -22,7 +22,7 @@ public class FixedStrategyNestingEngineTests
[Fact]
public void PreservesJobMaxPlates()
{
var engine = new FixedStrategyNestingEngine("Default");
var engine = new FixedStrategyNestingEngine("Fill");
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(100, 100)),
@@ -39,7 +39,7 @@ public class FixedStrategyNestingEngineTests
var job = new NestJob(
new[] { part },
new[] { stock },
new NestJobOptions("Default", maxPlates: 1)
new NestJobOptions("Fill", maxPlates: 1)
);
var result = engine.Solve(job);
@@ -165,7 +165,7 @@ public class GoldenLayoutTests
[Fact]
public void Default_MixedJob_GoldenLayout()
{
var result = SolveStable(new NestJobRunner(PlateNesterFactory.Create), MixedJob("Default"));
var result = SolveStable(new NestJobRunner(PlateNesterFactory.Create), MixedJob("Fill"));
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(20, result.Plates.Sum(p => p.Placements.Count));
AssertGolden(
@@ -299,7 +299,7 @@ public class GoldenLayoutTests
[Fact]
public void OrderedViaStockLadder_MixedJob_GoldenLayout()
{
var result = SolveStable(new StockLadderNestingEngine(), MixedJob("Default"));
var result = SolveStable(new StockLadderNestingEngine(), MixedJob("Fill"));
Assert.Equal(NestJobStatus.Complete, result.Status);
AssertGolden(
result,
@@ -338,7 +338,7 @@ public class GoldenLayoutTests
new NestPlateStock("small", new Size(14, 14), 4, 1, new Spacing(1, 1, 1, 1)),
new NestPlateStock("large", new Size(30, 24), 3, 1, new Spacing(1, 1, 1, 1)),
},
new NestJobOptions("Default")
new NestJobOptions("Fill")
);
private sealed record AuthoritativeStage(
@@ -408,7 +408,7 @@ public class GoldenLayoutTests
{
var stages = new List<AuthoritativeStage>();
var legacyPhases = new SortedSet<string>();
var result = new FixedStrategyNestingEngine("Default").Solve(
var result = new FixedStrategyNestingEngine("Fill").Solve(
ProgressJob(),
new StageCollector(stages, legacyPhases)
);
@@ -27,7 +27,7 @@ public class NestJobEngineSelectionTests
[Fact]
public void FactoryResolvesEachNamedBuiltInStrategy()
{
var defaultNester = PlateNesterFactory.Create("Default");
var defaultNester = PlateNesterFactory.Create("Fill");
var stripNester = PlateNesterFactory.Create("Strip");
var verticalNester = PlateNesterFactory.Create("Vertical Remnant");
var horizontalNester = PlateNesterFactory.Create("Horizontal Remnant");
@@ -145,7 +145,7 @@ public class NestJobExampleTests
quadrant: 1
),
},
new NestJobOptions("Default", maxPlates: 1)
new NestJobOptions("Fill", maxPlates: 1)
);
var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
@@ -172,7 +172,7 @@ public class NestJobExampleTests
var item = new NestItem { Drawing = drawing, Quantity = 1 };
var parts = PlateFillService.Nest(
"Default",
"Fill",
plate,
new List<NestItem> { item },
progress: null,
@@ -260,7 +260,7 @@ public class NestJobGeometryTests
}
[Theory]
[InlineData("Default")]
[InlineData("Fill")]
[InlineData("Strip")]
public void RealEngineSmokeCasesPreserveInputAndProduceSafeAccounting(string strategy)
{
@@ -105,7 +105,7 @@ public class NestJobRunnerTests
Assert.Equal(100, job.Plates[0].Size.Width);
Assert.Equal(1, job.Plates[0].EdgeSpacing.Left);
Assert.Equal(0, job.Plates[0].Quantity);
Assert.Equal("Default", job.Options.PlacementStrategy);
Assert.Equal("Fill", job.Options.PlacementStrategy);
Assert.Throws<NotSupportedException>(() => ((IList<NestJobPart>)job.Parts).Clear());
}
@@ -43,7 +43,7 @@ public class NestPipelineTests
var item = Item("bracket", 10);
var codes = item.Drawing.Program.Codes.Count;
var result = NestPipeline.Run(Request("Default", item));
var result = NestPipeline.Run(Request("Fill", item));
Assert.True(result.IsValid, string.Join("; ", result.Violations));
Assert.True(result.CanKeep);
@@ -240,7 +240,7 @@ public class NestPipelineTests
NestPipeline.Run(Request("Mystery Engine", Item("bracket", 1)))
);
Assert.Contains("Default", error.Message);
Assert.Contains("Fill", error.Message);
}
[Fact]
@@ -265,7 +265,7 @@ public class NestPipelineTests
cts.Cancel();
Assert.ThrowsAny<OperationCanceledException>(() =>
NestPipeline.Run(Request("Default", Item("bracket", 1)), null, cts.Token)
NestPipeline.Run(Request("Fill", Item("bracket", 1)), null, cts.Token)
);
}
}
@@ -12,7 +12,7 @@ public class NestingEngineRegistryTests
Assert.Contains("Rectangles", names);
Assert.Contains("Irregular", names);
Assert.Contains("Default", names);
Assert.Contains("Fill", names);
Assert.Contains("Strip", names);
Assert.Contains("Vertical Remnant", names);
Assert.Contains("Horizontal Remnant", names);
@@ -43,7 +43,7 @@ public class NestingEngineRegistryTests
var before = NestingEngineRegistry.AvailableEngines.Count;
NestingEngineRegistry.Register("Opus55NestingEngine", "stale plug-in",
() => new FixedStrategyNestingEngine("Default"));
() => new FixedStrategyNestingEngine("Fill"));
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
Assert.Equal("Irregular", NestingEngineRegistry.ResolveName("Opus55NestingEngine"));
@@ -67,9 +67,9 @@ public class NestingEngineRegistryTests
var before = NestingEngineRegistry.AvailableEngines.Count;
NestingEngineRegistry.Register(
"Default",
"Fill",
"duplicate",
() => new FixedStrategyNestingEngine("Default")
() => new FixedStrategyNestingEngine("Fill")
);
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
@@ -78,7 +78,7 @@ public class NestingEngineRegistryTests
[Fact]
public void CreateResolvesKnownNamesCaseInsensitivelyAndRejectsUnknown()
{
Assert.NotNull(NestingEngineRegistry.Create("default"));
Assert.NotNull(NestingEngineRegistry.Create("fill"));
Assert.NotNull(NestingEngineRegistry.Create("Vertical Remnant"));
Assert.NotNull(NestingEngineRegistry.Create("stockladder"));
@@ -8,7 +8,7 @@ namespace OpenNest.Engine.Tests.Jobs;
public class PlateFillerContractTests
{
[Theory]
[InlineData("Default")]
[InlineData("Fill")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
public void StandardPlateFiller_Fill_ReturnsPartsBoundToInputDrawing(string strategy)
@@ -353,7 +353,7 @@ public class PlateFillerContractTests
private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch
{
"Default" => new DefaultPlateFiller(plate),
"Fill" => new DefaultPlateFiller(plate),
"Vertical Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical),
"Horizontal Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal),
_ => throw new ArgumentOutOfRangeException(nameof(strategy)),
@@ -22,7 +22,7 @@ public class PlateNesterParityTests
private static NestJob Job(
IReadOnlyList<NestJobPart> parts,
int? stockQuantity = 3,
string strategy = "Default"
string strategy = "Fill"
)
{
var stock = new NestPlateStock("stock", PlateSize, stockQuantity, 1, Edge);
@@ -97,7 +97,7 @@ public class PlateNesterParityTests
[Fact]
public void Builtins_AreResolvedByProductionFactory()
{
Assert.IsType<DefaultPlateNester>(PlateNesterFactory.Create("Default"));
Assert.IsType<DefaultPlateNester>(PlateNesterFactory.Create("Fill"));
Assert.IsType<StripPlateNester>(PlateNesterFactory.Create("Strip"));
Assert.IsType<RemnantPlateNester>(PlateNesterFactory.Create("Vertical Remnant"));
Assert.IsType<RemnantPlateNester>(PlateNesterFactory.Create("Horizontal Remnant"));
@@ -234,7 +234,7 @@ public class PlateNesterParityTests
[Fact]
public void RemnantRestrictedRotation_NeverTouchesFiller()
{
// Same safety rule for the remnant nester, whose fillers inherit the Default pipeline's
// Same safety rule for the remnant nester, whose fillers inherit the Fill pipeline's
// automatic-rotation limitation. The throwing factory proves the filler is never
// constructed; completion at the locked angle proves OrderedPlateNester handled it.
var part = new NestJobPart(
@@ -9,7 +9,7 @@ public class PlateFillServiceTests
{
private static readonly string[] Strategies =
[
"Default",
"Fill",
"Strip",
"Vertical Remnant",
"Horizontal Remnant",
@@ -44,7 +44,7 @@ public class PlateFillServiceTests
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
var parts = PlateFillService.FillItem(
"Default",
"Fill",
plate,
new NestItem { Drawing = drawing, Quantity = 6 },
plate.WorkArea(),
@@ -120,7 +120,7 @@ public class PlateFillServiceTests
// The plateNumber overload reports the caller's plate index like the legacy
// engine's PlateNumber did for interactive multi-plate loops.
var progress = new CapturingProgress();
var parts = PlateFillService.Nest("Default", plate, items, 3, progress, CancellationToken.None);
var parts = PlateFillService.Nest("Fill", plate, items, 3, progress, CancellationToken.None);
Assert.NotEmpty(parts);
Assert.NotEmpty(progress.Reports);
@@ -217,7 +217,7 @@ public class PlateFillServiceTests
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
Assert.Throws<ArgumentNullException>(() =>
PlateFillService.FillItem("Default", null!, new NestItem { Drawing = drawing, Quantity = 1 }, new Box(0, 0, 10, 10), null, CancellationToken.None)
PlateFillService.FillItem("Fill", null!, new NestItem { Drawing = drawing, Quantity = 1 }, new Box(0, 0, 10, 10), null, CancellationToken.None)
);
}
@@ -61,8 +61,8 @@ public class AreaPackerTests
var area = plate.WorkArea();
var items = new List<NestItem> { Item("panel", 10 + SheetPacker.OverhangAllowance * 0.8, 4, 2) };
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
var again = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
var parts = PlateFillService.PackArea("Fill", plate, area, items, null, CancellationToken.None);
var again = PlateFillService.PackArea("Fill", plate, area, items, null, CancellationToken.None);
Assert.Equal(2, parts.Count);
Assert.Equal(parts.Select(p => (p.Location, p.Rotation)), again.Select(p => (p.Location, p.Rotation)));
@@ -92,7 +92,7 @@ public class AreaPackerTests
};
var area = plate.WorkArea();
var items = new List<NestItem> { Item("panel", 4, 10 + SheetPacker.OverhangAllowance * 0.8, 2) };
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
var parts = PlateFillService.PackArea("Fill", plate, area, items, null, CancellationToken.None);
Assert.Equal(2, parts.Count);
Assert.All(parts, p =>
@@ -117,7 +117,7 @@ public class AreaPackerTests
var items = new List<NestItem> { rotated ? Item("panel", 6 + excess, 10 + excess, 1)
: Item("panel", 10 + excess, 6 + excess, 1) };
var part = Assert.Single(PlateFillService.PackArea("Default", plate, plate.WorkArea(), items, null, CancellationToken.None));
var part = Assert.Single(PlateFillService.PackArea("Fill", plate, plate.WorkArea(), items, null, CancellationToken.None));
Assert.True(part.BoundingBox.Right <= 10 + NestTolerances.WorkAreaSlack);
Assert.True(part.BoundingBox.Top <= 6 + NestTolerances.WorkAreaSlack);
@@ -130,7 +130,7 @@ public class AreaPackerTests
{
var plate = new Plate(new Size(6, 10)) { PartSpacing = spacing };
var items = new List<NestItem> { Item("panel", 10 + NestTolerances.WorkAreaSlack * 1.1, 4, 1) };
Assert.Empty(PlateFillService.PackArea("Default", plate, plate.WorkArea(), items, null, CancellationToken.None));
Assert.Empty(PlateFillService.PackArea("Fill", plate, plate.WorkArea(), items, null, CancellationToken.None));
}
[Theory]
@@ -144,7 +144,7 @@ public class AreaPackerTests
var area = new Box(x, y, w, h);
var excess = SheetPacker.OverhangAllowance * 0.8;
var items = new List<NestItem> { w == 5 ? Item("panel", w + excess, h - 0.25, 1) : Item("panel", 8, h + excess, 1) };
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
var parts = PlateFillService.PackArea("Fill", plate, area, items, null, CancellationToken.None);
Assert.Equal(fits ? 1 : 0, parts.Count);
}
@@ -181,7 +181,7 @@ public class AreaPackerTests
var area = new Box(0, 0, 12, 12);
var items = new List<NestItem> { Item("tall", 5, 7, 3), Item("small", 4, 3, 2) };
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
var parts = PlateFillService.PackArea("Fill", plate, area, items, null, CancellationToken.None);
Assert.Equal(5, parts.Count);
Assert.Equal(3, parts.Count(p => p.BaseDrawing.Name == "tall"));
@@ -100,7 +100,7 @@ internal static class CutoutLatticeFill
.Select((p, index) => p with { InstanceIndex = index }).ToArray();
}
/// <summary>Default Fill over the cutout's bounds grown by one step on every side, in frame
/// <summary>Fill over the cutout's bounds grown by one step on every side, in frame
/// coordinates. Fill places whole parts only, so the margin keeps every offset covered.</summary>
private static List<NestJobPlacement> Lattice(NestJobPart insert, Box bounds, double step, double spacing,
CancellationToken token)
+1 -1
View File
@@ -6,7 +6,7 @@ namespace OpenNest.Engine.Jobs;
public sealed class NestJobOptions
{
public NestJobOptions(
string placementStrategy = "Default",
string placementStrategy = "Fill",
int? maxPlates = null,
double salvageRate = 0,
double minimumSalvageDimension = 0
@@ -28,6 +28,7 @@ public static class NestingEngineRegistry
{
["Opus55NestingEngine"] = "Irregular",
["RectanglesNestingEngine"] = "Rectangles",
["Default"] = "Fill",
};
static NestingEngineRegistry()
@@ -51,9 +52,9 @@ public static class NestingEngineRegistry
);
Register(
"Default",
"Fill",
"Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
() => new FixedStrategyNestingEngine("Default")
() => new FixedStrategyNestingEngine("Fill")
);
Register(
@@ -15,10 +15,16 @@ namespace OpenNest.Engine.Jobs.Placement;
/// </summary>
public static class PlateFillService
{
/// <summary>Strategy used when a caller selection names none.</summary>
public const string DefaultStrategy = "Fill";
/// <summary>Name of the Fill strategy in earlier releases; accepted as an alias.</summary>
private const string LegacyFillName = "Default";
/// <summary>The four built-in strategy names, in registry display order.</summary>
public static IReadOnlyList<string> BuiltInStrategies { get; } =
[
"Default",
"Fill",
"Strip",
"Vertical Remnant",
"Horizontal Remnant",
@@ -91,9 +97,10 @@ public static class PlateFillService
}
/// <summary>
/// Resolves a caller-supplied strategy name: null or empty means "Default"; otherwise the name
/// Resolves a caller-supplied strategy name: null or empty means "Fill"; otherwise the name
/// must match a built-in strategy, matched case-insensitively like the legacy registry's
/// ActiveEngineName so tolerant interactive callers keep working. Returns the canonical name;
/// ActiveEngineName so tolerant interactive callers keep working. "Default", the Fill
/// strategy's name in earlier releases, still resolves to "Fill". Returns the canonical name;
/// unknown names throw <see cref="NotSupportedException"/>.
/// </summary>
public static string ResolveStrategy(string strategy) => ResolveStrategy(strategy, true);
@@ -105,12 +112,14 @@ public static class PlateFillService
if (string.IsNullOrWhiteSpace(strategy))
{
if (allowEmpty)
return "Default";
return DefaultStrategy;
throw new NotSupportedException(
$"Unknown placement strategy: '{strategy}'. Known strategies: {string.Join(", ", BuiltInStrategies)}."
);
}
if (strategy.Equals(LegacyFillName, StringComparison.OrdinalIgnoreCase))
return DefaultStrategy;
foreach (var candidate in BuiltInStrategies)
{
if (candidate.Equals(strategy, StringComparison.OrdinalIgnoreCase))
@@ -122,13 +131,13 @@ public static class PlateFillService
);
}
/// <summary>Builds the filler for an optional strategy (null/empty = Default).</summary>
/// <summary>Builds the filler for an optional strategy (null/empty = Fill).</summary>
private static PlateFillerBase CreateFiller(string strategy, Plate plate)
{
ArgumentNullException.ThrowIfNull(plate);
return ResolveStrategy(strategy) switch
{
"Default" => new DefaultPlateFiller(plate),
"Fill" => new DefaultPlateFiller(plate),
"Strip" => new StripPlateFiller(plate),
"Vertical Remnant" => new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical),
_ => new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal),
@@ -140,7 +149,7 @@ public static class PlateFillService
{
ArgumentNullException.ThrowIfNull(strategy);
// An explicit empty string is an unknown strategy; only ResolveStrategy(string),
// used for caller selections, maps null/empty to Default.
// used for caller selections, maps null/empty to Fill.
ResolveStrategy(strategy, allowEmpty: false);
return CreateFiller(strategy, plate);
}
@@ -10,7 +10,7 @@ using OpenNest.Geometry;
namespace OpenNest.Engine.Jobs.Placement;
/// <summary>
/// Runs the Default Fill for one private drawing on a private plate, for callers that turn the
/// Runs the Fill strategy for one private drawing on a private plate, for callers that turn the
/// result into job poses. The caller owns the drawing and must call
/// <see cref="BestFitCache.Invalidate"/> for it when finished.
/// </summary>
@@ -13,8 +13,8 @@ namespace OpenNest.Engine.Jobs.Placement;
/// <see cref="DefaultPlateNester"/>.
/// </summary>
/// <remarks>
/// The remnant fillers share the Default pipeline's automatic-rotation limitation, so non-automatic
/// requirements route through <see cref="OrderedPlateNester"/> exactly as they do for Default.
/// The remnant fillers share the Fill pipeline's automatic-rotation limitation, so non-automatic
/// requirements route through <see cref="OrderedPlateNester"/> exactly as they do for Fill.
/// </remarks>
public sealed class RemnantPlateNester : IPlateNester
{
@@ -45,7 +45,7 @@ public sealed class RemnantPlateNester : IPlateNester
ArgumentNullException.ThrowIfNull(request);
token.ThrowIfCancellationRequested();
// Same safety rule as DefaultPlateNester: the remnant fillers inherit the Default pipeline's
// Same safety rule as DefaultPlateNester: the remnant fillers inherit the Fill pipeline's
// automatic-rotation limitation, so any non-automatic requirement goes to the policy-aware
// ordered nester.
if (request.Parts.Any(part => part.Rotation.Kind != RotationPolicyKind.Automatic))
+2 -1
View File
@@ -15,7 +15,8 @@ public static class PlateNesterFactory
ArgumentNullException.ThrowIfNull(strategy);
return strategy switch
{
"Default" => new DefaultPlateNester(),
// "Default" is the strategy's name in earlier releases.
"Fill" or "Default" => new DefaultPlateNester(),
"Strip" => new StripPlateNester(),
"Vertical Remnant" => RemnantPlateNester.Vertical(),
"Horizontal Remnant" => RemnantPlateNester.Horizontal(),
+4 -4
View File
@@ -31,7 +31,7 @@ namespace OpenNest.Mcp.Tools
[Description("Index of the plate to fill")] int plateIndex,
[Description("Name of the drawing to fill with")] string drawingName,
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0,
[Description("Placement strategy: Default, Strip, Vertical Remnant, Horizontal Remnant")]
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
string engine = null
)
{
@@ -84,7 +84,7 @@ namespace OpenNest.Mcp.Tools
[Description("Width of the area")] double width,
[Description("Length of the area")] double length,
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0,
[Description("Placement strategy: Default, Strip, Vertical Remnant, Horizontal Remnant")]
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
string engine = null
)
{
@@ -134,7 +134,7 @@ namespace OpenNest.Mcp.Tools
[Description("Index of the plate")] int plateIndex,
[Description("Name of the drawing to fill with")] string drawingName,
[Description("Maximum quantity per remnant (0 = unlimited)")] int quantity = 0,
[Description("Placement strategy: Default, Strip, Vertical Remnant, Horizontal Remnant")]
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
string engine = null
)
{
@@ -197,7 +197,7 @@ namespace OpenNest.Mcp.Tools
[Description("Index of the plate")] int plateIndex,
[Description("Comma-separated drawing names")] string drawingNames,
[Description("Comma-separated quantities for each drawing")] string quantities,
[Description("Placement strategy: Default, Strip, Vertical Remnant, Horizontal Remnant")]
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
string engine = null
)
{
@@ -10,7 +10,7 @@ public class NestLayoutCheckEquivalenceTests
{
[Theory]
[InlineData("StockLadder")]
[InlineData("Default")]
[InlineData("Fill")]
[InlineData("Strip")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
@@ -38,7 +38,7 @@ public class NativeUClearanceTests
PartSpacing = 0.25,
EdgeSpacing = new Spacing(1, 1),
};
var parts = PlateFillService.FillItem("Default", plate,
var parts = PlateFillService.FillItem("Fill", plate,
new NestItem { Drawing = drawing, Quantity = quantity }, plate.WorkArea(),
null, CancellationToken.None);
+1 -1
View File
@@ -28,7 +28,7 @@ public class EngineOverlapTests
}
[Theory]
[InlineData("Default")]
[InlineData("Fill")]
[InlineData("Strip")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
@@ -26,9 +26,9 @@ public class ExplicitPlacementStrategyTests
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ResolveStrategy_UnsetMeansDefault(string strategy)
public void ResolveStrategy_UnsetMeansFill(string strategy)
{
Assert.Equal("Default", PlateFillService.ResolveStrategy(strategy));
Assert.Equal("Fill", PlateFillService.ResolveStrategy(strategy));
}
[Theory]
@@ -63,7 +63,7 @@ public class ActionSelectAreaCutOffTests
program.LineTo(3, 0);
program.LineTo(0, 0);
// ActionFillArea passes SelectedArea directly to this service.
var parts = PlateFillService.FillItem("Default", plate,
var parts = PlateFillService.FillItem("Fill", plate,
new NestItem { Drawing = new Drawing("square", program) }, area, null, CancellationToken.None);
Assert.NotEmpty(parts);
Assert.All(parts, part =>
+3 -3
View File
@@ -13,7 +13,7 @@ namespace OpenNest.Forms
/// App-scoped nesting-engine selection. The selected name addresses a whole-job
/// INestingEngine resolved through NestingEngineRegistry at call time; single-plate
/// interactive fill uses FillStrategy, which maps a built-in engine to its placement
/// strategy and falls back to Default for jobs-only engines (StockLadder, plug-ins).
/// strategy and falls back to Fill for jobs-only engines (StockLadder, plug-ins).
/// </summary>
public static class EngineSelection
{
@@ -67,8 +67,8 @@ namespace OpenNest.Forms
.ToList();
/// <summary>Placement strategy for single-plate fill: the selection when it names a
/// built-in strategy, otherwise Default.</summary>
public static string FillStrategy => IsFillStrategy(engineName) ? engineName : DefaultEngineName;
/// built-in strategy, otherwise Fill.</summary>
public static string FillStrategy => IsFillStrategy(engineName) ? engineName : PlateFillService.DefaultStrategy;
public static bool IsFillStrategy(string name) =>
!string.IsNullOrWhiteSpace(name)
+2 -2
View File
@@ -64,7 +64,7 @@ Key options: `--size WxL`, `--autonest` (validated single-sheet whole-job nestin
```bash
dotnet run --project OpenNest.Benchmark -- ./benchmark-jobs \
--sheet-sizes 48x96,60x120,72x120 --engines Default,StockLadder --csv results.csv
--sheet-sizes 48x96,60x120,72x120 --engines Irregular,Rectangles --csv results.csv
```
Layouts are validated (bounds, spacing, quantity, rotation, stock match); invalid runs place nothing and pay the penalty. `--parallel` (default 3) speeds up scoring but inflates `Time(ms)` — use `--parallel 1` when comparing speed. Pass `--sheet-sizes` for an unbiased run; otherwise only each file's original sizes are offered. `--progress` logs each solve's start, the engine's `NestJobProgress` (plate evaluations throttled to one line per 2 s, every plate commit) and its finish. Custom engines drop in as DLLs implementing `INestingEngine` (public parameterless constructor) in an `Engines/` folder next to the benchmark. Built-in engines and how to change them: [nesting engines](docs/nesting-engines.md).
@@ -93,7 +93,7 @@ Engines implement `INestingEngine.Solve(NestJob)`. Desktop Auto Nest, console au
|--------|-------------|
| **Rectangles** | Plain and near-rectangular plates: maximal-rectangles box packing |
| **Irregular** | Irregular profiles: no-fit-polygon frontier packing |
| **Default** | Multi-phase: linear fill → pairs → rect best-fit → extents |
| **Fill** | Multi-phase: linear fill → pairs → rect best-fit → extents (named Default in earlier releases) |
| **Strip** | Iterative shrink-fill for mixed-drawing layouts |
| **Vertical / Horizontal Remnant** | Optimizes a clean remnant drop on one edge |
| **StockLadder** | Whole-job, stock-constrained baseline with salvage-credit ranking |
+1 -1
View File
@@ -18,7 +18,7 @@ Fresh-process import/cache/materialization probes used a 24-by-24 sheet, one-inc
| --- | --- | --- |
| Smallest-envelope kept pair, minimum raw clearance | 0.22548054744486495 | 0.24999929486961545 |
| `PairFiller`, 70 parts, minimum raw clearance | 0.22548054744486434 | 0.24999929486961417 |
| `PlateFillService.FillItem("Default")`, 70 parts, minimum raw clearance | 0.2254805474448648 | 0.24999929486961447 |
| `PlateFillService.FillItem("Fill")`, 70 parts, minimum raw clearance | 0.2254805474448648 | 0.24999929486961447 |
Measurements use raw material outlines tessellated at 1e-6 chord tolerance and independent Shapely boundary distances, not the slide solver or its offset contours. The roughly 7e-7 shortfall is within tessellation error. These specific pair/grid polygons were valid; no geometry repair was applied. The corrected top pair passes the existing validator. The full grids do **not** pass it; see below.
+11 -7
View File
@@ -14,7 +14,7 @@ in `OpenNest.Engine/NestingEngines/<Name>/`, its tests in `OpenNest.Engine.Tests
| Rectangles | Plain and near-rectangular plates | Each part packed as the box of its material at its minimum-area rotation, using a maximal-rectangles free list; stock chosen sheet by sheet by salvage-credited look-ahead cost |
| Irregular | Irregular profiles | No-fit-polygon frontier packing with gap filling and best-fit pairs, six whole-job strategy variants and a tail re-plan |
| StockLadder | Caller-supplied stock ladders | Constrained-first fill with equivalent-demand area repacking |
| Default, Strip, Vertical Remnant, Horizontal Remnant | Single-strategy fills | The fixed placement strategies behind interactive fill |
| Fill, Strip, Vertical Remnant, Horizontal Remnant | Single-strategy fills | The fixed placement strategies behind interactive fill; Fill is the multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder) |
Rectangles places irregular parts validly, but only as their bounding boxes; it never nests into a
notch or hole. Box sides account for how the layout check flattens arcs, so round-edged parts stay
@@ -44,8 +44,8 @@ with a single boundary/containment union.
Any remaining numerical hole is filled only when its entire ring is certified to lie in forbidden
space, preserving genuine enclosed placement pockets without changing spacing tolerances.
When remaining demand exceeds two, Irregular also offers Default Fill patterns as optional
multi-member candidates, not as solid bounding boxes or a whole-job Default fallback. It searches
When remaining demand exceeds two, Irregular also offers Fill patterns as optional
multi-member candidates, not as solid bounding boxes or a whole-job Fill fallback. It searches
the empty work area and physical leftover space for up to two high-area rectangles. Occupied
outlines are expanded by part spacing before rectangle search. Each sheet prepares blocks initially
and after its first placement, for up to four high-demand-area types; each type has at most eight
@@ -67,7 +67,7 @@ engine, so every engine benefits. Nothing calls it yet: a part inside a cutout m
the cutout's contour, and the sequencer does not enforce that order.
`CutoutLatticeFill` (`OpenNest.Engine/Jobs/Cutouts/`) fills one closed cutout with copies of one
part. It runs Default Fill over the cutout's bounds plus one part step on every side, then shifts
part. It runs Fill over the cutout's bounds plus one part step on every side, then shifts
that lattice across a grid of offsets of up to half a step each way. At each offset it keeps the
copies whose spacing-grown outline lies inside the cutout, using the part's inner-fit region of
the inscribed, flattened cutout, and the offset keeping the most copies wins. Every returned pose
@@ -78,16 +78,20 @@ calls for some parts, so results are not yet guaranteed identical between runs.
## Renamed engines
Earlier releases shipped these as plug-ins under other names. The registry maps the old names so
saved desktop selections, scripts and API requests keep working:
The registry maps names used by earlier releases (the first two shipped as plug-ins) so saved
desktop selections, scripts and API requests keep working:
| Old name | Now |
|---|---|
| `Opus55NestingEngine` | Irregular |
| `RectanglesNestingEngine` | Rectangles |
| `Default` | Fill |
Fill strategy calls (interactive fill, `PlateFillService`, console fill without `--autonest`, MCP fill
tools) also accept `Default` as Fill.
Gpt6Astra and Qwen38FlashNext are no longer shipped and have no alias. A saved selection of either
falls back to Default with the usual status-bar warning.
falls back to the default engine with the usual status-bar warning.
## Changing an engine
+1 -1
View File
@@ -18,7 +18,7 @@ Irregular's private block proposals opt into quantity-limited Row/Column stripe
perpendicular copies stop after reaching the requested count. The final fill still
trims its winner to the exact quantity. Existing overlap fallbacks and block spacing
certification remain active; fallback tiling can exceed the soft limit. Nonpositive
limits remain uncapped. Ordinary Default fills and the zero-quantity cutout lattice
limits remain uncapped. Ordinary Fill-strategy fills and the zero-quantity cutout lattice
keep their full-area behavior.
This can change which private block candidate wins; it is not a promise of identical
+1 -1
View File
@@ -20,7 +20,7 @@ try
var create = registry.GetMethod("Create")!;
// Every built-in engine the desktop offers must instantiate from the packaged assembly.
string[] expected = ["Rectangles", "Irregular", "StockLadder", "Default", "Strip", "Vertical Remnant", "Horizontal Remnant"];
string[] expected = ["Rectangles", "Irregular", "StockLadder", "Fill", "Strip", "Vertical Remnant", "Horizontal Remnant"];
foreach (var name in expected)
{
var engine = create.Invoke(null, [name])!;