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