feat(common-line): place parts on common lines automatically at a chosen level

Slice 3 of common-line cutting. On costly plate (N26-295: 3/16 RA310S at about
$2,500 a sheet) pairing parts one kerf apart fits the part the spacing cannot:
the last plate's 38 parts fit on one 48x120 with Auto Nest only with pairs.

- Drawing Properties gains "Allow common line" (Drawing.AllowCommonLine, off by
  default because customer edge-quality needs vary). The nest's CommonLineUsage
  (Off / When it adds parts / Whenever available) is set in Auto Nest and seeded
  from Tools > Nest Defaults; both persist (nest.json, defaults.json).
- NestPipeline runs the ordinary solve, then, with a level on, trials where
  eligible parts are offered as kerf-gap pair compounds. Placed pairs expand into
  parts plus explicit NestJobCommonLine groups, are validated again, and
  CommonLineRanking keeps a trial only by the level: count first, then sheets;
  "when it adds parts" keeps the plain layout on a tie, "whenever" also takes a
  shorter cut or less material, never at the cost of a part. Engines, FillScore
  and their comparers are unchanged; Off asks the engine exactly what it did.
- NestLayoutCheck exempts members only through a sheet's explicit group list and
  only when the job allows common lines; materializing and committing create
  Plate.CommonLines.
- Server nesting writes common-line jobs as version 3 and replays the recorded
  trial answers, so the desktop binds the same groups.
- Fill Area honors the level through a PlateFillService overload.
- The benchmark gains --common-lines <level> --kerf <k>.

Benchmark (synthetic nests + N26-295, --parallel 1): with no flag and with
--common-lines off every engine matches the base commit; with a level on no
layout becomes invalid and three runs save a sheet at equal nested area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-05 19:38:00 -05:00
1 parent f9b2713420
commit b026bd19d2
61 files changed
+2155 -118

No files matched your search

@@ -76,6 +76,8 @@ public sealed class NestSolveTranscript
PlateIndex = p.PlateIndex,
Stock = NestStockJson.From(p.Stock),
Placements = p.Placements.ToList(),
CommonLines = p.CommonLines.Count == 0 ? null
: p.CommonLines.Select(g => new CommonLineJson { Kerf = g.Kerf, Placements = g.Placements.ToList() }).ToList(),
}).ToList(),
Fulfillment = result.Fulfillment.ToList(),
StockUsage = result.StockUsage.ToList(),
@@ -90,7 +92,9 @@ public sealed class NestSolveTranscript
{
var stock = plate.Stock?.ToStock() ?? throw new FormatException("A result sheet has no stock.");
var own = job.Plates.FirstOrDefault(s => RepeatedPlateGrouping.SameStock(s, stock));
plates.Add(new NestJobPlateResult(plate.PlateIndex, own ?? stock, plate.Placements ?? new()));
plates.Add(new NestJobPlateResult(plate.PlateIndex, own ?? stock, plate.Placements ?? new(),
plate.CommonLines?.Select(g => g == null ? throw new FormatException("A result common-line group is empty.")
: new NestJobCommonLine(g.Kerf, g.Placements ?? new()))));
}
return new NestJobResult(Status, StopReason, plates, Fulfillment ?? new(), StockUsage ?? new());
}
@@ -101,5 +105,16 @@ public sealed class NestSolveTranscript
public int PlateIndex { get; set; }
public NestStockJson Stock { get; set; }
public List<NestJobPlacement> Placements { get; set; } = new();
/// <summary>Common-line groups an engine returned itself (none of the built-in engines
/// do: the pipeline makes pairs around them). Omitted when empty.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<CommonLineJson> CommonLines { get; set; }
}
public sealed class CommonLineJson
{
public double Kerf { get; set; }
public List<int> Placements { get; set; } = new();
}
}