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();