#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using OpenNest.Engine.NestingEngines.Irregular; using OpenNest.Engine.NestingEngines.Rectangles; namespace OpenNest.Engine.Jobs; /// /// Registry of whole-job implementations. The four production /// strategies are exposed through . Callers choose an /// engine explicitly from ; there is no process-global active /// selection. Plug-ins loaded from an Engines/ folder register under their CLR type name. /// public static class NestingEngineRegistry { private static readonly List engines = new(); /// /// Registry names used by earlier releases, mapped to the engine that replaced them, so saved /// selections and scripts keep working. Consulted only when no engine has the requested name. /// private static readonly Dictionary RenamedEngines = new(StringComparer.OrdinalIgnoreCase) { ["Opus55NestingEngine"] = "Irregular", ["RectanglesNestingEngine"] = "Rectangles", }; static NestingEngineRegistry() { Register( "Rectangles", "Plain and near-rectangular parts: maximal-rectangles box packing", () => new RectanglesNestingEngine() ); Register( "Irregular", "Irregular parts: no-fit-polygon frontier packing with look-ahead stock selection", () => new IrregularNestingEngine() ); Register( "StockLadder", "Caller-stock constrained-first fill and equivalent-demand area repacking", () => new StockLadderNestingEngine() ); Register( "Default", "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)", () => new FixedStrategyNestingEngine("Default") ); Register( "Strip", "Strip-based nesting for mixed-drawing layouts", () => new FixedStrategyNestingEngine("Strip") ); Register( "Vertical Remnant", "Optimizes for largest right-side vertical drop", () => new FixedStrategyNestingEngine("Vertical Remnant") ); Register( "Horizontal Remnant", "Optimizes for largest top-side horizontal drop", () => new FixedStrategyNestingEngine("Horizontal Remnant") ); } public static IReadOnlyList AvailableEngines => engines; /// /// Registered name for : an exact (case-insensitive) match, else the /// engine a renamed legacy name now maps to, else null. Hosts use this to restore a saved /// selection made under an old name. /// public static string? ResolveName(string? name) { if (string.IsNullOrWhiteSpace(name)) return null; var trimmed = name.Trim(); var info = engines.FirstOrDefault(e => e.Name.Equals(trimmed, StringComparison.OrdinalIgnoreCase)); if (info != null) return info.Name; return RenamedEngines.TryGetValue(trimmed, out var renamed) && engines.FirstOrDefault(e => e.Name.Equals(renamed, StringComparison.OrdinalIgnoreCase)) is { } target ? target.Name : null; } /// /// Creates the engine registered under (case-insensitive, renamed legacy /// names accepted). The caller's explicit choice is the whole selection mechanism; unknown names throw. /// public static INestingEngine Create(string name) { ArgumentException.ThrowIfNullOrWhiteSpace(name); var resolved = ResolveName(name); var info = resolved == null ? null : engines.First(e => e.Name == resolved); if (info == null) throw new NotSupportedException( $"Unknown nesting engine: {name}. Available: {string.Join(", ", engines.Select(e => e.Name))}." ); return info.Factory(); } public static void Register(string name, string description, Func factory) { if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) { Debug.WriteLine($"[NestingEngineRegistry] Duplicate engine '{name}' skipped"); return; } // A leftover plug-in under a renamed engine's old name would shadow its built-in replacement. if (RenamedEngines.ContainsKey(name)) { Debug.WriteLine($"[NestingEngineRegistry] '{name}' skipped: replaced by built-in '{RenamedEngines[name]}'"); return; } engines.Add(new NestingEngineInfo(name, description, factory)); } /// Scans *.dll in directory for non-abstract INestingEngine types with a public /// parameterless constructor, registering each under its CLR type name. Per-assembly and per-type /// isolation ensures one bad plugin never prevents the rest from loading. 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(INestingEngine).IsAssignableFrom(type)) continue; var ctor = type.GetConstructor(Type.EmptyTypes); if (ctor == null) { Debug.WriteLine( $"[NestingEngineRegistry] Skipping {type.Name}: no parameterless constructor" ); continue; } try { Register(type.Name, string.Empty, () => (INestingEngine)ctor.Invoke(null)); Debug.WriteLine( $"[NestingEngineRegistry] Loaded plugin engine: {type.Name}" ); } catch (Exception ex) { Debug.WriteLine( $"[NestingEngineRegistry] Failed to register {type.Name}: {ex.Message}" ); } } } catch (Exception ex) { Debug.WriteLine( $"[NestingEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}" ); } } } }