refactor(engine): remove legacy nesting engine surface
This commit is contained in:
@@ -41,13 +41,13 @@ Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
||||
|
||||
### OpenNest.Engine (class library, depends on Core)
|
||||
Nesting algorithms provide both a legacy single-plate API and a whole-job API. The legacy path centers on `NestEngineBase`, `DefaultNestEngine` (formerly `NestEngine`), and the global `NestEngineRegistry`. New job callers use immutable, ID-based contracts in `Jobs/`: `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named strategy without reading or changing the process-global registry.
|
||||
Nesting algorithms use the jobs-only API. `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named built-in placement strategy.
|
||||
|
||||
- **Whole-job API (`Jobs/`)**: `NestJob` owns part requirements, physical stock, and options for one material/thickness/unit system. `PartGeometrySnapshot` contains owned flat rapid/line/arc geometry; results contain stock IDs and placement poses (radians), not mutable desktop models. `NestJobPlacementValidator` validates contours, rotation, usable work area, overlap, and spacing before accounting commits. The runner selects valid trial candidates greedily by priority vector, sheet area, envelope, and input order; an incomplete result reports why but does not prove geometric impossibility. `DrawingJobMapper` and `NestResultMaterializer` are the domain-boundary adapters.
|
||||
- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester` and `StripPlateNester` are migrated built-ins with run-scoped private geometry; `LegacyPlateNesterAdapter` remains for remnant strategies and legacy plugins/callers during rollout. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` retains legacy name-based helpers and is deliberately outside the runner path.
|
||||
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. Existing desktop, CLI, and MCP callers remain on this compatibility path until separate migrations preserve their existing-plate, preview, and accept/cancel semantics.
|
||||
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
|
||||
- **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency.
|
||||
- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester`, `StripPlateNester`, and `RemnantPlateNester` are built-ins with run-scoped private geometry. `PlateFillService` is the public single-plate proposal service for interactive fill/group/pack flows; it returns parts without mutating caller-owned plates. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` retains name-based helpers and remains outside the runner path.
|
||||
- **Filler pipeline (`Jobs/Placement/Fillers/`)**: internal `DefaultPlateFiller`, `StripPlateFiller`, and policy-backed `RemnantPlateFiller` implement the standard single-plate geometry pipeline. `Default` runs the Linear, Pairs, RectBestFit, and Extents phases; remnant variants preserve their distinct comparer, direction, trim-axis, and angle-ordering policies.
|
||||
- **Engine registration**: `NestingEngineRegistry` holds whole-job `INestingEngine` implementations including the four fixed strategies and `StockLadder`. It loads plug-ins that implement `INestingEngine` and have a public parameterless constructor. Plug-ins for the removed single-plate inheritance API are not binary compatible.
|
||||
- **IFillComparer**: Interface enabling filler-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), and `HorizontalRemnantComparer` (minimize Y-extent) are grouped into `FillPolicy` on `FillContext`.
|
||||
- **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`.
|
||||
- **Strategies/** (`namespace OpenNest.Engine.Strategies`): Pluggable fill strategy layer — `IFillStrategy` interface, `FillContext`, `FillStrategyRegistry` (auto-discovers strategies via reflection, supports plugin DLLs), `FillHelpers`. Built-in strategies: `LinearFillStrategy`, `PairsFillStrategy`, `RectBestFitStrategy`, `ExtentsFillStrategy`.
|
||||
- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
@@ -6,15 +5,14 @@ using OpenNest.Engine.Jobs.Adapters;
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Strategy selection must be instance-scoped: explicit engine choices work without touching the
|
||||
/// process-global NestEngineRegistry.ActiveEngineName, and unknown strategies are rejected.
|
||||
/// Strategy selection is instance-scoped: explicit engine choices complete independently,
|
||||
/// and unknown strategies are rejected.
|
||||
/// </summary>
|
||||
public class NestJobEngineSelectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExplicitDefaultAndStripSelectionsDoNotTouchGlobalRegistry()
|
||||
public void ExplicitDefaultAndStripSelectionsCompleteIndependently()
|
||||
{
|
||||
var original = NestEngineRegistry.ActiveEngineName;
|
||||
var job = FiniteStockJobTests.Job(1);
|
||||
|
||||
var defaultResult = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
|
||||
@@ -24,14 +22,11 @@ public class NestJobEngineSelectionTests
|
||||
new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))
|
||||
);
|
||||
Assert.Equal(NestJobStatus.Complete, stripResult.Status);
|
||||
|
||||
Assert.Equal(original, NestEngineRegistry.ActiveEngineName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FactoryResolvesNamedEnginesWithoutGlobalState()
|
||||
public void FactoryResolvesEachNamedBuiltInStrategy()
|
||||
{
|
||||
var original = NestEngineRegistry.ActiveEngineName;
|
||||
var defaultNester = PlateNesterFactory.Create("Default");
|
||||
var stripNester = PlateNesterFactory.Create("Strip");
|
||||
var verticalNester = PlateNesterFactory.Create("Vertical Remnant");
|
||||
@@ -42,7 +37,6 @@ public class NestJobEngineSelectionTests
|
||||
Assert.NotNull(verticalNester);
|
||||
Assert.NotNull(horizontalNester);
|
||||
Assert.NotSame(defaultNester, stripNester);
|
||||
Assert.Equal(original, NestEngineRegistry.ActiveEngineName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -57,22 +51,6 @@ public class NestJobEngineSelectionTests
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyRegistryPluginsDoNotLeakIntoJobSelection()
|
||||
{
|
||||
// A plugin engine registered through the legacy registry must not become selectable
|
||||
// through the job factory; the new boundary is independent of registry state.
|
||||
NestEngineRegistry.Register(
|
||||
"ProbePlugin",
|
||||
"test plugin",
|
||||
plate => new PluginShapeEngine(plate)
|
||||
);
|
||||
Assert.Contains(NestEngineRegistry.AvailableEngines, e => e.Name == "ProbePlugin");
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => PlateNesterFactory.Create("ProbePlugin"));
|
||||
Assert.NotNull(PlateNesterFactory.Create("Default"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripEngineEndToEndPlacesAndAccounts()
|
||||
{
|
||||
@@ -89,10 +67,4 @@ public class NestJobEngineSelectionTests
|
||||
foreach (var f in result.Fulfillment)
|
||||
Assert.Equal(f.Requested, f.Placed + f.Unplaced);
|
||||
}
|
||||
|
||||
private sealed class PluginShapeEngine(Plate plate) : NestEngineBase(plate)
|
||||
{
|
||||
public override string Name => "ProbePlugin";
|
||||
public override string Description => "registered via legacy registry only";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Placement;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Runnable end-to-end example of the whole-job engine API: multiple part requirements, multiple plate
|
||||
/// sizes, and an enumeration of every returned plate, placement, leftover, and stock line. Also the
|
||||
/// documentation checkpoint for the legacy caller boundaries that have not been migrated (task 8).
|
||||
/// documentation checkpoint for the public single-plate placement service used by interactive flows.
|
||||
/// </summary>
|
||||
public class NestJobExampleTests
|
||||
{
|
||||
@@ -160,36 +161,27 @@ public class NestJobExampleTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy caller boundaries documented for task 8 — these paths still use the old single-plate
|
||||
/// engine entry points and are deliberately NOT migrated in this slice. Verified against source at
|
||||
/// the time of writing:
|
||||
/// - Desktop UI: OpenNest/Forms/MainForm.cs RunAutoNestAsync (~line 1004) and NestSinglePlateAsync
|
||||
/// (~line 1087) orchestrate plate-first and part-first fills directly against NestEngineRegistry
|
||||
/// engines. Migration requires preserving populated-plate editing, preview routing, and
|
||||
/// Accept-versus-Cancel semantics — a separate adapter design (documented follow-on).
|
||||
/// - CLI: OpenNest.Console/Program.cs calls engine.Nest(...) (~line 316) on one plate. Migration
|
||||
/// point: build a NestJob from imported drawings plus CLI plate options and call Solve once.
|
||||
/// - MCP: OpenNest.Mcp/Tools/NestingTools.cs calls engine.Nest(...) (~line 239) on the session
|
||||
/// plate. Migration point: same single job call, materialized through NestResultMaterializer.
|
||||
/// The public API (OpenNest.Api NestRunner) already delegates to NestJobRunner.Solve (task 6).
|
||||
/// This test exercises the legacy compatibility signature so an accidental removal of that entry
|
||||
/// point breaks the documented contract.
|
||||
/// The public placement service is the single-plate contract for preview-driven flows. It proposes
|
||||
/// parts without mutating the caller's plate, so the UI can accept or discard them explicitly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LegacyCompatibilityEntryPointsStillExist()
|
||||
public void PlateFillService_ProposesSinglePlatePartsWithoutMutatingPlate()
|
||||
{
|
||||
var plate = new Plate { Size = new Size(300.0, 200.0), Quadrant = 1 };
|
||||
var drawing = new Drawing("legacy", TestDrawingFactory.Rectangle(50.0, 50.0));
|
||||
var drawing = new Drawing("preview", TestDrawingFactory.Rectangle(50.0, 50.0));
|
||||
var item = new NestItem { Drawing = drawing, Quantity = 1 };
|
||||
|
||||
// MainForm/Console/MCP still reach the legacy single-plate signature unchanged; the engine
|
||||
// returns placed Parts for the caller to attach (legacy paths do not attach on their own).
|
||||
var engine = NestEngineRegistry.Create(plate);
|
||||
var parts = engine.Nest(new List<NestItem> { item }, null, CancellationToken.None);
|
||||
var parts = PlateFillService.Nest(
|
||||
"Default",
|
||||
plate,
|
||||
new List<NestItem> { item },
|
||||
progress: null,
|
||||
token: CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.NotNull(engine);
|
||||
var placed = Assert.Single(parts);
|
||||
Assert.Same(drawing, placed.BaseDrawing);
|
||||
Assert.Empty(plate.Parts);
|
||||
}
|
||||
|
||||
private static NestJobPart Part(
|
||||
|
||||
@@ -11,55 +11,31 @@ public class PlateFillerContractTests
|
||||
[InlineData("Default")]
|
||||
[InlineData("Vertical Remnant")]
|
||||
[InlineData("Horizontal Remnant")]
|
||||
public void StandardPlateFiller_Fill_MatchesCompatibilityFacade(string strategy)
|
||||
public void StandardPlateFiller_Fill_ReturnsPartsBoundToInputDrawing(string strategy)
|
||||
{
|
||||
var directPlate = new Plate(new Size(30, 50));
|
||||
var facadePlate = new Plate(new Size(30, 50));
|
||||
var directDrawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
|
||||
var facadeDrawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
|
||||
var directFiller = CreateFiller(strategy, directPlate);
|
||||
var facade = CreateFacade(strategy, facadePlate);
|
||||
var plate = new Plate(new Size(30, 50));
|
||||
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
|
||||
var filler = CreateFiller(strategy, plate);
|
||||
|
||||
var directParts = directFiller.Fill(
|
||||
new NestItem { Drawing = directDrawing, Quantity = 4 },
|
||||
directPlate.WorkArea(),
|
||||
null,
|
||||
CancellationToken.None
|
||||
);
|
||||
var facadeParts = facade.Fill(
|
||||
new NestItem { Drawing = facadeDrawing, Quantity = 4 },
|
||||
facadePlate.WorkArea(),
|
||||
var parts = filler.Fill(
|
||||
new NestItem { Drawing = drawing, Quantity = 4 },
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Equal(facade.WinnerPhase, directFiller.WinnerPhase);
|
||||
Assert.Equal(
|
||||
facade.PhaseResults.Select(result => (result.Phase, result.PartCount)),
|
||||
directFiller.PhaseResults.Select(result => (result.Phase, result.PartCount))
|
||||
);
|
||||
Assert.Equal(
|
||||
facade.AngleResults.Select(result => (result.AngleDeg, result.Direction, result.PartCount)),
|
||||
directFiller.AngleResults.Select(result => (result.AngleDeg, result.Direction, result.PartCount))
|
||||
);
|
||||
Assert.Equal(facadeParts.Count, directParts.Count);
|
||||
for (var i = 0; i < directParts.Count; i++)
|
||||
{
|
||||
Assert.Same(directDrawing, directParts[i].BaseDrawing);
|
||||
Assert.Same(facadeDrawing, facadeParts[i].BaseDrawing);
|
||||
Assert.Equal(facadeParts[i].Location.X, directParts[i].Location.X, 9);
|
||||
Assert.Equal(facadeParts[i].Location.Y, directParts[i].Location.Y, 9);
|
||||
Assert.Equal(facadeParts[i].Rotation, directParts[i].Rotation, 9);
|
||||
}
|
||||
Assert.NotEmpty(parts);
|
||||
Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
|
||||
Assert.NotEmpty(filler.PhaseResults);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityDefaultFacade_UsesOverriddenAngleSelection()
|
||||
public void DefaultPlateFiller_UsesOverriddenAngleSelection()
|
||||
{
|
||||
var plate = new Plate(new Size(30, 50));
|
||||
var engine = new AngleProbeDefaultNestEngine(plate);
|
||||
var filler = new AngleProbeDefaultPlateFiller(plate);
|
||||
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
new NestItem
|
||||
{
|
||||
Drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4)),
|
||||
@@ -71,18 +47,18 @@ public class PlateFillerContractTests
|
||||
);
|
||||
|
||||
Assert.NotEmpty(parts);
|
||||
Assert.True(engine.BuildAnglesCalled);
|
||||
Assert.True(filler.BuildAnglesCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityDefaultFacade_Nest_UsesOverriddenFillAndPackArea()
|
||||
public void DefaultPlateFiller_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 engine = new FillAndPackProbeDefaultNestEngine(plate, fillDrawing, packDrawing);
|
||||
var filler = new FillAndPackProbeDefaultPlateFiller(plate, fillDrawing, packDrawing);
|
||||
|
||||
var parts = engine.Nest(
|
||||
var parts = filler.Nest(
|
||||
new List<NestItem>
|
||||
{
|
||||
new() { Drawing = fillDrawing, Quantity = 10 },
|
||||
@@ -92,17 +68,16 @@ public class PlateFillerContractTests
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Equal(1, engine.FillCalls);
|
||||
Assert.Equal(1, engine.PackAreaCalls);
|
||||
Assert.Equal(1, filler.FillCalls);
|
||||
Assert.Equal(1, filler.PackAreaCalls);
|
||||
Assert.Equal(11, parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripPlateFiller_Nest_MatchesCompatibilityFacade()
|
||||
public void StripPlateFiller_Nest_ReturnsPlacedPartsAndDeductsInput()
|
||||
{
|
||||
var directPlate = new Plate(new Size(30, 50));
|
||||
var facadePlate = new Plate(new Size(30, 50));
|
||||
var directItems = new List<NestItem>
|
||||
var plate = new Plate(new Size(30, 50));
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
@@ -120,48 +95,29 @@ public class PlateFillerContractTests
|
||||
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 filler = new StripPlateFiller(plate) { PlateNumber = 7 };
|
||||
|
||||
var directParts = directFiller.Nest(directItems, null, CancellationToken.None);
|
||||
var facadeParts = facade.Nest(facadeItems, null, CancellationToken.None);
|
||||
var parts = filler.Nest(items, null, CancellationToken.None);
|
||||
|
||||
AssertEquivalentLayouts(facadeParts, directParts);
|
||||
Assert.Equal(facadeItems.Select(item => item.Quantity), directItems.Select(item => item.Quantity));
|
||||
Assert.NotEmpty(parts);
|
||||
Assert.All(parts, part => Assert.Contains(items, item => ReferenceEquals(item.Drawing, part.BaseDrawing)));
|
||||
Assert.All(items, item => Assert.InRange(item.Quantity, 0, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityStripFacade_Nest_UsesOverriddenPackArea()
|
||||
public void StripPlateFiller_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 filler = new PackProbeStripPlateFiller(plate, drawing);
|
||||
|
||||
var parts = engine.Nest(
|
||||
var parts = filler.Nest(
|
||||
new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } },
|
||||
null,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Equal(1, engine.PackAreaCalls);
|
||||
Assert.Equal(1, filler.PackAreaCalls);
|
||||
Assert.Single(parts);
|
||||
}
|
||||
|
||||
@@ -304,12 +260,12 @@ public class PlateFillerContractTests
|
||||
Assert.Equal(10, item.Quantity);
|
||||
}
|
||||
|
||||
private sealed class FillAndPackProbeDefaultNestEngine : DefaultNestEngine
|
||||
private sealed class FillAndPackProbeDefaultPlateFiller : DefaultPlateFiller
|
||||
{
|
||||
private readonly Drawing fillDrawing;
|
||||
private readonly Drawing packDrawing;
|
||||
|
||||
internal FillAndPackProbeDefaultNestEngine(
|
||||
internal FillAndPackProbeDefaultPlateFiller(
|
||||
Plate plate,
|
||||
Drawing fillDrawing,
|
||||
Drawing packDrawing
|
||||
@@ -352,9 +308,9 @@ public class PlateFillerContractTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AngleProbeDefaultNestEngine : DefaultNestEngine
|
||||
private sealed class AngleProbeDefaultPlateFiller : DefaultPlateFiller
|
||||
{
|
||||
internal AngleProbeDefaultNestEngine(Plate plate)
|
||||
internal AngleProbeDefaultPlateFiller(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
internal bool BuildAnglesCalled { get; private set; }
|
||||
@@ -370,11 +326,11 @@ public class PlateFillerContractTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PackProbeStripNestEngine : StripNestEngine
|
||||
private sealed class PackProbeStripPlateFiller : StripPlateFiller
|
||||
{
|
||||
private readonly Drawing drawing;
|
||||
|
||||
internal PackProbeStripNestEngine(Plate plate, Drawing drawing)
|
||||
internal PackProbeStripPlateFiller(Plate plate, Drawing drawing)
|
||||
: base(plate)
|
||||
{
|
||||
this.drawing = drawing;
|
||||
@@ -395,31 +351,6 @@ public class PlateFillerContractTests
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
"Default" => new DefaultPlateFiller(plate),
|
||||
@@ -428,14 +359,6 @@ public class PlateFillerContractTests
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(strategy)),
|
||||
};
|
||||
|
||||
private static NestEngineBase CreateFacade(string strategy, Plate plate) => strategy switch
|
||||
{
|
||||
"Default" => new DefaultNestEngine(plate),
|
||||
"Vertical Remnant" => new VerticalRemnantEngine(plate),
|
||||
"Horizontal Remnant" => new HorizontalRemnantEngine(plate),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(strategy)),
|
||||
};
|
||||
|
||||
private sealed class CapturingProgress : IProgress<NestProgress>
|
||||
{
|
||||
public List<NestProgress> Reports { get; } = new();
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public class DefaultNestEngine : NestEngineBase
|
||||
{
|
||||
private DefaultPlateFiller filler;
|
||||
private bool forceFullAngleSweep;
|
||||
|
||||
public DefaultNestEngine(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
public override string Name => "Default";
|
||||
|
||||
public override string Description =>
|
||||
"Multi-phase nesting (Linear, Pairs, RectBestFit, Extents)";
|
||||
|
||||
public override NestPhase WinnerPhase
|
||||
{
|
||||
get => Filler.WinnerPhase;
|
||||
protected set => Filler.SetWinnerPhase(value);
|
||||
}
|
||||
|
||||
public override List<PhaseResult> PhaseResults => Filler.PhaseResults;
|
||||
|
||||
public override List<AngleResult> AngleResults => Filler.AngleResults;
|
||||
|
||||
public bool ForceFullAngleSweep
|
||||
{
|
||||
get => forceFullAngleSweep;
|
||||
set
|
||||
{
|
||||
forceFullAngleSweep = value;
|
||||
if (filler != null)
|
||||
filler.ForceFullAngleSweep = value;
|
||||
}
|
||||
}
|
||||
|
||||
private DefaultPlateFiller Filler
|
||||
{
|
||||
get
|
||||
{
|
||||
if (filler == null || !ReferenceEquals(filler.Plate, Plate))
|
||||
{
|
||||
filler = CreateFiller(Plate);
|
||||
filler.ForceFullAngleSweep = forceFullAngleSweep;
|
||||
}
|
||||
return filler;
|
||||
}
|
||||
}
|
||||
|
||||
private DefaultPlateFiller PrepareFiller()
|
||||
{
|
||||
var current = Filler;
|
||||
current.PlateNumber = PlateNumber;
|
||||
current.NestDirection = NestDirection;
|
||||
return current;
|
||||
}
|
||||
|
||||
internal virtual DefaultPlateFiller CreateFiller(Plate plate) =>
|
||||
new LegacyDefaultPlateFiller(this, plate);
|
||||
|
||||
internal DefaultPlateFiller CreateRemnantFiller(Plate plate, RemnantFillPolicy policy) =>
|
||||
new LegacyRemnantPlateFiller(this, plate, policy);
|
||||
|
||||
protected override IFillComparer CreateComparer() => Filler.CreateComparerCore();
|
||||
|
||||
public override NestDirection? PreferredDirection => null;
|
||||
|
||||
public override ShrinkAxis TrimAxis => ShrinkAxis.Width;
|
||||
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
) => Filler.BuildAnglesCore(item, classification, workArea);
|
||||
|
||||
protected override void RecordProductiveAngles(List<AngleResult> angleResults) =>
|
||||
Filler.RecordProductiveAnglesCore(angleResults);
|
||||
|
||||
protected virtual void RunPipeline(FillContext context) => Filler.RunPipelineCore(context);
|
||||
|
||||
public override List<Part> Fill(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var current = PrepareFiller();
|
||||
return current.Fill(item, workArea, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> Fill(
|
||||
List<Part> groupParts,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var current = PrepareFiller();
|
||||
return current.Fill(groupParts, workArea, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> PackArea(
|
||||
Box box,
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var current = PrepareFiller();
|
||||
return current.PackArea(box, items, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> Nest(
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return base.Nest(items, progress, token);
|
||||
}
|
||||
|
||||
private sealed class LegacyDefaultPlateFiller : DefaultPlateFiller
|
||||
{
|
||||
private readonly DefaultNestEngine engine;
|
||||
|
||||
internal LegacyDefaultPlateFiller(DefaultNestEngine engine, Plate plate)
|
||||
: base(plate)
|
||||
{
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
protected override IFillComparer CreateComparer() => engine.CreateComparer();
|
||||
|
||||
public override NestDirection? PreferredDirection => engine.PreferredDirection;
|
||||
|
||||
public override ShrinkAxis TrimAxis => engine.TrimAxis;
|
||||
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
) => engine.BuildAngles(item, classification, workArea);
|
||||
|
||||
protected override void RecordProductiveAngles(List<AngleResult> angleResults) =>
|
||||
engine.RecordProductiveAngles(angleResults);
|
||||
|
||||
protected override void RunPipeline(FillContext context) => engine.RunPipeline(context);
|
||||
}
|
||||
|
||||
private sealed class LegacyRemnantPlateFiller : RemnantPlateFiller
|
||||
{
|
||||
private readonly DefaultNestEngine engine;
|
||||
|
||||
internal LegacyRemnantPlateFiller(
|
||||
DefaultNestEngine engine,
|
||||
Plate plate,
|
||||
RemnantFillPolicy policy
|
||||
)
|
||||
: base(plate, policy)
|
||||
{
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
protected override IFillComparer CreateComparer() => engine.CreateComparer();
|
||||
|
||||
public override NestDirection? PreferredDirection => engine.PreferredDirection;
|
||||
|
||||
public override ShrinkAxis TrimAxis => engine.TrimAxis;
|
||||
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
) => engine.BuildAngles(item, classification, workArea);
|
||||
|
||||
protected override void RecordProductiveAngles(List<AngleResult> angleResults) =>
|
||||
engine.RecordProductiveAngles(angleResults);
|
||||
|
||||
protected override void RunPipeline(FillContext context) => engine.RunPipeline(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Ranks fill results by count first, then density.
|
||||
/// This is the original scoring logic used by DefaultNestEngine.
|
||||
/// This is the original scoring logic used by the Default plate filler.
|
||||
/// </summary>
|
||||
public class DefaultFillComparer : IFillComparer
|
||||
{
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
/// <summary>
|
||||
/// Optimizes for the largest top-side horizontal drop.
|
||||
/// </summary>
|
||||
public class HorizontalRemnantEngine : DefaultNestEngine
|
||||
{
|
||||
public HorizontalRemnantEngine(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
public override string Name => "Horizontal Remnant";
|
||||
|
||||
public override string Description => "Optimizes for largest top-side horizontal drop";
|
||||
|
||||
protected override IFillComparer CreateComparer() =>
|
||||
RemnantFillPolicy.Horizontal.CreateComparer();
|
||||
|
||||
public override NestDirection? PreferredDirection =>
|
||||
RemnantFillPolicy.Horizontal.PreferredDirection;
|
||||
|
||||
public override ShrinkAxis TrimAxis => RemnantFillPolicy.Horizontal.TrimAxis;
|
||||
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
) => RemnantFillPolicy.Horizontal.BuildAngles(item, classification);
|
||||
|
||||
internal override DefaultPlateFiller CreateFiller(Plate plate) =>
|
||||
CreateRemnantFiller(plate, RemnantFillPolicy.Horizontal);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,10 @@ using System.Reflection;
|
||||
namespace OpenNest.Engine.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of whole-job INestingEngine implementations, parallel to NestEngineRegistry (which is for
|
||||
/// the legacy single-plate NestEngineBase). The four production strategies are exposed here through
|
||||
/// FixedStrategyNestingEngine so they compete on equal footing with model-submitted engines. Unlike
|
||||
/// NestEngineRegistry, this has no ActiveEngineName/global-selection concept — callers choose an engine
|
||||
/// explicitly from AvailableEngines.
|
||||
/// Registry of whole-job <see cref="INestingEngine"/> implementations. The four production
|
||||
/// strategies are exposed through <see cref="FixedStrategyNestingEngine"/> so they compete on
|
||||
/// equal footing with model-submitted engines. Callers choose an engine explicitly from
|
||||
/// <see cref="AvailableEngines"/>; there is no process-global active selection.
|
||||
/// </summary>
|
||||
public static class NestingEngineRegistry
|
||||
{
|
||||
@@ -55,8 +54,7 @@ public static class NestingEngineRegistry
|
||||
|
||||
/// <summary>
|
||||
/// Creates the engine registered under <paramref name="name"/> (case-insensitive). The caller's
|
||||
/// explicit choice is the whole selection mechanism: unlike the legacy registry there is no
|
||||
/// process-global active name to consult or mutate. Unknown names throw.
|
||||
/// explicit choice is the whole selection mechanism; unknown names throw.
|
||||
/// </summary>
|
||||
public static INestingEngine Create(string name)
|
||||
{
|
||||
@@ -81,9 +79,8 @@ public static class NestingEngineRegistry
|
||||
}
|
||||
|
||||
/// <summary>Scans *.dll in directory for non-abstract INestingEngine types with a public
|
||||
/// parameterless constructor, registering each under its CLR type name. Mirrors
|
||||
/// NestEngineRegistry.LoadPlugins's per-assembly/per-type isolation: one bad plugin never
|
||||
/// prevents the rest from loading.</summary>
|
||||
/// parameterless constructor, registering each under its CLR type name. Per-assembly and per-type
|
||||
/// isolation ensures one bad plugin never prevents the rest from loading.</summary>
|
||||
public static void LoadPlugins(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
|
||||
@@ -20,9 +20,7 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
internal DefaultPlateFiller(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
protected override IFillComparer CreateComparer() => CreateComparerCore();
|
||||
|
||||
internal IFillComparer CreateComparerCore() => new DefaultFillComparer();
|
||||
protected override IFillComparer CreateComparer() => new DefaultFillComparer();
|
||||
|
||||
internal bool ForceFullAngleSweep
|
||||
{
|
||||
@@ -34,18 +32,9 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
) => BuildAnglesCore(item, classification, workArea);
|
||||
|
||||
internal List<double> BuildAnglesCore(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
) => angleBuilder.Build(item, classification, workArea);
|
||||
|
||||
protected override void RecordProductiveAngles(List<AngleResult> angleResults) =>
|
||||
RecordProductiveAnglesCore(angleResults);
|
||||
|
||||
internal void RecordProductiveAnglesCore(List<AngleResult> angleResults)
|
||||
protected override void RecordProductiveAngles(List<AngleResult> angleResults)
|
||||
{
|
||||
angleBuilder.RecordProductive(angleResults);
|
||||
}
|
||||
@@ -419,9 +408,7 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
return BinConverter.ToParts(bin, items);
|
||||
}
|
||||
|
||||
protected virtual void RunPipeline(FillContext context) => RunPipelineCore(context);
|
||||
|
||||
internal void RunPipelineCore(FillContext context)
|
||||
protected virtual void RunPipeline(FillContext context)
|
||||
{
|
||||
var classification = PartClassifier.Classify(context.Item.Drawing);
|
||||
context.PartType = classification.Type;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
|
||||
@@ -27,11 +25,6 @@ internal abstract class PlateFillerBase
|
||||
|
||||
public NestPhase WinnerPhase { get; protected set; }
|
||||
|
||||
internal void SetWinnerPhase(NestPhase winnerPhase)
|
||||
{
|
||||
WinnerPhase = winnerPhase;
|
||||
}
|
||||
|
||||
public List<PhaseResult> PhaseResults { get; } = new();
|
||||
|
||||
public List<AngleResult> AngleResults { get; } = new();
|
||||
@@ -61,11 +54,9 @@ internal abstract class PlateFillerBase
|
||||
|
||||
protected FillPolicy BuildPolicy() => new(Comparer, PreferredDirection);
|
||||
|
||||
internal IFillComparer FillComparer => Comparer;
|
||||
|
||||
protected string BuildProgressSummary() => BuildProgressSummary(PhaseResults);
|
||||
|
||||
internal static string BuildProgressSummary(IReadOnlyList<PhaseResult> phaseResults)
|
||||
private static string BuildProgressSummary(IReadOnlyList<PhaseResult> phaseResults)
|
||||
{
|
||||
if (phaseResults.Count == 0)
|
||||
return null;
|
||||
@@ -78,70 +69,7 @@ internal abstract class PlateFillerBase
|
||||
}
|
||||
|
||||
protected bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea) =>
|
||||
IsBetterFill(Comparer, candidate, current, workArea);
|
||||
|
||||
internal static bool IsBetterFill(
|
||||
IFillComparer comparer,
|
||||
List<Part> candidate,
|
||||
List<Part> current,
|
||||
Box workArea
|
||||
) => comparer.IsBetter(candidate, current, workArea);
|
||||
|
||||
protected bool IsBetterValidFill(List<Part> candidate, List<Part> current, Box workArea)
|
||||
{
|
||||
if (
|
||||
candidate != null
|
||||
&& candidate.Count > 0
|
||||
&& HasOverlaps(candidate, Plate.PartSpacing)
|
||||
)
|
||||
{
|
||||
Debug.WriteLine(
|
||||
$"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsBetterFill(candidate, current, workArea);
|
||||
}
|
||||
|
||||
internal static bool HasOverlaps(List<Part> parts, double spacing)
|
||||
{
|
||||
if (parts == null || parts.Count <= 1)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var box1 = parts[i].BoundingBox;
|
||||
|
||||
for (var j = i + 1; j < parts.Count; j++)
|
||||
{
|
||||
var box2 = parts[j].BoundingBox;
|
||||
|
||||
var overlapX = System.Math.Min(box1.Right, box2.Right)
|
||||
- System.Math.Max(box1.Left, box2.Left);
|
||||
var overlapY = System.Math.Min(box1.Top, box2.Top)
|
||||
- System.Math.Max(box1.Bottom, box2.Bottom);
|
||||
|
||||
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
|
||||
continue;
|
||||
|
||||
List<Vector> points;
|
||||
if (parts[i].Intersects(parts[j], out points))
|
||||
{
|
||||
var first = parts[i].BoundingBox;
|
||||
var second = parts[j].BoundingBox;
|
||||
Debug.WriteLine(
|
||||
$"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({first.Left:F2},{first.Bottom:F2})-({first.Right:F2},{first.Top:F2}) rot={parts[i].Rotation:F2}"
|
||||
+ $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({second.Left:F2},{second.Bottom:F2})-({second.Right:F2},{second.Top:F2}) rot={parts[j].Rotation:F2}"
|
||||
+ $" intersections={points?.Count ?? 0}"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Comparer.IsBetter(candidate, current, workArea);
|
||||
|
||||
public virtual List<Part> Fill(
|
||||
NestItem item,
|
||||
|
||||
@@ -40,13 +40,6 @@ internal class StripPlateFiller : PlateFillerBase
|
||||
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);
|
||||
|
||||
@@ -5,9 +5,8 @@ namespace OpenNest.Engine.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Instance-scoped strategy resolution for the whole-job runner. All four built-in strategies
|
||||
/// resolve directly to filler-backed plate nesters. The process-global NestEngineRegistry
|
||||
/// (including plugin registrations and ActiveEngineName) is neither read nor modified.
|
||||
/// Unknown keys reject.
|
||||
/// resolve directly to filler-backed plate nesters. Selection is explicit per job; no shared
|
||||
/// mutable strategy setting is read or modified. Unknown keys reject.
|
||||
/// </summary>
|
||||
public static class PlateNesterFactory
|
||||
{
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public abstract class NestEngineBase
|
||||
{
|
||||
protected NestEngineBase(Plate plate)
|
||||
{
|
||||
Plate = plate;
|
||||
}
|
||||
|
||||
public Plate Plate { get; set; }
|
||||
|
||||
public int PlateNumber { get; set; }
|
||||
|
||||
public NestDirection NestDirection { get; set; }
|
||||
|
||||
private readonly List<PhaseResult> phaseResults = new();
|
||||
private readonly List<AngleResult> angleResults = new();
|
||||
|
||||
public virtual NestPhase WinnerPhase { get; protected set; }
|
||||
|
||||
public virtual List<PhaseResult> PhaseResults => phaseResults;
|
||||
|
||||
public virtual List<AngleResult> AngleResults => angleResults;
|
||||
|
||||
public abstract string Name { get; }
|
||||
|
||||
public abstract string Description { get; }
|
||||
|
||||
// --- Engine policy ---
|
||||
|
||||
private IFillComparer _comparer;
|
||||
|
||||
protected IFillComparer Comparer => _comparer ??= CreateComparer();
|
||||
|
||||
protected virtual IFillComparer CreateComparer() => new DefaultFillComparer();
|
||||
|
||||
public virtual NestDirection? PreferredDirection => null;
|
||||
|
||||
public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width;
|
||||
|
||||
public virtual List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
)
|
||||
{
|
||||
return new List<double>
|
||||
{
|
||||
classification.PrimaryAngle,
|
||||
classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI,
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual void RecordProductiveAngles(List<AngleResult> angleResults) { }
|
||||
|
||||
protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection);
|
||||
|
||||
// --- Virtual methods (side-effect-free, return parts) ---
|
||||
|
||||
public virtual List<Part> Fill(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return new List<Part>();
|
||||
}
|
||||
|
||||
public virtual List<Part> Fill(
|
||||
List<Part> groupParts,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return new List<Part>();
|
||||
}
|
||||
|
||||
public virtual List<Part> PackArea(
|
||||
Box box,
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return new List<Part>();
|
||||
}
|
||||
|
||||
// --- Nest: compatibility façade over shared single-plate orchestration ---
|
||||
|
||||
public virtual List<Part> Nest(
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return PlateFillOrchestrator.Nest(
|
||||
Plate,
|
||||
items,
|
||||
Comparer,
|
||||
(item, workArea, sink, cancellation) =>
|
||||
FillExact(item, workArea, sink, cancellation),
|
||||
(workArea, packItems, sink, cancellation) =>
|
||||
PackArea(workArea, packItems, sink, cancellation),
|
||||
progress,
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
// --- FillExact (non-virtual, delegates to virtual Fill) ---
|
||||
|
||||
public List<Part> FillExact(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
return Fill(item, workArea, progress, token);
|
||||
}
|
||||
|
||||
// --- Convenience overloads (mutate plate, return bool) ---
|
||||
|
||||
public bool Fill(NestItem item)
|
||||
{
|
||||
return Fill(item, Plate.WorkArea());
|
||||
}
|
||||
|
||||
public bool Fill(NestItem item, Box workArea)
|
||||
{
|
||||
var parts = Fill(item, workArea, null, CancellationToken.None);
|
||||
|
||||
if (parts == null || parts.Count == 0)
|
||||
return false;
|
||||
|
||||
Plate.Parts.AddRange(parts);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Fill(List<Part> groupParts)
|
||||
{
|
||||
return Fill(groupParts, Plate.WorkArea());
|
||||
}
|
||||
|
||||
public bool Fill(List<Part> groupParts, Box workArea)
|
||||
{
|
||||
var parts = Fill(groupParts, workArea, null, CancellationToken.None);
|
||||
|
||||
if (parts == null || parts.Count == 0)
|
||||
return false;
|
||||
|
||||
Plate.Parts.AddRange(parts);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Pack(List<NestItem> items)
|
||||
{
|
||||
var workArea = Plate.WorkArea();
|
||||
var parts = PackArea(workArea, items, null, CancellationToken.None);
|
||||
|
||||
if (parts == null || parts.Count == 0)
|
||||
return false;
|
||||
|
||||
Plate.Parts.AddRange(parts);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Protected utilities ---
|
||||
|
||||
internal static void ReportProgress(IProgress<NestProgress> progress, ProgressReport report)
|
||||
{
|
||||
NestProgressReporter.Report(progress, report);
|
||||
}
|
||||
|
||||
protected string BuildProgressSummary() =>
|
||||
PlateFillerBase.BuildProgressSummary(PhaseResults);
|
||||
|
||||
protected bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea) =>
|
||||
PlateFillerBase.IsBetterFill(Comparer, candidate, current, workArea);
|
||||
|
||||
protected bool IsBetterValidFill(List<Part> candidate, List<Part> current, Box workArea)
|
||||
{
|
||||
if (
|
||||
candidate != null
|
||||
&& candidate.Count > 0
|
||||
&& HasOverlaps(candidate, Plate.PartSpacing)
|
||||
)
|
||||
{
|
||||
Debug.WriteLine(
|
||||
$"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsBetterFill(candidate, current, workArea);
|
||||
}
|
||||
|
||||
protected static bool HasOverlaps(List<Part> parts, double spacing) =>
|
||||
PlateFillerBase.HasOverlaps(parts, spacing);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public class NestEngineInfo
|
||||
{
|
||||
public NestEngineInfo(string name, string description, Func<Plate, NestEngineBase> factory)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
Factory = factory;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public Func<Plate, NestEngineBase> Factory { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public static class NestEngineRegistry
|
||||
{
|
||||
private static readonly List<NestEngineInfo> engines = new();
|
||||
|
||||
static NestEngineRegistry()
|
||||
{
|
||||
Register(
|
||||
"Default",
|
||||
"Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
|
||||
plate => new DefaultNestEngine(plate)
|
||||
);
|
||||
|
||||
Register(
|
||||
"Strip",
|
||||
"Strip-based nesting for mixed-drawing layouts",
|
||||
plate => new StripNestEngine(plate)
|
||||
);
|
||||
|
||||
Register(
|
||||
"Vertical Remnant",
|
||||
"Optimizes for largest right-side vertical drop",
|
||||
plate => new VerticalRemnantEngine(plate)
|
||||
);
|
||||
|
||||
Register(
|
||||
"Horizontal Remnant",
|
||||
"Optimizes for largest top-side horizontal drop",
|
||||
plate => new HorizontalRemnantEngine(plate)
|
||||
);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<NestEngineInfo> AvailableEngines => engines;
|
||||
|
||||
public static string ActiveEngineName { get; set; } = "Default";
|
||||
|
||||
public static NestEngineBase Create(Plate plate)
|
||||
{
|
||||
var info = engines.FirstOrDefault(e =>
|
||||
e.Name.Equals(ActiveEngineName, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Engine '{ActiveEngineName}' not found, falling back to Default"
|
||||
);
|
||||
info = engines[0];
|
||||
}
|
||||
|
||||
return info.Factory(plate);
|
||||
}
|
||||
|
||||
public static void Register(
|
||||
string name,
|
||||
string description,
|
||||
Func<Plate, NestEngineBase> factory
|
||||
)
|
||||
{
|
||||
if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Debug.WriteLine($"[NestEngineRegistry] Duplicate engine '{name}' skipped");
|
||||
return;
|
||||
}
|
||||
|
||||
engines.Add(new NestEngineInfo(name, description, factory));
|
||||
}
|
||||
|
||||
public static void LoadPlugins(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return;
|
||||
|
||||
foreach (var dll in Directory.GetFiles(directory, "*.dll"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(dll);
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (type.IsAbstract || !typeof(NestEngineBase).IsAssignableFrom(type))
|
||||
continue;
|
||||
|
||||
var ctor = type.GetConstructor(new[] { typeof(Plate) });
|
||||
|
||||
if (ctor == null)
|
||||
{
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Skipping {type.Name}: no Plate constructor"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary instance to read Name and Description.
|
||||
try
|
||||
{
|
||||
var tempPlate = new Plate();
|
||||
var instance = (NestEngineBase)ctor.Invoke(new object[] { tempPlate });
|
||||
Register(
|
||||
instance.Name,
|
||||
instance.Description,
|
||||
plate => (NestEngineBase)ctor.Invoke(new object[] { plate })
|
||||
);
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Loaded plugin engine: {instance.Name}"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Failed to instantiate {type.Name}: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(
|
||||
$"[NestEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ namespace OpenNest.Engine.Strategies
|
||||
/// the canonical drawing's baked source angle whenever it's non-zero. No-op when
|
||||
/// <see cref="OriginalDrawing"/> isn't set. Internal so callers that build their own
|
||||
/// <see cref="ProgressReport"/> outside <see cref="ReportProgress"/> (e.g. the fallback
|
||||
/// report in <c>DefaultPlateFiller.RunPipelineCore</c> for strategies that don't self-report)
|
||||
/// report in <c>DefaultPlateFiller.RunPipeline</c> for strategies that don't self-report)
|
||||
/// can apply the same rebind before reaching the UI.
|
||||
/// </summary>
|
||||
internal List<Part> ToOriginalFrame(List<Part> parts)
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public class StripNestEngine : NestEngineBase
|
||||
{
|
||||
private StripPlateFiller filler;
|
||||
|
||||
public StripNestEngine(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
public override string Name => "Strip";
|
||||
|
||||
public override string Description =>
|
||||
"Iterative shrink-fill nesting for mixed-drawing layouts";
|
||||
|
||||
private StripPlateFiller Filler
|
||||
{
|
||||
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(
|
||||
NestItem item,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var current = PrepareFiller();
|
||||
return current.Fill(item, workArea, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> Fill(
|
||||
List<Part> groupParts,
|
||||
Box workArea,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var current = PrepareFiller();
|
||||
return current.Fill(groupParts, workArea, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> PackArea(
|
||||
Box box,
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var current = PrepareFiller();
|
||||
return current.PackAreaCore(box, items, progress, token);
|
||||
}
|
||||
|
||||
public override List<Part> Nest(
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var current = PrepareFiller();
|
||||
return current.Nest(items, progress, token);
|
||||
}
|
||||
|
||||
private sealed class LegacyStripPlateFiller : StripPlateFiller
|
||||
{
|
||||
private readonly StripNestEngine engine;
|
||||
|
||||
internal LegacyStripPlateFiller(StripNestEngine engine, Plate plate)
|
||||
: base(plate)
|
||||
{
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
public override List<Part> PackArea(
|
||||
Box box,
|
||||
List<NestItem> items,
|
||||
IProgress<NestProgress> progress,
|
||||
CancellationToken token
|
||||
) => engine.PackArea(box, items, progress, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
/// <summary>
|
||||
/// Optimizes for the largest right-side vertical drop.
|
||||
/// </summary>
|
||||
public class VerticalRemnantEngine : DefaultNestEngine
|
||||
{
|
||||
public VerticalRemnantEngine(Plate plate)
|
||||
: base(plate) { }
|
||||
|
||||
public override string Name => "Vertical Remnant";
|
||||
|
||||
public override string Description => "Optimizes for largest right-side vertical drop";
|
||||
|
||||
protected override IFillComparer CreateComparer() => RemnantFillPolicy.Vertical.CreateComparer();
|
||||
|
||||
public override NestDirection? PreferredDirection => RemnantFillPolicy.Vertical.PreferredDirection;
|
||||
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
Box workArea
|
||||
) => RemnantFillPolicy.Vertical.BuildAngles(item, classification);
|
||||
|
||||
internal override DefaultPlateFiller CreateFiller(Plate plate) =>
|
||||
CreateRemnantFiller(plate, RemnantFillPolicy.Vertical);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using Xunit.Abstractions;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Jobs.Placement;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
@@ -31,7 +32,7 @@ public class EngineOverlapTests
|
||||
[InlineData("Strip")]
|
||||
[InlineData("Vertical Remnant")]
|
||||
[InlineData("Horizontal Remnant")]
|
||||
public void FillPlate_NoOverlaps(string engineName)
|
||||
public void FillPlate_NoOverlaps(string strategy)
|
||||
{
|
||||
var drawing = ImportDxf();
|
||||
if (drawing is null)
|
||||
@@ -39,23 +40,21 @@ public class EngineOverlapTests
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
|
||||
NestEngineRegistry.ActiveEngineName = engineName;
|
||||
var engine = NestEngineRegistry.Create(plate);
|
||||
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var success = engine.Fill(item);
|
||||
var parts = PlateFillService.FillItem(
|
||||
strategy,
|
||||
plate,
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
progress: null,
|
||||
token: CancellationToken.None
|
||||
);
|
||||
plate.Parts.AddRange(parts);
|
||||
|
||||
_output.WriteLine(
|
||||
$"Engine: {engine.Name}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}"
|
||||
$"Strategy: {strategy}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}"
|
||||
);
|
||||
|
||||
if (engine is DefaultNestEngine defaultEngine)
|
||||
{
|
||||
_output.WriteLine($"Winner phase: {defaultEngine.WinnerPhase}");
|
||||
foreach (var pr in defaultEngine.PhaseResults)
|
||||
_output.WriteLine($" Phase {pr.Phase}: {pr.PartCount} parts in {pr.TimeMs}ms");
|
||||
}
|
||||
|
||||
// Show rotation distribution
|
||||
var rotGroups = plate
|
||||
.Parts.GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1))
|
||||
@@ -74,7 +73,7 @@ public class EngineOverlapTests
|
||||
|
||||
Assert.False(
|
||||
hasOverlaps,
|
||||
$"Engine '{engineName}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts"
|
||||
$"Strategy '{strategy}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
@@ -17,49 +18,49 @@ public class EngineRefactorSmokeTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEngine_FillNestItem_ProducesResults()
|
||||
public void DefaultPlateFiller_FillNestItem_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
System.Threading.CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts");
|
||||
Assert.True(parts.Count > 0, "DefaultPlateFiller should fill parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEngine_FillGroupParts_ProducesResults()
|
||||
public void DefaultPlateFiller_FillGroupParts_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
var groupParts = new List<Part> { new Part(drawing) };
|
||||
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
groupParts,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
System.Threading.CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(parts.Count > 0, "DefaultNestEngine group fill should produce parts");
|
||||
Assert.True(parts.Count > 0, "DefaultPlateFiller group fill should produce parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEngine_ForceFullAngleSweep_StillWorks()
|
||||
public void DefaultPlateFiller_ForceFullAngleSweep_StillWorks()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
engine.ForceFullAngleSweep = true;
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
filler.ForceFullAngleSweep = true;
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
@@ -70,33 +71,33 @@ public class EngineRefactorSmokeTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripEngine_Nest_ProducesResults()
|
||||
public void StripPlateFiller_Nest_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new StripNestEngine(plate);
|
||||
var filler = new StripPlateFiller(plate);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
|
||||
new NestItem { Drawing = MakeRectDrawing(8, 5, "small"), Quantity = 5 },
|
||||
};
|
||||
|
||||
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
|
||||
var parts = filler.Nest(items, null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "StripNestEngine should nest parts");
|
||||
Assert.True(parts.Count > 0, "StripPlateFiller should nest parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEngine_Nest_ProducesResults()
|
||||
public void DefaultPlateFiller_Nest_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
|
||||
new NestItem { Drawing = MakeRectDrawing(15, 8, "b"), Quantity = 3 },
|
||||
};
|
||||
|
||||
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
|
||||
var parts = filler.Nest(items, null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "Base Nest method should place parts");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace OpenNest.Tests.Engine;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 3.2: MultiPlateNester and PlateOptimizer take an explicit placement strategy at their
|
||||
/// top-level call boundary instead of consulting the process-global NestEngineRegistry.
|
||||
/// top-level call boundary instead of consulting process-global selection state.
|
||||
/// </summary>
|
||||
public class ExplicitPlacementStrategyTests
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Threading;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
@@ -37,9 +38,9 @@ public class NestInvarianceTests
|
||||
private static int RunFillCount(Drawing drawing, Plate plate)
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
progress: null,
|
||||
@@ -71,9 +72,9 @@ public class NestInvarianceTests
|
||||
foreach (var theta in new[] { 0.0, 0.3, 0.8, 1.2 })
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var item = new NestItem { Drawing = MakeImportedAt(theta) };
|
||||
var parts = engine.Fill(item, workArea, progress: null, token: CancellationToken.None);
|
||||
var parts = filler.Fill(item, workArea, progress: null, token: CancellationToken.None);
|
||||
|
||||
Assert.NotNull(parts);
|
||||
foreach (var p in parts)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
@@ -18,82 +20,79 @@ public class RemnantEngineTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerticalRemnantEngine_UsesVerticalRemnantComparer()
|
||||
public void VerticalRemnantPlateFiller_UsesHorizontalPreferredDirection()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new VerticalRemnantEngine(plate);
|
||||
Assert.Equal("Vertical Remnant", engine.Name);
|
||||
Assert.Equal(NestDirection.Horizontal, engine.PreferredDirection);
|
||||
var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
|
||||
Assert.Equal(NestDirection.Horizontal, filler.PreferredDirection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalRemnantEngine_UsesHorizontalRemnantComparer()
|
||||
public void HorizontalRemnantPlateFiller_UsesVerticalPreferredDirection()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new HorizontalRemnantEngine(plate);
|
||||
Assert.Equal("Horizontal Remnant", engine.Name);
|
||||
Assert.Equal(NestDirection.Vertical, engine.PreferredDirection);
|
||||
var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal);
|
||||
Assert.Equal(NestDirection.Vertical, filler.PreferredDirection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerticalRemnantEngine_Fill_ProducesResults()
|
||||
public void VerticalRemnantPlateFiller_Fill_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new VerticalRemnantEngine(plate);
|
||||
var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
System.Threading.CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(parts.Count > 0, "VerticalRemnantEngine should fill parts");
|
||||
Assert.True(parts.Count > 0, "VerticalRemnantPlateFiller should fill parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalRemnantEngine_Fill_ProducesResults()
|
||||
public void HorizontalRemnantPlateFiller_Fill_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new HorizontalRemnantEngine(plate);
|
||||
var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
System.Threading.CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(parts.Count > 0, "HorizontalRemnantEngine should fill parts");
|
||||
Assert.True(parts.Count > 0, "HorizontalRemnantPlateFiller should fill parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_ContainsBothRemnantEngines()
|
||||
public void PlateFillService_ListsBothRemnantStrategies()
|
||||
{
|
||||
var names = NestEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
|
||||
Assert.Contains("Vertical Remnant", names);
|
||||
Assert.Contains("Horizontal Remnant", names);
|
||||
Assert.Contains("Vertical Remnant", PlateFillService.BuiltInStrategies);
|
||||
Assert.Contains("Horizontal Remnant", PlateFillService.BuiltInStrategies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerticalRemnantEngine_ProducesTighterXExtent_ThanDefault()
|
||||
public void VerticalRemnantPlateFiller_ProducesTighterXExtent_ThanDefault()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
|
||||
var defaultEngine = new DefaultNestEngine(plate);
|
||||
var remnantEngine = new VerticalRemnantEngine(plate);
|
||||
var defaultFiller = new DefaultPlateFiller(plate);
|
||||
var remnantFiller = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
|
||||
|
||||
var defaultParts = defaultEngine.Fill(
|
||||
var defaultParts = defaultFiller.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
System.Threading.CancellationToken.None
|
||||
);
|
||||
var remnantParts = remnantEngine.Fill(
|
||||
var remnantParts = remnantFiller.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
@@ -110,7 +109,7 @@ public class RemnantEngineTests
|
||||
|
||||
Assert.True(
|
||||
remnantXExtent <= defaultXExtent + 0.01,
|
||||
$"Remnant X-extent ({remnantXExtent:F1}) should be <= default ({defaultXExtent:F1})"
|
||||
$"Remnant X-extent ({remnantXExtent:F1}) should be <= default filler ({defaultXExtent:F1})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine;
|
||||
|
||||
@@ -54,8 +55,8 @@ public class IterativeShrinkFillerTests
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
||||
@@ -75,8 +76,8 @@ public class IterativeShrinkFillerTests
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
||||
@@ -100,8 +101,8 @@ public class IterativeShrinkFillerTests
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 60, 30), fillFunc, 1.0);
|
||||
@@ -122,8 +123,8 @@ public class IterativeShrinkFillerTests
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine;
|
||||
|
||||
@@ -35,8 +36,8 @@ public class RemnantFillerTests2
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var placed = filler.FillItems(items, fillFunc);
|
||||
@@ -59,8 +60,8 @@ public class RemnantFillerTests2
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
filler.FillItems(items, fillFunc);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine;
|
||||
|
||||
@@ -27,8 +28,8 @@ public class ShrinkFillerTests
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Length);
|
||||
@@ -49,8 +50,8 @@ public class ShrinkFillerTests
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Width);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine;
|
||||
|
||||
@@ -21,14 +22,14 @@ public class FillPipelineTests
|
||||
public void Pipeline_PopulatesPhaseResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
filler.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(
|
||||
engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
|
||||
$"Expected phase results from all active strategies, got {engine.PhaseResults.Count}"
|
||||
filler.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
|
||||
$"Expected phase results from all active strategies, got {filler.PhaseResults.Count}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,10 +37,10 @@ public class FillPipelineTests
|
||||
public void Pipeline_SetsWinnerPhase()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(
|
||||
var parts = filler.Fill(
|
||||
item,
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
@@ -48,11 +49,11 @@ public class FillPipelineTests
|
||||
|
||||
Assert.True(parts.Count > 0);
|
||||
Assert.True(
|
||||
engine.WinnerPhase == NestPhase.Pairs
|
||||
|| engine.WinnerPhase == NestPhase.Linear
|
||||
|| engine.WinnerPhase == NestPhase.RectBestFit
|
||||
|| engine.WinnerPhase == NestPhase.Extents
|
||||
|| engine.WinnerPhase == NestPhase.Custom
|
||||
filler.WinnerPhase == NestPhase.Pairs
|
||||
|| filler.WinnerPhase == NestPhase.Linear
|
||||
|| filler.WinnerPhase == NestPhase.RectBestFit
|
||||
|| filler.WinnerPhase == NestPhase.Extents
|
||||
|| filler.WinnerPhase == NestPhase.Custom
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,13 +61,13 @@ public class FillPipelineTests
|
||||
public void Pipeline_RespectsCancellation()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var filler = new DefaultPlateFiller(plate);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// Pre-cancelled token should return empty or partial results without throwing
|
||||
var parts = engine.Fill(item, plate.WorkArea(), null, cts.Token);
|
||||
var parts = filler.Fill(item, plate.WorkArea(), null, cts.Token);
|
||||
|
||||
// Should not throw — graceful degradation
|
||||
Assert.NotNull(parts);
|
||||
|
||||
@@ -7,8 +7,7 @@ using OpenNest.Engine.Jobs.Placement;
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// App-scoped nesting-engine selection — the desktop replacement for the process-global
|
||||
/// NestEngineRegistry.ActiveEngineName. The selected name addresses a whole-job
|
||||
/// 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).
|
||||
|
||||
@@ -72,8 +72,8 @@ namespace OpenNest.Forms
|
||||
// BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||
|
||||
// Jobs-side plug-in discovery: INestingEngine implementations are registered per
|
||||
// assembly/type with the same per-DLL isolation as before. Binary plug-ins derived
|
||||
// from the legacy NestEngineBase no longer load here after the Phase-4 removal.
|
||||
// assembly/type with per-DLL isolation. Existing plug-ins must implement the jobs
|
||||
// contract and expose a public parameterless constructor.
|
||||
var enginesDir = Path.Combine(Application.StartupPath, "Engines");
|
||||
NestingEngineRegistry.LoadPlugins(enginesDir);
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ The new whole-job contracts in `OpenNest.Engine/Jobs` (`namespace OpenNest`) use
|
||||
|
||||
`NestJobRunner.Solve` allocates a job across physical sheets from the full stock inventory: every available stock entry is trialled independently each iteration, and only the winning candidate consumes a sheet or reduces demand. Selection is a documented deterministic greedy policy — lexicographic placed-count vector by ascending part priority, then lower consumed sheet area, then smaller placement envelope, then original stock input order (see `NestJobCandidateComparer`). It is a tie policy, not a guarantee of global-minimum material or plate count. Finite stock is never exceeded; `MaxPlates` caps sheet count; empty parts complete without consuming stock; empty or fully exhausted stock returns `Incomplete/StockExhausted`; a zero-placement candidate stops with `NoPlacementFound` and consumes no sheet.
|
||||
|
||||
`DrawingJobMapper` snapshots caller drawings/items under explicit requirement IDs. The built-in plate nesters create fresh private drawings, items, and plates for each trial through `CandidatePlacementContext` and map returned drawings **by reference**, never by name. Mutable legacy quantities never drive the fulfillment ledger. `PlateNesterFactory` resolves the built-in strategy names (`Default`, `Strip`, `Vertical Remnant`, `Horizontal Remnant`) to instance-scoped placement strategies; it neither reads nor changes the process-global `NestEngineRegistry`, and unknown keys reject. Quantity deduction in the engine paths the runner reaches (base-class fill/pack, strip deduction, remnant-fill ledger, shrink-leftover counting) is keyed by drawing reference, not display name, so same-name drawings and repeated requirements stay independent. `NestResultMaterializer` returns a detached domain nest and `DrawingsByPartId` identity map. Each output plate represents one physical sheet (`Quantity = 1`), and each placement is attached exactly once so domain quantity events do not double count.
|
||||
`DrawingJobMapper` snapshots caller drawings/items under explicit requirement IDs. The built-in plate nesters create fresh private drawings, items, and plates for each trial through `CandidatePlacementContext` and map returned drawings **by reference**, never by name. Per-trial `NestItem` quantities never drive the fulfillment ledger. `PlateNesterFactory` resolves the built-in strategy names (`Default`, `Strip`, `Vertical Remnant`, `Horizontal Remnant`) to instance-scoped placement strategies and rejects unknown keys. Quantity deduction inside a filler run (fill/pack, strip deduction, remnant-fill ledger, shrink-leftover counting) is keyed by drawing reference, not display name, so same-name drawings and repeated requirements stay independent. `NestResultMaterializer` returns a detached domain nest and `DrawingsByPartId` identity map. Each output plate represents one physical sheet (`Quantity = 1`), and each placement is attached exactly once so domain quantity events do not double count.
|
||||
|
||||
```csharp
|
||||
var job = new NestJob(
|
||||
@@ -118,7 +118,7 @@ var domainResult = NestResultMaterializer.Materialize(job, result);
|
||||
|
||||
**Placement strategies:** `Default`, `Strip`, `Vertical Remnant`, and `Horizontal Remnant` are filler-backed built-ins (`OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs`, `StripPlateNester.cs`, `RemnantPlateNester.cs`) that run the internal `Jobs/Placement/Fillers` geometry while keeping demand read-only. A runnable end-to-end example — multiple requirements, mixed finite/unlimited stock, full plate/leftover enumeration — lives in `OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs`.
|
||||
|
||||
**Legacy caller boundaries (not yet migrated):** the desktop UI (`MainForm.RunAutoNestAsync` / `NestSinglePlateAsync`), the CLI (`OpenNest.Console`), and MCP (`NestingTools`) still call the old single-plate `engine.Nest(...)` entry points unchanged. UI adoption needs a separate adapter preserving populated-plate editing, preview routing, and Accept-versus-Cancel semantics. The public API (`OpenNest.Api`, `NestRunner.RunAsync`) already delegates to one `NestJobRunner.Solve` call and reports status, stop reason, part fulfillment, stock usage, and plate-to-stock mapping; `.nestquote` archives carry a schema version and round-trip incomplete jobs.
|
||||
**Single-plate placement:** interactive fills use `PlateFillService`, which explicitly selects one of `Default`, `Strip`, `Vertical Remnant`, or `Horizontal Remnant` and returns proposed parts without mutating the caller's plate. The caller remains responsible for preview accept/cancel and attachment. Whole-job work in the desktop app, console, MCP server, and public API resolves a named `INestingEngine`, solves one `NestJob`, and materializes committed results. `NestRunner.RunAsync` reports status, stop reason, part fulfillment, stock usage, and plate-to-stock mapping; `.nestquote` archives carry a schema version and round-trip incomplete jobs.
|
||||
|
||||
### Fresh DXF job verification (headless)
|
||||
|
||||
@@ -244,7 +244,7 @@ Each engine-on-job solve is independent, so `--parallel <n>` (default 3) runs up
|
||||
|
||||
An engine's layout is rejected (scoring zero for that job) if any part falls outside the work area, any two parts are closer than the required spacing, or a drawing gets more parts placed than requested. A run that doesn't finish within its time budget also scores zero, as a timeout.
|
||||
|
||||
Custom competitor engines can be added by dropping a DLL implementing `INestingEngine` with a public parameterless constructor into the `Engines/` directory next to the benchmark executable; each one is registered under its own CLR type name. This is a separate plugin contract from the desktop app's `NestEngineRegistry`/`NestEngineBase` (which requires a `(Plate)` constructor) — a `NestEngineBase` plugin dropped into the benchmark's `Engines/` folder is silently skipped, since the benchmark only ever solves whole jobs.
|
||||
Custom competitor engines can be added by dropping a DLL that implements `INestingEngine` with a public parameterless constructor into the `Engines/` directory next to the benchmark executable; each one is registered under its own CLR type name. This jobs plug-in contract is also the supported extension point for new nesting engines. Plugins built for the removed single-plate inheritance API are not binary compatible and must be migrated to `INestingEngine`.
|
||||
|
||||
### Conservative bend endpoint repair (opt-in)
|
||||
|
||||
@@ -392,15 +392,17 @@ source geometry changes were made. Source SHA-256:
|
||||
|
||||
## Nesting Engines
|
||||
|
||||
OpenNest uses a pluggable engine architecture. The active engine can be selected at runtime.
|
||||
OpenNest uses jobs-only nesting engines. `NestingEngineRegistry` resolves each engine by explicit name; `NestJobRunner` is the only component that commits demand and stock. Interactive single-plate operations use `PlateFillService` and return proposed parts for the caller to accept or discard.
|
||||
|
||||
| Engine | Description |
|
||||
|--------|-------------|
|
||||
| **Default** | Multi-phase strategy: linear fill, pair fill, rect best-fit, then remainder. Balances density and speed. |
|
||||
| **Default** | Multi-phase strategy: linear fill, pair fill, rect best-fit, then extents. Balances density and speed. |
|
||||
| **Strip** | Iterative shrink-fill strategy for mixed-drawing layouts. |
|
||||
| **Vertical Remnant** | Optimizes for a clean vertical drop on the right side of the plate. |
|
||||
| **Horizontal Remnant** | Optimizes for a clean horizontal drop on the top of the plate. |
|
||||
| **StockLadder** | Whole-job stock-constrained strategy; available to named job callers but not the desktop's four-choice combo. |
|
||||
|
||||
Custom engines can be built by subclassing `NestEngineBase` and registering via `NestEngineRegistry` or dropping a plugin DLL in the `Engines/` directory.
|
||||
Custom nesting plugins must implement `INestingEngine` with a public parameterless constructor and can be loaded from the `Engines/` directory. Plugins built for the removed single-plate inheritance API are not binary compatible.
|
||||
|
||||
### Fill Strategies
|
||||
|
||||
|
||||
Reference in New Issue
Block a user