From 44372d129ee8e8d5d08df6f7718c4949948e7d18 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 1 Nov 2026 21:17:16 -0500 Subject: [PATCH] feat(ui): rename the Fill engine to Lattice and describe engines "Fill" said nothing about what the engine does, since every engine fills. It is now Lattice, and Fill stays an alias, so saved selections, scripts and API requests keep working. Engine descriptions are rewritten in plain language and shown beside each name in the desktop engine lists; the main toolbar's tooltip also says when Fill Area falls back to Lattice. Docs also cover the leftover placement from the previous commit. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Benchmark/BenchmarkJob.cs | 2 +- .../Forms/EngineSelectionTests.cs | 15 ++++++ OpenNest.Desktop/Forms/EngineSelection.cs | 28 +++++++++-- .../FillStrategyNameTests.cs | 22 ++++----- .../Jobs/EngineNameResolverTests.cs | 8 ++-- .../Jobs/NestJobRunnerTests.cs | 2 +- .../Jobs/NestPipelineTests.cs | 2 +- .../Jobs/NestingEngineRegistryTests.cs | 10 +++- .../Jobs/PlateNesterParityTests.cs | 6 +-- OpenNest.Engine/Jobs/EngineNameResolver.cs | 2 +- OpenNest.Engine/Jobs/NestJobOptions.cs | 2 +- OpenNest.Engine/Jobs/NestingEngineRegistry.cs | 23 ++++----- .../Jobs/Placement/PlacementStrategies.cs | 16 ++++--- .../Jobs/Placement/PlateFillService.cs | 10 ++-- .../DefaultEngineSelectionTests.cs | 4 +- OpenNest.Mcp/Tools/NestingTools.cs | 2 +- .../Engine/ExplicitPlacementStrategyTests.cs | 4 +- OpenNest/Forms/AutoNestForm.cs | 1 + OpenNest/Forms/EngineComboBoxDescriptions.cs | 47 +++++++++++++++++++ OpenNest/Forms/MainForm.Status.cs | 3 ++ OpenNest/Forms/MainForm.cs | 2 + docs/nesting-engines.md | 35 ++++++++++++-- 22 files changed, 185 insertions(+), 61 deletions(-) create mode 100644 OpenNest/Forms/EngineComboBoxDescriptions.cs diff --git a/OpenNest.Benchmark/BenchmarkJob.cs b/OpenNest.Benchmark/BenchmarkJob.cs index 164afdee..f8499d01 100644 --- a/OpenNest.Benchmark/BenchmarkJob.cs +++ b/OpenNest.Benchmark/BenchmarkJob.cs @@ -94,7 +94,7 @@ namespace OpenNest.Benchmark parts, stock, new NestJobOptions( - "Fill", + "Lattice", maxPlates, salvageRate ?? SalvageRate, minimumSalvageDimension ?? 0, diff --git a/OpenNest.Desktop.Tests/Forms/EngineSelectionTests.cs b/OpenNest.Desktop.Tests/Forms/EngineSelectionTests.cs index ef50deb9..adc1a465 100644 --- a/OpenNest.Desktop.Tests/Forms/EngineSelectionTests.cs +++ b/OpenNest.Desktop.Tests/Forms/EngineSelectionTests.cs @@ -25,4 +25,19 @@ public class EngineSelectionTests [InlineData(null)] public void JobsOnlyEnginesAndBlanks_AreNotFillStrategies(string? name) => Assert.False(EngineSelection.IsFillStrategy(name!)); + + [Fact] + public void EveryBuiltInEngineHasADescription() + { + foreach (var name in EngineSelection.UiEngineNames) + Assert.NotEmpty(EngineSelection.Description(name)); + } + + [Fact] + public void ToolTipSaysWhatFillAreaUsesForJobOnlyEngines() + { + Assert.DoesNotContain("Fill Area", EngineSelection.ToolTip("Vertical Remnant")); + Assert.Contains("Fill Area uses Lattice", EngineSelection.ToolTip("Irregular")); + } + } diff --git a/OpenNest.Desktop/Forms/EngineSelection.cs b/OpenNest.Desktop/Forms/EngineSelection.cs index db74349f..84df4972 100644 --- a/OpenNest.Desktop/Forms/EngineSelection.cs +++ b/OpenNest.Desktop/Forms/EngineSelection.cs @@ -64,10 +64,28 @@ namespace OpenNest.Forms /// built-in strategy, otherwise Fill. public static string FillStrategy => IsFillStrategy(engineName) ? engineName : PlateFillService.DefaultStrategy; - public static bool IsFillStrategy(string name) => - !string.IsNullOrWhiteSpace(name) - && PlateFillService.BuiltInStrategies.Any( - s => s.Equals(name.Trim(), StringComparison.OrdinalIgnoreCase) - ); + public static bool IsFillStrategy(string name) + { + var resolved = NestingEngineRegistry.ResolveName(name); + return resolved != null + && PlateFillService.BuiltInStrategies.Any( + s => s.Equals(resolved, StringComparison.OrdinalIgnoreCase) + ); + } + + /// The plain description shown beside an engine's name; empty for plug-ins. + public static string Description(string name) => + NestingEngineRegistry.AvailableEngines + .FirstOrDefault(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + ?.Description ?? string.Empty; + + /// The description plus what Fill Area uses, for a tooltip on the selection. + public static string ToolTip(string name) + { + var description = Description(name); + return IsFillStrategy(name) + ? description + : $"{description}\nFill Area uses {PlateFillService.DefaultStrategy} with this engine.".TrimStart(); + } } } diff --git a/OpenNest.Engine.Tests/FillStrategyNameTests.cs b/OpenNest.Engine.Tests/FillStrategyNameTests.cs index 786214ef..a65892d4 100644 --- a/OpenNest.Engine.Tests/FillStrategyNameTests.cs +++ b/OpenNest.Engine.Tests/FillStrategyNameTests.cs @@ -7,8 +7,8 @@ using OpenNest.Geometry; namespace OpenNest.Engine.Tests; /// -/// The multi-phase fill engine and placement strategy are named "Fill". "Default" names the -/// router engine; fill-strategy callers still read "Default" as Fill. +/// The multi-phase fill engine and placement strategy are named "Lattice". "Default" names the +/// router engine; fill-strategy callers still read "Default" as Lattice. /// public class FillStrategyNameTests { @@ -18,9 +18,9 @@ public class FillStrategyNameTests var engines = NestingEngineRegistry.AvailableEngines.Select(e => e.Name).ToList(); Assert.Equal("Default", engines[0]); - Assert.Contains("Fill", engines); + Assert.Contains("Lattice", engines); Assert.IsType(NestingEngineRegistry.Create("Fill")); - Assert.Equal(new[] { "Fill", "Strip", "Vertical Remnant", "Horizontal Remnant" }, + Assert.Equal(new[] { "Lattice", "Strip", "Vertical Remnant", "Horizontal Remnant" }, PlateFillService.BuiltInStrategies); } @@ -45,11 +45,11 @@ public class FillStrategyNameTests } [Theory] - [InlineData(null, "Fill")] - [InlineData("", "Fill")] - [InlineData("fill", "Fill")] - [InlineData("Default", "Fill")] - [InlineData("DEFAULT", "Fill")] + [InlineData(null, "Lattice")] + [InlineData("", "Lattice")] + [InlineData("fill", "Lattice")] + [InlineData("Default", "Lattice")] + [InlineData("DEFAULT", "Lattice")] public void PlacementStrategyResolvesFillAndItsLegacyName(string? strategy, string expected) { Assert.Equal(expected, PlateFillService.ResolveStrategy(strategy!)); @@ -90,7 +90,7 @@ public class FillStrategyNameTests new Plate(new Size(60, 80)), new NestItem(), new Box(0, 0, 1, 1), null, CancellationToken.None)); Assert.Equal("Unknown placement strategy: Mystery. Known strategies: " - + "Fill, Strip, Vertical Remnant, Horizontal Remnant.", error.Message); + + "Lattice, Strip, Vertical Remnant, Horizontal Remnant.", error.Message); } [Fact] @@ -98,7 +98,7 @@ public class FillStrategyNameTests { Assert.IsType(PlateNesterFactory.Create("Fill")); Assert.IsType(PlateNesterFactory.Create("Default")); - Assert.Equal("Fill", new NestJobOptions().PlacementStrategy); + Assert.Equal("Lattice", new NestJobOptions().PlacementStrategy); var job = FiniteStockJobTests.Job(1); var legacy = new NestJobRunner(PlateNesterFactory.Create) diff --git a/OpenNest.Engine.Tests/Jobs/EngineNameResolverTests.cs b/OpenNest.Engine.Tests/Jobs/EngineNameResolverTests.cs index 6c57c7e9..df6bc646 100644 --- a/OpenNest.Engine.Tests/Jobs/EngineNameResolverTests.cs +++ b/OpenNest.Engine.Tests/Jobs/EngineNameResolverTests.cs @@ -6,11 +6,11 @@ namespace OpenNest.Engine.Tests.Jobs; public class EngineNameResolverTests { [Theory] - [InlineData("Fill", "Fill")] + [InlineData("Fill", "Lattice")] [InlineData(" strip ", "Strip")] - [InlineData("Default", "Fill")] - [InlineData(null, "Fill")] - [InlineData(" ", "Fill")] + [InlineData("Default", "Lattice")] + [InlineData(null, "Lattice")] + [InlineData(" ", "Lattice")] public void FillStrategiesWinAndAreCanonicalized(string name, string expected) { var result = EngineNameResolver.Classify(name); diff --git a/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs index 95551dad..1986b42e 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs @@ -105,7 +105,7 @@ public class NestJobRunnerTests Assert.Equal(100, job.Plates[0].Size.Width); Assert.Equal(1, job.Plates[0].EdgeSpacing.Left); Assert.Equal(0, job.Plates[0].Quantity); - Assert.Equal("Fill", job.Options.PlacementStrategy); + Assert.Equal("Lattice", job.Options.PlacementStrategy); Assert.Throws(() => ((IList)job.Parts).Clear()); } diff --git a/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs b/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs index 1f88c64e..7feb0c68 100644 --- a/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestPipelineTests.cs @@ -282,7 +282,7 @@ public class NestPipelineTests NestPipeline.Run(Request("Mystery Engine", Item("bracket", 1))) ); - Assert.Contains("Fill", error.Message); + Assert.Contains("Lattice", error.Message); } [Fact] diff --git a/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs b/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs index 44ef56b1..4487be03 100644 --- a/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs @@ -6,6 +6,14 @@ namespace OpenNest.Engine.Tests.Jobs; public class NestingEngineRegistryTests { + [Fact] + public void TheFormerFillNameStillSelectsLattice() + { + Assert.Equal("Lattice", NestingEngineRegistry.ResolveName("Fill")); + Assert.Equal("Lattice", NestingEngineRegistry.ResolveName("fill")); + Assert.IsType(NestingEngineRegistry.Create("Fill")); + } + [Fact] public void BuiltInStrategiesAreRegistered() { @@ -14,7 +22,7 @@ public class NestingEngineRegistryTests Assert.Contains("Rectangles", names); Assert.Contains("Irregular", names); Assert.Contains("Circles", names); - Assert.Contains("Fill", names); + Assert.Contains("Lattice", names); Assert.Contains("Strip", names); Assert.Contains("Vertical Remnant", names); Assert.Contains("Horizontal Remnant", names); diff --git a/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs b/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs index 21b63a4e..ca614ffb 100644 --- a/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs +++ b/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs @@ -106,8 +106,8 @@ public class PlateNesterParityTests } [Theory] - [InlineData("Fill", "Fill")] - [InlineData("Default", "Fill")] + [InlineData("Fill", "Lattice")] + [InlineData("Default", "Lattice")] [InlineData("Strip", "Strip")] [InlineData("Vertical Remnant", "Vertical Remnant")] [InlineData("Horizontal Remnant", "Horizontal Remnant")] @@ -119,7 +119,7 @@ public class PlateNesterParityTests Assert.Equal(canonical, entry.Name); var (policy, layout) = canonical switch { - "Fill" => (PlateFillPolicy.Fill, PlateLayout.FillThenPack), + "Lattice" => (PlateFillPolicy.Fill, PlateLayout.FillThenPack), "Strip" => (PlateFillPolicy.Fill, PlateLayout.Strips), "Vertical Remnant" => (PlateFillPolicy.VerticalRemnant, PlateLayout.FillThenPack), _ => (PlateFillPolicy.HorizontalRemnant, PlateLayout.FillThenPack), diff --git a/OpenNest.Engine/Jobs/EngineNameResolver.cs b/OpenNest.Engine/Jobs/EngineNameResolver.cs index 93191735..571d6b4d 100644 --- a/OpenNest.Engine/Jobs/EngineNameResolver.cs +++ b/OpenNest.Engine/Jobs/EngineNameResolver.cs @@ -22,7 +22,7 @@ public readonly record struct EngineNameClassification(EngineNameKind Kind, stri /// /// Classifies a name from a front end selector that accepts both single-plate fill strategies /// () and whole-job engines (). -/// Fill strategies win, so "Fill" and "Strip" resolve as strategies; an omitted name means +/// Fill strategies win, so "Lattice" and "Strip" resolve as strategies; an omitted name means /// . Front ends keep their own message wording. /// public static class EngineNameResolver diff --git a/OpenNest.Engine/Jobs/NestJobOptions.cs b/OpenNest.Engine/Jobs/NestJobOptions.cs index 50c00261..f6d73120 100644 --- a/OpenNest.Engine/Jobs/NestJobOptions.cs +++ b/OpenNest.Engine/Jobs/NestJobOptions.cs @@ -7,7 +7,7 @@ namespace OpenNest.Engine.Jobs; public sealed class NestJobOptions { public NestJobOptions( - string placementStrategy = "Fill", + string placementStrategy = "Lattice", int? maxPlates = null, double salvageRate = 0, double minimumSalvageDimension = 0, diff --git a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs index 547342eb..fb4cb5fa 100644 --- a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs +++ b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs @@ -34,6 +34,7 @@ public static class NestingEngineRegistry ["RectanglesNestingEngine"] = "Rectangles", // Retired: Default chooses stock at least as well on every benchmark job. ["StockLadder"] = "Default", + ["Fill"] = "Lattice", }; static NestingEngineRegistry() @@ -41,55 +42,55 @@ public static class 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", + "Any job: tries Rectangles, Circles and Irregular and keeps the best", () => new DefaultNestingEngine() ); Register( "Rectangles", - "Plain and near-rectangular parts: maximal-rectangles box packing", + "Packs each part as a box; best for plain rectangular parts", () => new RectanglesNestingEngine() ); Register( "Irregular", - "Irregular parts: no-fit-polygon frontier packing with look-ahead stock selection", + "Fits irregular shapes into each other's gaps and notches", () => new IrregularNestingEngine() ); Register( "Circles", - "Round parts (blanks, washers, flanges): disc packing by enclosing circle", + "Round parts such as blanks, washers and flanges", () => new CirclesNestingEngine() ); Register( "FlatBar", - "Flat bar: parts cut in one row along each bar with the saw cut-list packer", + "One row of parts along a flat bar", () => new FlatBarNestingEngine() ); Register( - "Fill", - "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)", - () => new FixedStrategyNestingEngine("Fill") + "Lattice", + "Repeats parts in tight rows and pairs; best for many copies of a few parts", + () => new FixedStrategyNestingEngine("Lattice") ); Register( "Strip", - "Strip-based nesting for mixed-drawing layouts", + "Fills one strip at a time, part by part; each uses the space the last one left", () => new FixedStrategyNestingEngine("Strip") ); Register( "Vertical Remnant", - "Optimizes for largest right-side vertical drop", + "Repeats parts in rows and pairs, keeping the largest drop on the right side", () => new FixedStrategyNestingEngine("Vertical Remnant") ); Register( "Horizontal Remnant", - "Optimizes for largest top-side horizontal drop", + "Repeats parts in rows and pairs, keeping the largest drop on the top", () => new FixedStrategyNestingEngine("Horizontal Remnant") ); } diff --git a/OpenNest.Engine/Jobs/Placement/PlacementStrategies.cs b/OpenNest.Engine/Jobs/Placement/PlacementStrategies.cs index ac0cb5e4..8dbf2937 100644 --- a/OpenNest.Engine/Jobs/Placement/PlacementStrategies.cs +++ b/OpenNest.Engine/Jobs/Placement/PlacementStrategies.cs @@ -11,8 +11,8 @@ namespace OpenNest.Engine.Jobs.Placement; /// internal static class PlacementStrategies { - /// Name of the Fill strategy in earlier releases; accepted as an alias. - private const string LegacyFillName = "Default"; + /// Names of the Lattice strategy in earlier releases; accepted as aliases. + private static readonly string[] LegacyLatticeNames = ["Default", "Fill"]; /// One built-in strategy: its canonical name and the filler it runs. internal sealed record Entry(string Name, PlateFillPolicy Policy, PlateLayout Layout) @@ -23,7 +23,7 @@ internal static class PlacementStrategies /// In order. internal static IReadOnlyList Entries { get; } = [ - new("Fill", PlateFillPolicy.Fill, PlateLayout.FillThenPack), + new("Lattice", PlateFillPolicy.Fill, PlateLayout.FillThenPack), new("Strip", PlateFillPolicy.Fill, PlateLayout.Strips), new("Vertical Remnant", PlateFillPolicy.VerticalRemnant, PlateLayout.FillThenPack), new("Horizontal Remnant", PlateFillPolicy.HorizontalRemnant, PlateLayout.FillThenPack), @@ -31,14 +31,18 @@ internal static class PlacementStrategies /// /// Finds the strategy names under ; - /// "Default", the Fill strategy's earlier name, finds Fill. Null when none matches. + /// "Default" and "Fill", the Lattice strategy's earlier names, find Lattice. Null when none + /// matches. /// internal static Entry Find(string name, StringComparison comparison) { if (name == null) return null; - if (name.Equals(LegacyFillName, comparison)) - return Entries[0]; + foreach (var legacy in LegacyLatticeNames) + { + if (name.Equals(legacy, comparison)) + return Entries[0]; + } foreach (var entry in Entries) { if (entry.Name.Equals(name, comparison)) diff --git a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs index 6eb60785..df1195ce 100644 --- a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs +++ b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs @@ -18,12 +18,12 @@ namespace OpenNest.Engine.Jobs.Placement; public static class PlateFillService { /// Strategy used when a caller selection names none. - public const string DefaultStrategy = "Fill"; + public const string DefaultStrategy = "Lattice"; /// The four built-in strategy names, in registry display order. public static IReadOnlyList BuiltInStrategies { get; } = [ - "Fill", + "Lattice", "Strip", "Vertical Remnant", "Horizontal Remnant", @@ -403,10 +403,10 @@ public static class PlateFillService && a.Bottom < b.Top && a.Top > b.Bottom; /// - /// Resolves a caller-supplied strategy name: null or empty means "Fill"; otherwise the name + /// Resolves a caller-supplied strategy name: null or empty means "Lattice"; otherwise the name /// must match a built-in strategy, matched case-insensitively like the legacy registry's - /// ActiveEngineName so tolerant interactive callers keep working. "Default", the Fill - /// strategy's name in earlier releases, still resolves to "Fill". Returns the canonical name; + /// ActiveEngineName so tolerant interactive callers keep working. "Default" and "Fill", the + /// strategy's names in earlier releases, still resolve to "Lattice". Returns the canonical name; /// unknown names throw . /// public static string ResolveStrategy(string strategy) => ResolveStrategy(strategy, true); diff --git a/OpenNest.FrontEnd.Tests/DefaultEngineSelectionTests.cs b/OpenNest.FrontEnd.Tests/DefaultEngineSelectionTests.cs index 9dc11e00..60e0a9d9 100644 --- a/OpenNest.FrontEnd.Tests/DefaultEngineSelectionTests.cs +++ b/OpenNest.FrontEnd.Tests/DefaultEngineSelectionTests.cs @@ -49,14 +49,14 @@ public class DefaultEngineSelectionTests : IDisposable } [Fact] - public void McpFillToolsWithoutAnEngineUseFill() + public void McpFillToolsWithoutAnEngineUseLattice() { var session = Session(); session.DefaultEngineName = "Rectangles"; // The session default is for autonest only. var response = new NestingTools(session).FillPlate(0, "square", 2); - Assert.Contains("(Fill): success", response); + Assert.Contains("(Lattice): success", response); Assert.Equal(2, session.GetPlate(0).Parts.Count); } diff --git a/OpenNest.Mcp/Tools/NestingTools.cs b/OpenNest.Mcp/Tools/NestingTools.cs index d2e2ceea..65547a6b 100644 --- a/OpenNest.Mcp/Tools/NestingTools.cs +++ b/OpenNest.Mcp/Tools/NestingTools.cs @@ -31,7 +31,7 @@ namespace OpenNest.Mcp.Tools + "Rectangles: plain and near-rectangular parts; packs each part as its bounding box, fastest. " + "Circles: round parts (blanks, washers, flanges); packs each part as its enclosing circle; places nothing when any part is not round. " + "FlatBar: flat bar, parts about as wide as the bar; each stock cut as one row of parts along its length, shaped ends nested together; not run by Default. " - + "Fill: many copies of few drawings; lattice fill with pairs and rectangle best-fit. " + + "Lattice: many copies of few drawings; lattice fill with pairs and rectangle best-fit. " + "Strip: Fill variant laying mixed drawings in strips. " + "Vertical Remnant, Horizontal Remnant: Fill variants keeping a right-side or top-side drop. " + "Plug-ins loaded from Engines/ use their class name."; diff --git a/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs b/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs index c2126b90..2745a4d1 100644 --- a/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs +++ b/OpenNest.Tests/Engine/ExplicitPlacementStrategyTests.cs @@ -15,9 +15,9 @@ public class ExplicitPlacementStrategyTests [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void ResolveStrategy_UnsetMeansFill(string strategy) + public void ResolveStrategy_UnsetMeansLattice(string strategy) { - Assert.Equal("Fill", PlateFillService.ResolveStrategy(strategy)); + Assert.Equal("Lattice", PlateFillService.ResolveStrategy(strategy)); } [Theory] diff --git a/OpenNest/Forms/AutoNestForm.cs b/OpenNest/Forms/AutoNestForm.cs index 23734e43..fcb28c5d 100644 --- a/OpenNest/Forms/AutoNestForm.cs +++ b/OpenNest/Forms/AutoNestForm.cs @@ -89,6 +89,7 @@ namespace OpenNest.Forms { foreach (var name in EngineSelection.UiEngineNames) engineComboBox.Items.Add(name); + EngineComboBoxDescriptions.Apply(engineComboBox); engineComboBox.SelectedItem = EngineSelection.EngineName; } diff --git a/OpenNest/Forms/EngineComboBoxDescriptions.cs b/OpenNest/Forms/EngineComboBoxDescriptions.cs new file mode 100644 index 00000000..3d547bc4 --- /dev/null +++ b/OpenNest/Forms/EngineComboBoxDescriptions.cs @@ -0,0 +1,47 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace OpenNest.Forms +{ + /// + /// Shows each engine's description beside its name in the open list of an engine combo box. + /// Items stay plain engine names, so selection code is unchanged; the closed box shows the + /// name alone. + /// + internal static class EngineComboBoxDescriptions + { + private const int DropDownWidth = 620; + + internal static void Apply(ComboBox comboBox) + { + comboBox.DrawMode = DrawMode.OwnerDrawFixed; + comboBox.DropDownWidth = DropDownWidth; + comboBox.DrawItem += DrawItem; + } + + private static void DrawItem(object sender, DrawItemEventArgs e) + { + e.DrawBackground(); + + if (e.Index >= 0 && sender is ComboBox comboBox) + { + var name = comboBox.Items[e.Index] as string ?? string.Empty; + var flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter + | TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis; + TextRenderer.DrawText(e.Graphics, name, e.Font, e.Bounds, e.ForeColor, flags); + + var inList = (e.State & DrawItemState.ComboBoxEdit) == 0; + if (inList) + { + const int nameColumn = 140; + var bounds = Rectangle.FromLTRB(e.Bounds.Left + nameColumn, e.Bounds.Top, e.Bounds.Right, e.Bounds.Bottom); + var selected = (e.State & DrawItemState.Selected) != 0; + TextRenderer.DrawText(e.Graphics, EngineSelection.Description(name), e.Font, bounds, + selected ? e.ForeColor : SystemColors.GrayText, flags); + } + } + + e.DrawFocusRectangle(); + } + } +} diff --git a/OpenNest/Forms/MainForm.Status.cs b/OpenNest/Forms/MainForm.Status.cs index aa67039e..ef7476ac 100644 --- a/OpenNest/Forms/MainForm.Status.cs +++ b/OpenNest/Forms/MainForm.Status.cs @@ -86,7 +86,10 @@ namespace OpenNest.Forms private void EngineComboBox_SelectedIndexChanged(object sender, EventArgs e) { if (engineComboBox.SelectedItem is string name) + { EngineSelection.EngineName = name; + engineComboBox.ToolTipText = EngineSelection.ToolTip(name); + } } #region Statusbar Events diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 1422cf1e..e145aec8 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -165,7 +165,9 @@ namespace OpenNest.Forms foreach (var name in EngineSelection.UiEngineNames) engineComboBox.Items.Add(name); + EngineComboBoxDescriptions.Apply(engineComboBox.ComboBox); engineComboBox.SelectedItem = EngineSelection.EngineName; + engineComboBox.ToolTipText = EngineSelection.ToolTip(EngineSelection.EngineName); engineComboBox.SelectedIndexChanged += EngineComboBox_SelectedIndexChanged; updaterStatus = new UpdaterStatus(this, statusStrip1, statusLabel1.Font); diff --git a/docs/nesting-engines.md b/docs/nesting-engines.md index 9042b439..d78bef14 100644 --- a/docs/nesting-engines.md +++ b/docs/nesting-engines.md @@ -16,7 +16,31 @@ in `OpenNest.Engine/NestingEngines//`, its tests in `OpenNest.Engine.Tests | Circles | Round parts: blanks, washers, flanges, gears by their tip circle | Each part packed as its minimum enclosing circle at centers tangent to two of walls, placed discs and clamp keep-outs, with hexagonal and square lattice seeds; bids only when every part is round; stock chosen as Rectangles does, plus whole-job runs preferring each stock and a tail re-plan | | Irregular | Irregular profiles | No-fit-polygon frontier packing with gap filling and best-fit pairs, six whole-job strategy variants and a tail re-plan; under a time limit it keeps trying seeded variants until the limit | | FlatBar | Flat bar: parts about as wide as the bar | Each stock read as a bar and cut in one row: the CutList cut-list packer and a best-fill search on part lengths less their self-nesting gain, then a slide-left pass that nests shaped ends and refills freed length; stock chosen bar by bar by whole-plan cost | -| Fill, Strip, Vertical Remnant, Horizontal Remnant | Single-strategy fills | The fixed placement strategies behind interactive fill; Fill is the multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder). In whole jobs, all four place a plate with any fixed or bounded part rotation by the rotation-aware ordered placer instead | +| Lattice, Strip, Vertical Remnant, Horizontal Remnant | Many copies of a few parts | The fixed placement strategies behind interactive fill; Lattice is the multi-phase lattice fill (linear, pairs, rectangle best-fit, remainder), and the remnant strategies run it ranked to keep the largest drop. In whole jobs, all four place a plate with any fixed or bounded part rotation by the rotation-aware ordered placer instead | + +Each engine's plain description (`NestingEngineInfo.Description`) is shown beside its name in the +desktop engine lists. The main-window selection also picks Fill Area's strategy; a selection that +is not one of these four strategies leaves Fill Area on Lattice, and its tooltip says so. + +### Leftovers after a fill + +The four strategies fill the high-demand parts first, then place the leftovers in two orders and +keep whichever the strategy's own comparer ranks better as a whole layout; a tie keeps pack-first. +Neither order wins every job: pairs reach the gaps a fill leaves before singles take them, but plain +rectangles often pack tighter one at a time. + +- Pack first packs everything but quantity-two items into the largest remnant, then places those as + best-fit pairs. +- Pairs first, tried when any leftover has an even quantity, places each even quantity as pairs, + largest bounding box first, so a part that fits a gap reaches it before a smaller one takes it, + then packs the rest into the largest remnant left. + +A pair may also turn a quarter when its part has no rotation limits, is tried in every remnant, and +is ranked with the layout around it, since a pair's own extent cannot show whether it lengthens the +layout. The chosen pair then slides left and down against the true outlines around it +(`CompactPair`), because remnants are boxes and the closing whole-layout settle moves everything as +one group. Trimming a fill to quantity keeps its interlocked pairs whole, so the spare space stays +one usable cell rather than two half gaps. Neither Irregular nor Rectangles wins every job, even within its own lane, so Default runs both rather than choosing by part shape; Rectangles adds little time and is also the fallback when an @@ -434,11 +458,12 @@ desktop selections, scripts and API requests keep working: | `Opus55NestingEngine` | Irregular | | `RectanglesNestingEngine` | Rectangles | | `StockLadder` | Default | +| `Fill` | Lattice | -In v0.3.0 and earlier, `Default` was the multi-phase fill engine now named Fill. `Default` now names the -choosing engine above, so saved selections, scripts and API requests that name it get that -engine. Fill-strategy calls (interactive fill, `PlateFillService`, console fill without -`--autonest`, MCP fill tools) still read `Default` as Fill. +In v0.3.0 and earlier, `Default` was the multi-phase fill engine now named Lattice. `Default` now +names the choosing engine above, so saved selections, scripts and API requests that name it get +that engine. Fill-strategy calls (interactive fill, `PlateFillService`, console fill without +`--autonest`, MCP fill tools) still read `Default` and `Fill` as Lattice. StockLadder, a greedy constrained-first engine kept out of the desktop list, was retired because Default matched or beat it on every benchmark job, with and without salvage credit and