Files
OpenNest/OpenNest.Engine/Jobs/Placement/PlateFillService.CommonLines.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

117 lines
4.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Engine.Jobs.CommonLines;
using OpenNest.Geometry;
namespace OpenNest.Engine.Jobs.Placement;
/// <summary>A proposed fill: new parts and the common-line groups among them (empty when none).</summary>
public sealed record PlateFillProposal(List<Part> Parts, IReadOnlyList<CommonLineGroup> CommonLines);
public static partial class PlateFillService
{
/// <summary>
/// <see cref="FillItem(string, Plate, NestItem, Box, IProgress{NestProgress}, CancellationToken)"/>
/// at a common-line usage level. With the level off, a drawing that does not allow common lines,
/// or no positive kerf, it is exactly that fill. Otherwise the area is also filled with kerf-gap
/// pairs of the drawing (<see cref="CommonLinePair"/>), each expanded into its two parts and a
/// group, and the pair fill replaces the plain one when it places more parts, or (at
/// <see cref="CommonLineUsage.Whenever"/>) the same number and the strategy's own ranking does
/// not prefer the plain fill. Ties at <see cref="CommonLineUsage.WhenItAddsParts"/> keep the
/// plain fill. The proposal is not committed: the caller adds the parts, then the groups.
/// </summary>
public static PlateFillProposal FillItem(
string strategy,
Plate plate,
NestItem item,
Box workArea,
CommonLineUsage usage,
double kerf,
IProgress<NestProgress> progress,
CancellationToken token
)
{
ArgumentNullException.ThrowIfNull(item);
var plain = FillItem(strategy, plate, item, workArea, progress, token);
var none = new PlateFillProposal(plain, Array.Empty<CommonLineGroup>());
if (usage == CommonLineUsage.Off || item.Drawing?.AllowCommonLine != true
|| !double.IsFinite(kerf) || kerf <= 0 || !(kerf < plate.PartSpacing)
|| item.Quantity is > 0 and < 2 || token.IsCancellationRequested)
return none;
var constraints = item.Drawing.Constraints;
// The second copy turns half a turn, so a fixed rotation must allow its 180° equivalent.
if (item.StepAngle > 0 && constraints?.Allow180Equivalent != true)
return none;
CommonLinePair pair;
try
{
pair = CommonLinePair.Build(PartGeometrySnapshot.FromProgram(item.Drawing.Program), kerf);
}
catch (NotSupportedException)
{
return none;
}
if (pair == null)
return none;
var proxy = new Drawing(item.Drawing.Name, DrawingJobMapper.ToProgram(pair.Geometry))
{
Priority = item.Drawing.Priority,
Constraints = constraints,
};
var proxyItem = new NestItem
{
Drawing = proxy,
Priority = item.Priority,
Quantity = item.Quantity > 0 ? item.Quantity / 2 : item.Quantity,
StepAngle = item.StepAngle,
RotationStart = item.RotationStart,
RotationEnd = item.RotationEnd,
};
// Previews show the plain fill; the pair fill is compared once both are done.
var pairs = FillItem(strategy, plate, proxyItem, workArea, null, token);
if (pairs.Count == 0 || token.IsCancellationRequested)
return none;
var parts = new List<Part>(2 * pairs.Count);
var groups = new List<CommonLineGroup>(pairs.Count);
foreach (var placed in pairs)
{
var rotation = placed.Rotation;
var first = Place(item.Drawing, rotation, placed.Location);
var sine = System.Math.Sin(rotation);
var cosine = System.Math.Cos(rotation);
var second = Place(item.Drawing, rotation + CommonLinePair.SecondRotation,
new Vector(placed.Location.X + pair.SecondX * cosine - pair.SecondY * sine,
placed.Location.Y + pair.SecondX * sine + pair.SecondY * cosine));
parts.Add(first);
parts.Add(second);
groups.Add(new CommonLineGroup(new[] { first, second }, kerf));
}
if (parts.Count < plain.Count)
return none;
if (parts.Count == plain.Count)
{
if (usage != CommonLineUsage.Whenever
|| RequireFiller(strategy, plate).IsBetterFill(plain, parts, workArea))
return none;
}
return new PlateFillProposal(parts, groups);
}
// The proxy's frame is the drawing's own: rotate about the program origin, then move.
private static Part Place(Drawing drawing, double rotation, Vector location)
{
var part = new Part(drawing);
part.Rotate(rotation);
part.Location = location;
part.UpdateBounds();
return part;
}
}