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