diff --git a/OpenNest.Engine/OpenNest.Engine.csproj b/OpenNest.Engine/OpenNest.Engine.csproj
index b2a9561f..59012891 100644
--- a/OpenNest.Engine/OpenNest.Engine.csproj
+++ b/OpenNest.Engine/OpenNest.Engine.csproj
@@ -8,6 +8,7 @@
+
diff --git a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs
index abb1bb1f..30b5b4d6 100644
--- a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs
+++ b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs
@@ -1,31 +1,40 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
using System.Linq;
-using System.Reflection;
namespace OpenNest.Engine.Strategies
{
+ ///
+ /// The built-in fill strategies in pipeline order, with one process-wide disabled set that the
+ /// desktop Options form sets. Every fill that runs the strategy pipeline reads it.
+ ///
public static class FillStrategyRegistry
{
- private static readonly List strategies = new();
+ // Pipeline order, ascending by each strategy's Order value.
+ private static readonly IReadOnlyList builtIn = Array.AsReadOnly(
+ new IFillStrategy[]
+ {
+ new PairsFillStrategy(),
+ new RowFillStrategy(),
+ new ColumnFillStrategy(),
+ new RectBestFitStrategy(),
+ new ExtentsFillStrategy(),
+ new LinearFillStrategy(),
+ new TiltedRemnantStrategy(),
+ }
+ );
+
+ private static IReadOnlyList strategies = builtIn;
private static List sorted;
private static HashSet enabledFilter;
private static readonly HashSet disabled = new(StringComparer.OrdinalIgnoreCase);
- static FillStrategyRegistry()
- {
- LoadFrom(typeof(FillStrategyRegistry).Assembly);
- }
-
public static IReadOnlyList Strategies => sorted ??= FilterStrategies();
///
- /// Returns all registered strategies regardless of enabled/disabled state.
+ /// Returns all strategies regardless of enabled/disabled state.
///
- public static IReadOnlyList AllStrategies =>
- strategies.OrderBy(s => s.Order).ToList();
+ public static IReadOnlyList AllStrategies => strategies;
///
/// Returns the names of all permanently disabled strategies.
@@ -38,11 +47,11 @@ namespace OpenNest.Engine.Strategies
enabledFilter != null
? strategies.Where(s => enabledFilter.Contains(s.Name))
: strategies.Where(s => !disabled.Contains(s.Name));
- return source.OrderBy(s => s.Order).ToList();
+ return source.ToList();
}
///
- /// Permanently disables strategies by name. They remain registered
+ /// Permanently disables strategies by name. They remain listed
/// but are excluded from the default pipeline.
///
public static void Disable(params string[] names)
@@ -75,79 +84,31 @@ namespace OpenNest.Engine.Strategies
sorted = null;
}
- public static void LoadFrom(Assembly assembly)
+ ///
+ /// Replaces the built-in list, in the given order, until the result is disposed. For tests
+ /// that drive fills through a host that builds its own filler, such as the plate view.
+ ///
+ internal static IDisposable UseForTesting(IReadOnlyList replacement)
{
- foreach (var type in assembly.GetTypes())
- {
- if (
- type.IsAbstract
- || type.IsInterface
- || !typeof(IFillStrategy).IsAssignableFrom(type)
- )
- continue;
-
- var ctor = type.GetConstructor(Type.EmptyTypes);
- if (ctor == null)
- {
- Debug.WriteLine(
- $"[FillStrategyRegistry] Skipping {type.Name}: no parameterless constructor"
- );
- continue;
- }
-
- try
- {
- var instance = (IFillStrategy)ctor.Invoke(null);
-
- if (
- strategies.Any(s =>
- s.Name.Equals(instance.Name, StringComparison.OrdinalIgnoreCase)
- )
- )
- {
- Debug.WriteLine(
- $"[FillStrategyRegistry] Duplicate strategy '{instance.Name}' skipped"
- );
- continue;
- }
-
- strategies.Add(instance);
- Debug.WriteLine(
- $"[FillStrategyRegistry] Registered: {instance.Name} (Order={instance.Order})"
- );
- }
- catch (Exception ex)
- {
- Debug.WriteLine(
- $"[FillStrategyRegistry] Failed to instantiate {type.Name}: {ex.Message}"
- );
- }
- }
-
+ var prior = strategies;
+ strategies = replacement;
sorted = null;
+ return new Restore(prior);
}
- public static void LoadPlugins(string directory)
+ private sealed class Restore : IDisposable
{
- if (!Directory.Exists(directory))
- return;
+ private IReadOnlyList prior;
- foreach (var dll in Directory.GetFiles(directory, "*.dll"))
+ public Restore(IReadOnlyList prior) => this.prior = prior;
+
+ public void Dispose()
{
- try
- {
- var assembly = Assembly.LoadFrom(dll);
- LoadFrom(assembly);
- Debug.WriteLine(
- $"[FillStrategyRegistry] Loaded plugin assembly: {Path.GetFileName(dll)}"
- );
- }
- catch (Exception ex)
- {
- Debug.WriteLine(
- $"[FillStrategyRegistry] Failed to load {Path.GetFileName(dll)}: {ex.Message}"
- );
- }
+ if (prior == null)
+ return;
+ strategies = prior;
+ sorted = null;
+ prior = null;
}
}
}
diff --git a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs
index b2de875e..6f98c54e 100644
--- a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs
+++ b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs
@@ -6,20 +6,16 @@ namespace OpenNest.Tests.Strategies;
public class FillStrategyRegistryTests
{
[Fact]
- public void Registry_DiscoversBuiltInStrategies()
+ public void Registry_ListsBuiltInStrategiesInPipelineOrder()
{
- var strategies = FillStrategyRegistry.Strategies;
-
- Assert.True(
- strategies.Count >= 6,
- $"Expected at least 6 built-in strategies, got {strategies.Count}"
+ Assert.Equal(
+ ["Pairs", "Row", "Column", "RectBestFit", "Extents", "Linear", "TiltedRemnant"],
+ FillStrategyRegistry.AllStrategies.Select(s => s.Name)
+ );
+ Assert.Equal(
+ [100, 150, 160, 200, 300, 400, 500],
+ FillStrategyRegistry.AllStrategies.Select(s => s.Order)
);
- Assert.Contains(strategies, s => s.Name == "Pairs");
- Assert.Contains(strategies, s => s.Name == "RectBestFit");
- Assert.Contains(strategies, s => s.Name == "Extents");
- Assert.Contains(strategies, s => s.Name == "Linear");
- Assert.Contains(strategies, s => s.Name == "Row");
- Assert.Contains(strategies, s => s.Name == "Column");
}
[Fact]
diff --git a/OpenNest.WinForms.Tests/Forms/PlateViewFillLifetimeTests.cs b/OpenNest.WinForms.Tests/Forms/PlateViewFillLifetimeTests.cs
index 98e127a7..801fef5e 100644
--- a/OpenNest.WinForms.Tests/Forms/PlateViewFillLifetimeTests.cs
+++ b/OpenNest.WinForms.Tests/Forms/PlateViewFillLifetimeTests.cs
@@ -168,8 +168,8 @@ public class PlateViewFillLifetimeTests
});
}
- // Use the real strategy registry rather than a production-only injection hook.
- public sealed class BlockingFillStrategy : IFillStrategy
+ // PlateView builds its own filler, so the registry's list is swapped for the test's strategy.
+ private sealed class BlockingFillStrategy : IFillStrategy
{
public string Name => "WinForms lifetime regression";
public NestPhase Phase => NestPhase.Linear;
@@ -183,7 +183,7 @@ public class PlateViewFillLifetimeTests
private readonly ManualResetEventSlim entered = new();
private readonly ManualResetEventSlim release = new();
private readonly BlockingFillStrategy strategy;
- private readonly string[] priorStrategies;
+ private readonly IDisposable strategies;
private readonly string priorEngine;
private readonly SynchronizationContext? priorContext;
@@ -197,14 +197,10 @@ public class PlateViewFillLifetimeTests
public FillRun()
{
- priorStrategies = FillStrategyRegistry.Strategies.Select(s => s.Name).ToArray();
priorEngine = EngineSelection.EngineName;
- FillStrategyRegistry.LoadFrom(typeof(BlockingFillStrategy).Assembly);
- strategy = Assert.Single(FillStrategyRegistry.AllStrategies.OfType());
- FillStrategyRegistry.Disable(strategy.Name);
- FillStrategyRegistry.SetEnabled(strategy.Name);
+ strategy = new BlockingFillStrategy { Run = Fill };
+ strategies = FillStrategyRegistry.UseForTesting([strategy]);
EngineSelection.EngineName = EngineSelection.DefaultEngineName;
- strategy.Run = Fill;
var program = new CNC.Program();
program.Codes.Add(new RapidMove(0, 0));
@@ -260,7 +256,7 @@ public class PlateViewFillLifetimeTests
finally
{
strategy.Run = null;
- FillStrategyRegistry.SetEnabled(priorStrategies);
+ strategies.Dispose();
EngineSelection.EngineName = priorEngine;
SynchronizationContext.SetSynchronizationContext(priorContext);
View.Dispose();