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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-01 21:17:16 -05:00
1 parent 4b2aba83bb
commit 44372d129e
22 files changed
+185 -61

No files matched your search

+1 -1
View File
@@ -94,7 +94,7 @@ namespace OpenNest.Benchmark
parts,
stock,
new NestJobOptions(
"Fill",
"Lattice",
maxPlates,
salvageRate ?? SalvageRate,
minimumSalvageDimension ?? 0,
@@ -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"));
}
}
+23 -5
View File
@@ -64,10 +64,28 @@ namespace OpenNest.Forms
/// built-in strategy, otherwise Fill.</summary>
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)
);
}
/// <summary>The plain description shown beside an engine's name; empty for plug-ins.</summary>
public static string Description(string name) =>
NestingEngineRegistry.AvailableEngines
.FirstOrDefault(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
?.Description ?? string.Empty;
/// <summary>The description plus what Fill Area uses, for a tooltip on the selection.</summary>
public static string ToolTip(string name)
{
var description = Description(name);
return IsFillStrategy(name)
? description
: $"{description}\nFill Area uses {PlateFillService.DefaultStrategy} with this engine.".TrimStart();
}
}
}
+11 -11
View File
@@ -7,8 +7,8 @@ using OpenNest.Geometry;
namespace OpenNest.Engine.Tests;
/// <summary>
/// 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.
/// </summary>
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<FixedStrategyNestingEngine>(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<PlateNester>(PlateNesterFactory.Create("Fill"));
Assert.IsType<PlateNester>(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)
@@ -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);
@@ -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<NotSupportedException>(() => ((IList<NestJobPart>)job.Parts).Clear());
}
@@ -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]
@@ -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<FixedStrategyNestingEngine>(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);
@@ -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),
+1 -1
View File
@@ -22,7 +22,7 @@ public readonly record struct EngineNameClassification(EngineNameKind Kind, stri
/// <summary>
/// Classifies a name from a front end selector that accepts both single-plate fill strategies
/// (<see cref="PlateFillService"/>) and whole-job engines (<see cref="NestingEngineRegistry"/>).
/// 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
/// <see cref="PlateFillService.DefaultStrategy"/>. Front ends keep their own message wording.
/// </summary>
public static class EngineNameResolver
+1 -1
View File
@@ -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,
+12 -11
View File
@@ -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")
);
}
@@ -11,8 +11,8 @@ namespace OpenNest.Engine.Jobs.Placement;
/// </summary>
internal static class PlacementStrategies
{
/// <summary>Name of the Fill strategy in earlier releases; accepted as an alias.</summary>
private const string LegacyFillName = "Default";
/// <summary>Names of the Lattice strategy in earlier releases; accepted as aliases.</summary>
private static readonly string[] LegacyLatticeNames = ["Default", "Fill"];
/// <summary>One built-in strategy: its canonical name and the filler it runs.</summary>
internal sealed record Entry(string Name, PlateFillPolicy Policy, PlateLayout Layout)
@@ -23,7 +23,7 @@ internal static class PlacementStrategies
/// <summary>In <see cref="PlateFillService.BuiltInStrategies"/> order.</summary>
internal static IReadOnlyList<Entry> 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
/// <summary>
/// Finds the strategy <paramref name="name"/> names under <paramref name="comparison"/>;
/// "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.
/// </summary>
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))
@@ -18,12 +18,12 @@ namespace OpenNest.Engine.Jobs.Placement;
public static class PlateFillService
{
/// <summary>Strategy used when a caller selection names none.</summary>
public const string DefaultStrategy = "Fill";
public const string DefaultStrategy = "Lattice";
/// <summary>The four built-in strategy names, in registry display order.</summary>
public static IReadOnlyList<string> 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;
/// <summary>
/// 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 <see cref="NotSupportedException"/>.
/// </summary>
public static string ResolveStrategy(string strategy) => ResolveStrategy(strategy, true);
@@ -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);
}
+1 -1
View File
@@ -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.";
@@ -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]
+1
View File
@@ -89,6 +89,7 @@ namespace OpenNest.Forms
{
foreach (var name in EngineSelection.UiEngineNames)
engineComboBox.Items.Add(name);
EngineComboBoxDescriptions.Apply(engineComboBox);
engineComboBox.SelectedItem = EngineSelection.EngineName;
}
@@ -0,0 +1,47 @@
using System.Drawing;
using System.Windows.Forms;
namespace OpenNest.Forms
{
/// <summary>
/// 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.
/// </summary>
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();
}
}
}
+3
View File
@@ -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
+2
View File
@@ -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);
+30 -5
View File
@@ -16,7 +16,31 @@ in `OpenNest.Engine/NestingEngines/<Name>/`, 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