mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 12:41:06 -04:00
Circles packs every part as its minimum enclosing circle at centers tangent to two of the work-area walls, placed discs and clamp keep-outs, bottom-left and left-bottom, with hexagonal and square lattice seeds. It bids only when every part fills at least 0.8 of its enclosing circle and otherwise returns everything unplaced at once. Default runs it as a third candidate. Rectangles' sheet-by-sheet stock loop moves to NestingEngines/SheetByStock (Rectangles output unchanged) and Default's candidate scoring and commit forwarding become shared helpers. Circles also runs the whole job preferring each offered stock and keeps the best result. Contract tests take shapes an engine can override. Synthetic round jobs, salvage-credited: Circles wins or ties all six with valid layouts; Default cost falls about 4% versus Irregular alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
7.6 KiB
C#
209 lines
7.6 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using OpenNest.Engine.NestingEngines.Circles;
|
|
using OpenNest.Engine.NestingEngines.Default;
|
|
using OpenNest.Engine.NestingEngines.FlatBar;
|
|
using OpenNest.Engine.NestingEngines.Irregular;
|
|
using OpenNest.Engine.NestingEngines.Rectangles;
|
|
|
|
namespace OpenNest.Engine.Jobs;
|
|
|
|
/// <summary>
|
|
/// Registry of whole-job <see cref="INestingEngine"/> implementations. "Default" chooses among the
|
|
/// built-in engines per job; the four fill strategies are exposed through
|
|
/// <see cref="FixedStrategyNestingEngine"/>. Callers choose an
|
|
/// engine explicitly from <see cref="AvailableEngines"/>; there is no process-global active
|
|
/// selection. Plug-ins loaded from an Engines/ folder register under their CLR type name.
|
|
/// </summary>
|
|
public static class NestingEngineRegistry
|
|
{
|
|
private static readonly List<NestingEngineInfo> engines = new();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private static readonly Dictionary<string, string> RenamedEngines = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["Opus55NestingEngine"] = "Irregular",
|
|
["RectanglesNestingEngine"] = "Rectangles",
|
|
};
|
|
|
|
static NestingEngineRegistry()
|
|
{
|
|
// Listed first: the engine front ends use when the caller names none.
|
|
Register(
|
|
"Default",
|
|
"Any job: runs Irregular, Rectangles and Circles and keeps the cheapest valid layout",
|
|
() => new DefaultNestingEngine()
|
|
);
|
|
|
|
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(
|
|
"Circles",
|
|
"Round parts (blanks, washers, flanges): disc packing by enclosing circle",
|
|
() => new CirclesNestingEngine()
|
|
);
|
|
|
|
Register(
|
|
"FlatBar",
|
|
"Flat bar: parts cut in one row along each bar with the saw cut-list packer",
|
|
() => new FlatBarNestingEngine()
|
|
);
|
|
|
|
Register(
|
|
"StockLadder",
|
|
"Caller-stock constrained-first fill and equivalent-demand area repacking",
|
|
() => new StockLadderNestingEngine()
|
|
);
|
|
|
|
Register(
|
|
"Fill",
|
|
"Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
|
|
() => new FixedStrategyNestingEngine("Fill")
|
|
);
|
|
|
|
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<NestingEngineInfo> AvailableEngines => engines;
|
|
|
|
/// <summary>
|
|
/// Registered name for <paramref name="name"/>: 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the engine registered under <paramref name="name"/> (case-insensitive, renamed legacy
|
|
/// names accepted). The caller's explicit choice is the whole selection mechanism; unknown names throw.
|
|
/// </summary>
|
|
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<INestingEngine> 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));
|
|
}
|
|
|
|
/// <summary>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.</summary>
|
|
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}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|