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.
|
- **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)
|
### 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.
|
- **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.
|
- **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.
|
||||||
- **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.
|
- **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.
|
||||||
- **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`.
|
- **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.
|
||||||
- **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.
|
- **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`.
|
- **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`.
|
- **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.
|
- **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.Geometry;
|
||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
using OpenNest.Engine.Jobs.Adapters;
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
@@ -6,15 +5,14 @@ using OpenNest.Engine.Jobs.Adapters;
|
|||||||
namespace OpenNest.Engine.Tests.Jobs;
|
namespace OpenNest.Engine.Tests.Jobs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strategy selection must be instance-scoped: explicit engine choices work without touching the
|
/// Strategy selection is instance-scoped: explicit engine choices complete independently,
|
||||||
/// process-global NestEngineRegistry.ActiveEngineName, and unknown strategies are rejected.
|
/// and unknown strategies are rejected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NestJobEngineSelectionTests
|
public class NestJobEngineSelectionTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExplicitDefaultAndStripSelectionsDoNotTouchGlobalRegistry()
|
public void ExplicitDefaultAndStripSelectionsCompleteIndependently()
|
||||||
{
|
{
|
||||||
var original = NestEngineRegistry.ActiveEngineName;
|
|
||||||
var job = FiniteStockJobTests.Job(1);
|
var job = FiniteStockJobTests.Job(1);
|
||||||
|
|
||||||
var defaultResult = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
|
var defaultResult = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
|
||||||
@@ -24,14 +22,11 @@ public class NestJobEngineSelectionTests
|
|||||||
new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))
|
new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))
|
||||||
);
|
);
|
||||||
Assert.Equal(NestJobStatus.Complete, stripResult.Status);
|
Assert.Equal(NestJobStatus.Complete, stripResult.Status);
|
||||||
|
|
||||||
Assert.Equal(original, NestEngineRegistry.ActiveEngineName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FactoryResolvesNamedEnginesWithoutGlobalState()
|
public void FactoryResolvesEachNamedBuiltInStrategy()
|
||||||
{
|
{
|
||||||
var original = NestEngineRegistry.ActiveEngineName;
|
|
||||||
var defaultNester = PlateNesterFactory.Create("Default");
|
var defaultNester = PlateNesterFactory.Create("Default");
|
||||||
var stripNester = PlateNesterFactory.Create("Strip");
|
var stripNester = PlateNesterFactory.Create("Strip");
|
||||||
var verticalNester = PlateNesterFactory.Create("Vertical Remnant");
|
var verticalNester = PlateNesterFactory.Create("Vertical Remnant");
|
||||||
@@ -42,7 +37,6 @@ public class NestJobEngineSelectionTests
|
|||||||
Assert.NotNull(verticalNester);
|
Assert.NotNull(verticalNester);
|
||||||
Assert.NotNull(horizontalNester);
|
Assert.NotNull(horizontalNester);
|
||||||
Assert.NotSame(defaultNester, stripNester);
|
Assert.NotSame(defaultNester, stripNester);
|
||||||
Assert.Equal(original, NestEngineRegistry.ActiveEngineName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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]
|
[Fact]
|
||||||
public void StripEngineEndToEndPlacesAndAccounts()
|
public void StripEngineEndToEndPlacesAndAccounts()
|
||||||
{
|
{
|
||||||
@@ -89,10 +67,4 @@ public class NestJobEngineSelectionTests
|
|||||||
foreach (var f in result.Fulfillment)
|
foreach (var f in result.Fulfillment)
|
||||||
Assert.Equal(f.Requested, f.Placed + f.Unplaced);
|
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 OpenNest.Geometry;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Tests.Jobs;
|
namespace OpenNest.Engine.Tests.Jobs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runnable end-to-end example of the whole-job engine API: multiple part requirements, multiple plate
|
/// 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
|
/// 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>
|
/// </summary>
|
||||||
public class NestJobExampleTests
|
public class NestJobExampleTests
|
||||||
{
|
{
|
||||||
@@ -160,36 +161,27 @@ public class NestJobExampleTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Legacy caller boundaries documented for task 8 — these paths still use the old single-plate
|
/// The public placement service is the single-plate contract for preview-driven flows. It proposes
|
||||||
/// engine entry points and are deliberately NOT migrated in this slice. Verified against source at
|
/// parts without mutating the caller's plate, so the UI can accept or discard them explicitly.
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LegacyCompatibilityEntryPointsStillExist()
|
public void PlateFillService_ProposesSinglePlatePartsWithoutMutatingPlate()
|
||||||
{
|
{
|
||||||
var plate = new Plate { Size = new Size(300.0, 200.0), Quadrant = 1 };
|
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 };
|
var item = new NestItem { Drawing = drawing, Quantity = 1 };
|
||||||
|
|
||||||
// MainForm/Console/MCP still reach the legacy single-plate signature unchanged; the engine
|
var parts = PlateFillService.Nest(
|
||||||
// returns placed Parts for the caller to attach (legacy paths do not attach on their own).
|
"Default",
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
plate,
|
||||||
var parts = engine.Nest(new List<NestItem> { item }, null, CancellationToken.None);
|
new List<NestItem> { item },
|
||||||
|
progress: null,
|
||||||
|
token: CancellationToken.None
|
||||||
|
);
|
||||||
|
|
||||||
Assert.NotNull(engine);
|
|
||||||
var placed = Assert.Single(parts);
|
var placed = Assert.Single(parts);
|
||||||
Assert.Same(drawing, placed.BaseDrawing);
|
Assert.Same(drawing, placed.BaseDrawing);
|
||||||
|
Assert.Empty(plate.Parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NestJobPart Part(
|
private static NestJobPart Part(
|
||||||
|
|||||||
@@ -11,55 +11,31 @@ public class PlateFillerContractTests
|
|||||||
[InlineData("Default")]
|
[InlineData("Default")]
|
||||||
[InlineData("Vertical Remnant")]
|
[InlineData("Vertical Remnant")]
|
||||||
[InlineData("Horizontal 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 plate = new Plate(new Size(30, 50));
|
||||||
var facadePlate = new Plate(new Size(30, 50));
|
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
|
||||||
var directDrawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
|
var filler = CreateFiller(strategy, plate);
|
||||||
var facadeDrawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
|
|
||||||
var directFiller = CreateFiller(strategy, directPlate);
|
|
||||||
var facade = CreateFacade(strategy, facadePlate);
|
|
||||||
|
|
||||||
var directParts = directFiller.Fill(
|
var parts = filler.Fill(
|
||||||
new NestItem { Drawing = directDrawing, Quantity = 4 },
|
new NestItem { Drawing = drawing, Quantity = 4 },
|
||||||
directPlate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
|
||||||
CancellationToken.None
|
|
||||||
);
|
|
||||||
var facadeParts = facade.Fill(
|
|
||||||
new NestItem { Drawing = facadeDrawing, Quantity = 4 },
|
|
||||||
facadePlate.WorkArea(),
|
|
||||||
null,
|
null,
|
||||||
CancellationToken.None
|
CancellationToken.None
|
||||||
);
|
);
|
||||||
|
|
||||||
Assert.Equal(facade.WinnerPhase, directFiller.WinnerPhase);
|
Assert.NotEmpty(parts);
|
||||||
Assert.Equal(
|
Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
|
||||||
facade.PhaseResults.Select(result => (result.Phase, result.PartCount)),
|
Assert.NotEmpty(filler.PhaseResults);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CompatibilityDefaultFacade_UsesOverriddenAngleSelection()
|
public void DefaultPlateFiller_UsesOverriddenAngleSelection()
|
||||||
{
|
{
|
||||||
var plate = new Plate(new Size(30, 50));
|
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
|
new NestItem
|
||||||
{
|
{
|
||||||
Drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4)),
|
Drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4)),
|
||||||
@@ -71,18 +47,18 @@ public class PlateFillerContractTests
|
|||||||
);
|
);
|
||||||
|
|
||||||
Assert.NotEmpty(parts);
|
Assert.NotEmpty(parts);
|
||||||
Assert.True(engine.BuildAnglesCalled);
|
Assert.True(filler.BuildAnglesCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CompatibilityDefaultFacade_Nest_UsesOverriddenFillAndPackArea()
|
public void DefaultPlateFiller_Nest_UsesOverriddenFillAndPackArea()
|
||||||
{
|
{
|
||||||
var plate = new Plate(new Size(100, 100));
|
var plate = new Plate(new Size(100, 100));
|
||||||
var fillDrawing = new Drawing("fill", TestDrawingFactory.Rectangle(10, 10));
|
var fillDrawing = new Drawing("fill", TestDrawingFactory.Rectangle(10, 10));
|
||||||
var packDrawing = new Drawing("pack", 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 List<NestItem>
|
||||||
{
|
{
|
||||||
new() { Drawing = fillDrawing, Quantity = 10 },
|
new() { Drawing = fillDrawing, Quantity = 10 },
|
||||||
@@ -92,17 +68,16 @@ public class PlateFillerContractTests
|
|||||||
CancellationToken.None
|
CancellationToken.None
|
||||||
);
|
);
|
||||||
|
|
||||||
Assert.Equal(1, engine.FillCalls);
|
Assert.Equal(1, filler.FillCalls);
|
||||||
Assert.Equal(1, engine.PackAreaCalls);
|
Assert.Equal(1, filler.PackAreaCalls);
|
||||||
Assert.Equal(11, parts.Count);
|
Assert.Equal(11, parts.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StripPlateFiller_Nest_MatchesCompatibilityFacade()
|
public void StripPlateFiller_Nest_ReturnsPlacedPartsAndDeductsInput()
|
||||||
{
|
{
|
||||||
var directPlate = new Plate(new Size(30, 50));
|
var plate = new Plate(new Size(30, 50));
|
||||||
var facadePlate = new Plate(new Size(30, 50));
|
var items = new List<NestItem>
|
||||||
var directItems = new List<NestItem>
|
|
||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
@@ -120,48 +95,29 @@ public class PlateFillerContractTests
|
|||||||
Quantity = 3,
|
Quantity = 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
var facadeItems = new List<NestItem>
|
var filler = new StripPlateFiller(plate) { PlateNumber = 7 };
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Drawing = new Drawing("rect-a", TestDrawingFactory.Rectangle(6, 4)),
|
|
||||||
Quantity = 5,
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Drawing = new Drawing("rect-b", TestDrawingFactory.Rectangle(4, 3)),
|
|
||||||
Quantity = 4,
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Drawing = new Drawing("rect-c", TestDrawingFactory.Rectangle(2, 7)),
|
|
||||||
Quantity = 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
var directFiller = new StripPlateFiller(directPlate) { PlateNumber = 7 };
|
|
||||||
var facade = new StripNestEngine(facadePlate) { PlateNumber = 7 };
|
|
||||||
|
|
||||||
var directParts = directFiller.Nest(directItems, null, CancellationToken.None);
|
var parts = filler.Nest(items, null, CancellationToken.None);
|
||||||
var facadeParts = facade.Nest(facadeItems, null, CancellationToken.None);
|
|
||||||
|
|
||||||
AssertEquivalentLayouts(facadeParts, directParts);
|
Assert.NotEmpty(parts);
|
||||||
Assert.Equal(facadeItems.Select(item => item.Quantity), directItems.Select(item => item.Quantity));
|
Assert.All(parts, part => Assert.Contains(items, item => ReferenceEquals(item.Drawing, part.BaseDrawing)));
|
||||||
|
Assert.All(items, item => Assert.InRange(item.Quantity, 0, 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CompatibilityStripFacade_Nest_UsesOverriddenPackArea()
|
public void StripPlateFiller_Nest_UsesOverriddenPackArea()
|
||||||
{
|
{
|
||||||
var plate = new Plate(new Size(100, 100));
|
var plate = new Plate(new Size(100, 100));
|
||||||
var drawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
|
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 } },
|
new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } },
|
||||||
null,
|
null,
|
||||||
CancellationToken.None
|
CancellationToken.None
|
||||||
);
|
);
|
||||||
|
|
||||||
Assert.Equal(1, engine.PackAreaCalls);
|
Assert.Equal(1, filler.PackAreaCalls);
|
||||||
Assert.Single(parts);
|
Assert.Single(parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,12 +260,12 @@ public class PlateFillerContractTests
|
|||||||
Assert.Equal(10, item.Quantity);
|
Assert.Equal(10, item.Quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FillAndPackProbeDefaultNestEngine : DefaultNestEngine
|
private sealed class FillAndPackProbeDefaultPlateFiller : DefaultPlateFiller
|
||||||
{
|
{
|
||||||
private readonly Drawing fillDrawing;
|
private readonly Drawing fillDrawing;
|
||||||
private readonly Drawing packDrawing;
|
private readonly Drawing packDrawing;
|
||||||
|
|
||||||
internal FillAndPackProbeDefaultNestEngine(
|
internal FillAndPackProbeDefaultPlateFiller(
|
||||||
Plate plate,
|
Plate plate,
|
||||||
Drawing fillDrawing,
|
Drawing fillDrawing,
|
||||||
Drawing packDrawing
|
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) { }
|
: base(plate) { }
|
||||||
|
|
||||||
internal bool BuildAnglesCalled { get; private set; }
|
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;
|
private readonly Drawing drawing;
|
||||||
|
|
||||||
internal PackProbeStripNestEngine(Plate plate, Drawing drawing)
|
internal PackProbeStripPlateFiller(Plate plate, Drawing drawing)
|
||||||
: base(plate)
|
: base(plate)
|
||||||
{
|
{
|
||||||
this.drawing = drawing;
|
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
|
private static PlateFillerBase CreateFiller(string strategy, Plate plate) => strategy switch
|
||||||
{
|
{
|
||||||
"Default" => new DefaultPlateFiller(plate),
|
"Default" => new DefaultPlateFiller(plate),
|
||||||
@@ -428,14 +359,6 @@ public class PlateFillerContractTests
|
|||||||
_ => throw new ArgumentOutOfRangeException(nameof(strategy)),
|
_ => 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>
|
private sealed class CapturingProgress : IProgress<NestProgress>
|
||||||
{
|
{
|
||||||
public List<NestProgress> Reports { get; } = new();
|
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>
|
/// <summary>
|
||||||
/// Ranks fill results by count first, then density.
|
/// 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>
|
/// </summary>
|
||||||
public class DefaultFillComparer : IFillComparer
|
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;
|
namespace OpenNest.Engine.Jobs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registry of whole-job INestingEngine implementations, parallel to NestEngineRegistry (which is for
|
/// Registry of whole-job <see cref="INestingEngine"/> implementations. The four production
|
||||||
/// the legacy single-plate NestEngineBase). The four production strategies are exposed here through
|
/// strategies are exposed through <see cref="FixedStrategyNestingEngine"/> so they compete on
|
||||||
/// FixedStrategyNestingEngine so they compete on equal footing with model-submitted engines. Unlike
|
/// equal footing with model-submitted engines. Callers choose an engine explicitly from
|
||||||
/// NestEngineRegistry, this has no ActiveEngineName/global-selection concept — callers choose an engine
|
/// <see cref="AvailableEngines"/>; there is no process-global active selection.
|
||||||
/// explicitly from AvailableEngines.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NestingEngineRegistry
|
public static class NestingEngineRegistry
|
||||||
{
|
{
|
||||||
@@ -55,8 +54,7 @@ public static class NestingEngineRegistry
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the engine registered under <paramref name="name"/> (case-insensitive). The caller's
|
/// 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
|
/// explicit choice is the whole selection mechanism; unknown names throw.
|
||||||
/// process-global active name to consult or mutate. Unknown names throw.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static INestingEngine Create(string name)
|
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
|
/// <summary>Scans *.dll in directory for non-abstract INestingEngine types with a public
|
||||||
/// parameterless constructor, registering each under its CLR type name. Mirrors
|
/// parameterless constructor, registering each under its CLR type name. Per-assembly and per-type
|
||||||
/// NestEngineRegistry.LoadPlugins's per-assembly/per-type isolation: one bad plugin never
|
/// isolation ensures one bad plugin never prevents the rest from loading.</summary>
|
||||||
/// prevents the rest from loading.</summary>
|
|
||||||
public static void LoadPlugins(string directory)
|
public static void LoadPlugins(string directory)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(directory))
|
if (!Directory.Exists(directory))
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ internal class DefaultPlateFiller : PlateFillerBase
|
|||||||
internal DefaultPlateFiller(Plate plate)
|
internal DefaultPlateFiller(Plate plate)
|
||||||
: base(plate) { }
|
: base(plate) { }
|
||||||
|
|
||||||
protected override IFillComparer CreateComparer() => CreateComparerCore();
|
protected override IFillComparer CreateComparer() => new DefaultFillComparer();
|
||||||
|
|
||||||
internal IFillComparer CreateComparerCore() => new DefaultFillComparer();
|
|
||||||
|
|
||||||
internal bool ForceFullAngleSweep
|
internal bool ForceFullAngleSweep
|
||||||
{
|
{
|
||||||
@@ -34,18 +32,9 @@ internal class DefaultPlateFiller : PlateFillerBase
|
|||||||
NestItem item,
|
NestItem item,
|
||||||
ClassificationResult classification,
|
ClassificationResult classification,
|
||||||
Box workArea
|
Box workArea
|
||||||
) => BuildAnglesCore(item, classification, workArea);
|
|
||||||
|
|
||||||
internal List<double> BuildAnglesCore(
|
|
||||||
NestItem item,
|
|
||||||
ClassificationResult classification,
|
|
||||||
Box workArea
|
|
||||||
) => angleBuilder.Build(item, classification, workArea);
|
) => angleBuilder.Build(item, classification, workArea);
|
||||||
|
|
||||||
protected override void RecordProductiveAngles(List<AngleResult> angleResults) =>
|
protected override void RecordProductiveAngles(List<AngleResult> angleResults)
|
||||||
RecordProductiveAnglesCore(angleResults);
|
|
||||||
|
|
||||||
internal void RecordProductiveAnglesCore(List<AngleResult> angleResults)
|
|
||||||
{
|
{
|
||||||
angleBuilder.RecordProductive(angleResults);
|
angleBuilder.RecordProductive(angleResults);
|
||||||
}
|
}
|
||||||
@@ -419,9 +408,7 @@ internal class DefaultPlateFiller : PlateFillerBase
|
|||||||
return BinConverter.ToParts(bin, items);
|
return BinConverter.ToParts(bin, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void RunPipeline(FillContext context) => RunPipelineCore(context);
|
protected virtual void RunPipeline(FillContext context)
|
||||||
|
|
||||||
internal void RunPipelineCore(FillContext context)
|
|
||||||
{
|
{
|
||||||
var classification = PartClassifier.Classify(context.Item.Drawing);
|
var classification = PartClassifier.Classify(context.Item.Drawing);
|
||||||
context.PartType = classification.Type;
|
context.PartType = classification.Type;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Engine.Strategies;
|
using OpenNest.Engine.Strategies;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest.Engine.Jobs.Placement.Fillers;
|
namespace OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
|
|
||||||
@@ -27,11 +25,6 @@ internal abstract class PlateFillerBase
|
|||||||
|
|
||||||
public NestPhase WinnerPhase { get; protected set; }
|
public NestPhase WinnerPhase { get; protected set; }
|
||||||
|
|
||||||
internal void SetWinnerPhase(NestPhase winnerPhase)
|
|
||||||
{
|
|
||||||
WinnerPhase = winnerPhase;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<PhaseResult> PhaseResults { get; } = new();
|
public List<PhaseResult> PhaseResults { get; } = new();
|
||||||
|
|
||||||
public List<AngleResult> AngleResults { get; } = new();
|
public List<AngleResult> AngleResults { get; } = new();
|
||||||
@@ -61,11 +54,9 @@ internal abstract class PlateFillerBase
|
|||||||
|
|
||||||
protected FillPolicy BuildPolicy() => new(Comparer, PreferredDirection);
|
protected FillPolicy BuildPolicy() => new(Comparer, PreferredDirection);
|
||||||
|
|
||||||
internal IFillComparer FillComparer => Comparer;
|
|
||||||
|
|
||||||
protected string BuildProgressSummary() => BuildProgressSummary(PhaseResults);
|
protected string BuildProgressSummary() => BuildProgressSummary(PhaseResults);
|
||||||
|
|
||||||
internal static string BuildProgressSummary(IReadOnlyList<PhaseResult> phaseResults)
|
private static string BuildProgressSummary(IReadOnlyList<PhaseResult> phaseResults)
|
||||||
{
|
{
|
||||||
if (phaseResults.Count == 0)
|
if (phaseResults.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
@@ -78,70 +69,7 @@ internal abstract class PlateFillerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea) =>
|
protected bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea) =>
|
||||||
IsBetterFill(Comparer, candidate, current, workArea);
|
Comparer.IsBetter(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual List<Part> Fill(
|
public virtual List<Part> Fill(
|
||||||
NestItem item,
|
NestItem item,
|
||||||
|
|||||||
@@ -40,13 +40,6 @@ internal class StripPlateFiller : PlateFillerBase
|
|||||||
List<NestItem> items,
|
List<NestItem> items,
|
||||||
IProgress<NestProgress> progress,
|
IProgress<NestProgress> progress,
|
||||||
CancellationToken token
|
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);
|
var inner = new DefaultPlateFiller(Plate);
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ namespace OpenNest.Engine.Jobs;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instance-scoped strategy resolution for the whole-job runner. All four built-in strategies
|
/// 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
|
/// resolve directly to filler-backed plate nesters. Selection is explicit per job; no shared
|
||||||
/// (including plugin registrations and ActiveEngineName) is neither read nor modified.
|
/// mutable strategy setting is read or modified. Unknown keys reject.
|
||||||
/// Unknown keys reject.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class PlateNesterFactory
|
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
|
/// 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="OriginalDrawing"/> isn't set. Internal so callers that build their own
|
||||||
/// <see cref="ProgressReport"/> outside <see cref="ReportProgress"/> (e.g. the fallback
|
/// <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.
|
/// can apply the same rebind before reaching the UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal List<Part> ToOriginalFrame(List<Part> parts)
|
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 OpenNest.IO;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
|
|
||||||
namespace OpenNest.Tests.Engine;
|
namespace OpenNest.Tests.Engine;
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ public class EngineOverlapTests
|
|||||||
[InlineData("Strip")]
|
[InlineData("Strip")]
|
||||||
[InlineData("Vertical Remnant")]
|
[InlineData("Vertical Remnant")]
|
||||||
[InlineData("Horizontal Remnant")]
|
[InlineData("Horizontal Remnant")]
|
||||||
public void FillPlate_NoOverlaps(string engineName)
|
public void FillPlate_NoOverlaps(string strategy)
|
||||||
{
|
{
|
||||||
var drawing = ImportDxf();
|
var drawing = ImportDxf();
|
||||||
if (drawing is null)
|
if (drawing is null)
|
||||||
@@ -39,23 +40,21 @@ public class EngineOverlapTests
|
|||||||
|
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
|
|
||||||
NestEngineRegistry.ActiveEngineName = engineName;
|
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
|
||||||
|
|
||||||
var item = new NestItem { Drawing = drawing };
|
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(
|
_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
|
// Show rotation distribution
|
||||||
var rotGroups = plate
|
var rotGroups = plate
|
||||||
.Parts.GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1))
|
.Parts.GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1))
|
||||||
@@ -74,7 +73,7 @@ public class EngineOverlapTests
|
|||||||
|
|
||||||
Assert.False(
|
Assert.False(
|
||||||
hasOverlaps,
|
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.Geometry;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
|
|
||||||
namespace OpenNest.Tests.Engine;
|
namespace OpenNest.Tests.Engine;
|
||||||
|
|
||||||
@@ -17,49 +18,49 @@ public class EngineRefactorSmokeTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultEngine_FillNestItem_ProducesResults()
|
public void DefaultPlateFiller_FillNestItem_ProducesResults()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
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 item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||||
|
|
||||||
var parts = engine.Fill(
|
var parts = filler.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
System.Threading.CancellationToken.None
|
System.Threading.CancellationToken.None
|
||||||
);
|
);
|
||||||
|
|
||||||
Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts");
|
Assert.True(parts.Count > 0, "DefaultPlateFiller should fill parts");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultEngine_FillGroupParts_ProducesResults()
|
public void DefaultPlateFiller_FillGroupParts_ProducesResults()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
var drawing = MakeRectDrawing(20, 10);
|
var drawing = MakeRectDrawing(20, 10);
|
||||||
var groupParts = new List<Part> { new Part(drawing) };
|
var groupParts = new List<Part> { new Part(drawing) };
|
||||||
|
|
||||||
var parts = engine.Fill(
|
var parts = filler.Fill(
|
||||||
groupParts,
|
groupParts,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
System.Threading.CancellationToken.None
|
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]
|
[Fact]
|
||||||
public void DefaultEngine_ForceFullAngleSweep_StillWorks()
|
public void DefaultPlateFiller_ForceFullAngleSweep_StillWorks()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
engine.ForceFullAngleSweep = true;
|
filler.ForceFullAngleSweep = true;
|
||||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||||
|
|
||||||
var parts = engine.Fill(
|
var parts = filler.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
@@ -70,33 +71,33 @@ public class EngineRefactorSmokeTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StripEngine_Nest_ProducesResults()
|
public void StripPlateFiller_Nest_ProducesResults()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
var engine = new StripNestEngine(plate);
|
var filler = new StripPlateFiller(plate);
|
||||||
var items = new List<NestItem>
|
var items = new List<NestItem>
|
||||||
{
|
{
|
||||||
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
|
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
|
||||||
new NestItem { Drawing = MakeRectDrawing(8, 5, "small"), Quantity = 5 },
|
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]
|
[Fact]
|
||||||
public void DefaultEngine_Nest_ProducesResults()
|
public void DefaultPlateFiller_Nest_ProducesResults()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
var items = new List<NestItem>
|
var items = new List<NestItem>
|
||||||
{
|
{
|
||||||
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
|
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
|
||||||
new NestItem { Drawing = MakeRectDrawing(15, 8, "b"), Quantity = 3 },
|
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");
|
Assert.True(parts.Count > 0, "Base Nest method should place parts");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace OpenNest.Tests.Engine;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase 3.2: MultiPlateNester and PlateOptimizer take an explicit placement strategy at their
|
/// 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>
|
/// </summary>
|
||||||
public class ExplicitPlacementStrategyTests
|
public class ExplicitPlacementStrategyTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Threading;
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.BestFit;
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
|
||||||
@@ -37,9 +38,9 @@ public class NestInvarianceTests
|
|||||||
private static int RunFillCount(Drawing drawing, Plate plate)
|
private static int RunFillCount(Drawing drawing, Plate plate)
|
||||||
{
|
{
|
||||||
BestFitCache.Clear();
|
BestFitCache.Clear();
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
var item = new NestItem { Drawing = drawing };
|
var item = new NestItem { Drawing = drawing };
|
||||||
var parts = engine.Fill(
|
var parts = filler.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
progress: null,
|
progress: null,
|
||||||
@@ -71,9 +72,9 @@ public class NestInvarianceTests
|
|||||||
foreach (var theta in new[] { 0.0, 0.3, 0.8, 1.2 })
|
foreach (var theta in new[] { 0.0, 0.3, 0.8, 1.2 })
|
||||||
{
|
{
|
||||||
BestFitCache.Clear();
|
BestFitCache.Clear();
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
var item = new NestItem { Drawing = MakeImportedAt(theta) };
|
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);
|
Assert.NotNull(parts);
|
||||||
foreach (var p in parts)
|
foreach (var p in parts)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Tests.Engine;
|
namespace OpenNest.Tests.Engine;
|
||||||
@@ -18,82 +20,79 @@ public class RemnantEngineTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VerticalRemnantEngine_UsesVerticalRemnantComparer()
|
public void VerticalRemnantPlateFiller_UsesHorizontalPreferredDirection()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
var engine = new VerticalRemnantEngine(plate);
|
var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
|
||||||
Assert.Equal("Vertical Remnant", engine.Name);
|
Assert.Equal(NestDirection.Horizontal, filler.PreferredDirection);
|
||||||
Assert.Equal(NestDirection.Horizontal, engine.PreferredDirection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HorizontalRemnantEngine_UsesHorizontalRemnantComparer()
|
public void HorizontalRemnantPlateFiller_UsesVerticalPreferredDirection()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
var engine = new HorizontalRemnantEngine(plate);
|
var filler = new RemnantPlateFiller(plate, RemnantFillPolicy.Horizontal);
|
||||||
Assert.Equal("Horizontal Remnant", engine.Name);
|
Assert.Equal(NestDirection.Vertical, filler.PreferredDirection);
|
||||||
Assert.Equal(NestDirection.Vertical, engine.PreferredDirection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VerticalRemnantEngine_Fill_ProducesResults()
|
public void VerticalRemnantPlateFiller_Fill_ProducesResults()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
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 item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||||
|
|
||||||
var parts = engine.Fill(
|
var parts = filler.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
System.Threading.CancellationToken.None
|
System.Threading.CancellationToken.None
|
||||||
);
|
);
|
||||||
|
|
||||||
Assert.True(parts.Count > 0, "VerticalRemnantEngine should fill parts");
|
Assert.True(parts.Count > 0, "VerticalRemnantPlateFiller should fill parts");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HorizontalRemnantEngine_Fill_ProducesResults()
|
public void HorizontalRemnantPlateFiller_Fill_ProducesResults()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
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 item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||||
|
|
||||||
var parts = engine.Fill(
|
var parts = filler.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
System.Threading.CancellationToken.None
|
System.Threading.CancellationToken.None
|
||||||
);
|
);
|
||||||
|
|
||||||
Assert.True(parts.Count > 0, "HorizontalRemnantEngine should fill parts");
|
Assert.True(parts.Count > 0, "HorizontalRemnantPlateFiller should fill parts");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Registry_ContainsBothRemnantEngines()
|
public void PlateFillService_ListsBothRemnantStrategies()
|
||||||
{
|
{
|
||||||
var names = NestEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
|
Assert.Contains("Vertical Remnant", PlateFillService.BuiltInStrategies);
|
||||||
Assert.Contains("Vertical Remnant", names);
|
Assert.Contains("Horizontal Remnant", PlateFillService.BuiltInStrategies);
|
||||||
Assert.Contains("Horizontal Remnant", names);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VerticalRemnantEngine_ProducesTighterXExtent_ThanDefault()
|
public void VerticalRemnantPlateFiller_ProducesTighterXExtent_ThanDefault()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
var plate = new Plate(60, 120);
|
||||||
var drawing = MakeRectDrawing(20, 10);
|
var drawing = MakeRectDrawing(20, 10);
|
||||||
var item = new NestItem { Drawing = drawing };
|
var item = new NestItem { Drawing = drawing };
|
||||||
|
|
||||||
var defaultEngine = new DefaultNestEngine(plate);
|
var defaultFiller = new DefaultPlateFiller(plate);
|
||||||
var remnantEngine = new VerticalRemnantEngine(plate);
|
var remnantFiller = new RemnantPlateFiller(plate, RemnantFillPolicy.Vertical);
|
||||||
|
|
||||||
var defaultParts = defaultEngine.Fill(
|
var defaultParts = defaultFiller.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
System.Threading.CancellationToken.None
|
System.Threading.CancellationToken.None
|
||||||
);
|
);
|
||||||
var remnantParts = remnantEngine.Fill(
|
var remnantParts = remnantFiller.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
@@ -110,7 +109,7 @@ public class RemnantEngineTests
|
|||||||
|
|
||||||
Assert.True(
|
Assert.True(
|
||||||
remnantXExtent <= defaultXExtent + 0.01,
|
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.Fill;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
|
||||||
@@ -54,8 +55,8 @@ public class IterativeShrinkFillerTests
|
|||||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
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) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
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) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 60, 30), fillFunc, 1.0);
|
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) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
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.Fill;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
|
||||||
@@ -35,8 +36,8 @@ public class RemnantFillerTests2
|
|||||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
var placed = filler.FillItems(items, fillFunc);
|
var placed = filler.FillItems(items, fillFunc);
|
||||||
@@ -59,8 +60,8 @@ public class RemnantFillerTests2
|
|||||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
filler.FillItems(items, fillFunc);
|
filler.FillItems(items, fillFunc);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
|
||||||
@@ -27,8 +28,8 @@ public class ShrinkFillerTests
|
|||||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Length);
|
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) =>
|
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||||
{
|
{
|
||||||
var plate = new Plate(b.Width, b.Length);
|
var plate = new Plate(b.Width, b.Length);
|
||||||
var engine = new DefaultNestEngine(plate);
|
var filler = new DefaultPlateFiller(plate);
|
||||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
return filler.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Width);
|
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Width);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Engine.Strategies;
|
using OpenNest.Engine.Strategies;
|
||||||
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
|
||||||
@@ -21,14 +22,14 @@ public class FillPipelineTests
|
|||||||
public void Pipeline_PopulatesPhaseResults()
|
public void Pipeline_PopulatesPhaseResults()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
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 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(
|
Assert.True(
|
||||||
engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
|
filler.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
|
||||||
$"Expected phase results from all active strategies, got {engine.PhaseResults.Count}"
|
$"Expected phase results from all active strategies, got {filler.PhaseResults.Count}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +37,10 @@ public class FillPipelineTests
|
|||||||
public void Pipeline_SetsWinnerPhase()
|
public void Pipeline_SetsWinnerPhase()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
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 item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||||
|
|
||||||
var parts = engine.Fill(
|
var parts = filler.Fill(
|
||||||
item,
|
item,
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
null,
|
null,
|
||||||
@@ -48,11 +49,11 @@ public class FillPipelineTests
|
|||||||
|
|
||||||
Assert.True(parts.Count > 0);
|
Assert.True(parts.Count > 0);
|
||||||
Assert.True(
|
Assert.True(
|
||||||
engine.WinnerPhase == NestPhase.Pairs
|
filler.WinnerPhase == NestPhase.Pairs
|
||||||
|| engine.WinnerPhase == NestPhase.Linear
|
|| filler.WinnerPhase == NestPhase.Linear
|
||||||
|| engine.WinnerPhase == NestPhase.RectBestFit
|
|| filler.WinnerPhase == NestPhase.RectBestFit
|
||||||
|| engine.WinnerPhase == NestPhase.Extents
|
|| filler.WinnerPhase == NestPhase.Extents
|
||||||
|| engine.WinnerPhase == NestPhase.Custom
|
|| filler.WinnerPhase == NestPhase.Custom
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +61,13 @@ public class FillPipelineTests
|
|||||||
public void Pipeline_RespectsCancellation()
|
public void Pipeline_RespectsCancellation()
|
||||||
{
|
{
|
||||||
var plate = new Plate(60, 120);
|
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 item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||||
var cts = new System.Threading.CancellationTokenSource();
|
var cts = new System.Threading.CancellationTokenSource();
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
|
|
||||||
// Pre-cancelled token should return empty or partial results without throwing
|
// 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
|
// Should not throw — graceful degradation
|
||||||
Assert.NotNull(parts);
|
Assert.NotNull(parts);
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ using OpenNest.Engine.Jobs.Placement;
|
|||||||
namespace OpenNest.Forms
|
namespace OpenNest.Forms
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// App-scoped nesting-engine selection — the desktop replacement for the process-global
|
/// App-scoped nesting-engine selection. The selected name addresses a whole-job
|
||||||
/// NestEngineRegistry.ActiveEngineName. The selected name addresses a whole-job
|
|
||||||
/// INestingEngine resolved through NestingEngineRegistry at call time; single-plate
|
/// INestingEngine resolved through NestingEngineRegistry at call time; single-plate
|
||||||
/// interactive fill uses FillStrategy, which maps a built-in engine to its placement
|
/// 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 Default for jobs-only engines (StockLadder, plug-ins).
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ namespace OpenNest.Forms
|
|||||||
// BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
// BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||||
|
|
||||||
// Jobs-side plug-in discovery: INestingEngine implementations are registered per
|
// Jobs-side plug-in discovery: INestingEngine implementations are registered per
|
||||||
// assembly/type with the same per-DLL isolation as before. Binary plug-ins derived
|
// assembly/type with per-DLL isolation. Existing plug-ins must implement the jobs
|
||||||
// from the legacy NestEngineBase no longer load here after the Phase-4 removal.
|
// contract and expose a public parameterless constructor.
|
||||||
var enginesDir = Path.Combine(Application.StartupPath, "Engines");
|
var enginesDir = Path.Combine(Application.StartupPath, "Engines");
|
||||||
NestingEngineRegistry.LoadPlugins(enginesDir);
|
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.
|
`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
|
```csharp
|
||||||
var job = new NestJob(
|
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`.
|
**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)
|
### 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.
|
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)
|
### Conservative bend endpoint repair (opt-in)
|
||||||
|
|
||||||
@@ -392,15 +392,17 @@ source geometry changes were made. Source SHA-256:
|
|||||||
|
|
||||||
## Nesting Engines
|
## 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 |
|
| 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. |
|
| **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. |
|
| **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
|
### Fill Strategies
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user