Files
OpenNest/OpenNest.Engine/Jobs/Adapters/NestResultMaterializer.cs
T
ajandClaude Opus 5.5 b026bd19d2 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>
2026-11-05 19:38:00 -05:00

81 lines
3.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Engine.Jobs.Adapters;
/// <summary>A detached mutable domain nest plus explicit requirement identity (never inferred from names).</summary>
public sealed class MaterializedNestResult
{
internal MaterializedNestResult(Nest nest, Dictionary<string, Drawing> drawings)
{
Nest = nest;
DrawingsByPartId = new ReadOnlyDictionary<string, Drawing>(drawings);
}
public Nest Nest { get; }
public IReadOnlyDictionary<string, Drawing> DrawingsByPartId { get; }
}
/// <summary>Materializes a result from the same job. Geometry safety remains the solver's future validation boundary.</summary>
public static class NestResultMaterializer
{
public static MaterializedNestResult Materialize(NestJob job, NestJobResult result)
{
ArgumentNullException.ThrowIfNull(job);
ArgumentNullException.ThrowIfNull(result);
var nest = new Nest();
var drawings = job.Parts.ToDictionary(
p => p.Id,
DrawingJobMapper.CreateDrawing,
StringComparer.Ordinal
);
foreach (var drawing in drawings.Values)
nest.Drawings.Add(drawing);
foreach (var sheet in result.Plates)
{
var plate = DrawingJobMapper.CreatePlate(sheet.Stock);
foreach (var pose in sheet.Placements)
{
if (!drawings.TryGetValue(pose.PartId, out var drawing))
throw new ArgumentException(
"Result contains a requirement not present in the job.",
nameof(result)
);
// Do not use CreateAtOrigin: it normalizes bounds and would change the snapshot frame.
var part = new Part(drawing);
part.Rotate(pose.Rotation);
part.Location = new Vector(pose.X, pose.Y);
part.UpdateBounds();
// Quantity=1 is set before the only attachment; Plate's event owns Nested accounting.
plate.Parts.Add(part);
}
AddCommonLines(plate, plate.Parts.ToList(), sheet.CommonLines);
nest.Plates.Add(plate);
}
return new MaterializedNestResult(nest, drawings);
}
/// <summary>
/// Adds a sheet's explicit common-line groups to <paramref name="plate"/>; members are the
/// sheet's placements by position in <paramref name="parts"/>. A group naming a position
/// outside the sheet, a part twice, a part of an earlier group, or fewer than two parts throws:
/// such a result is malformed.
/// </summary>
internal static void AddCommonLines(Plate plate, IReadOnlyList<Part> parts,
IReadOnlyList<NestJobCommonLine> groups)
{
if (groups.Count == 0)
return;
var taken = new HashSet<int>();
foreach (var group in groups)
{
if (group.Placements.Count < 2 || group.Placements.Any(i => i < 0 || i >= parts.Count || !taken.Add(i)))
throw new ArgumentException("A common-line group names a placement that is missing, repeated or in another group.");
plate.CommonLines.Add(new CommonLineGroup(group.Placements.Select(i => parts[i]), group.Kerf));
}
}
}