Files
OpenNest/OpenNest.Engine/Jobs/NestJobOptions.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

98 lines
4.4 KiB
C#

using System;
using OpenNest;
namespace OpenNest.Engine.Jobs;
/// <summary>Immutable per-job options; selection never changes the legacy global registry.</summary>
public sealed class NestJobOptions
{
public NestJobOptions(
string placementStrategy = "Lattice",
int? maxPlates = null,
double salvageRate = 0,
double minimumSalvageDimension = 0,
bool searchUntilCancelled = false,
CutOffAxis? offcutCut = null
)
{
if (offcutCut is { } axis && !Enum.IsDefined(axis))
throw new ArgumentOutOfRangeException(nameof(offcutCut));
ArgumentException.ThrowIfNullOrWhiteSpace(placementStrategy);
if (maxPlates <= 0)
throw new ArgumentOutOfRangeException(nameof(maxPlates));
if (!double.IsFinite(salvageRate) || salvageRate < 0 || salvageRate > 1)
throw new ArgumentOutOfRangeException(nameof(salvageRate));
if (!double.IsFinite(minimumSalvageDimension) || minimumSalvageDimension < 0)
throw new ArgumentOutOfRangeException(nameof(minimumSalvageDimension));
PlacementStrategy = placementStrategy;
MaxPlates = maxPlates;
SalvageRate = salvageRate;
MinimumSalvageDimension = minimumSalvageDimension;
SearchUntilCancelled = searchUntilCancelled;
OffcutCut = offcutCut;
}
/// <summary>
/// When set, only the edge strips one cut along this axis frees count as a sheet's offcut, for
/// salvage credit and for the compactness tie-break between equal-cost plans:
/// <see cref="CutOffAxis.Horizontal"/> keeps full-width strips at the bottom or top (the drop
/// off the end of a sheet), <see cref="CutOffAxis.Vertical"/> full-length ones at the sides.
/// Null (the default) counts the largest of all four.
/// </summary>
public CutOffAxis? OffcutCut { get; }
/// <summary>
/// Asks engines that support it to keep searching after their normal search, reporting
/// each better plan (<see cref="NestJobStage.PlanImproved"/>) until the solve's token is
/// cancelled, which then ends the solve with <see cref="OperationCanceledException"/>.
/// Meant for a caller that keeps the best reported plan at a time limit, such as
/// <see cref="NestingEngines.DeadlineNestingEngine"/>. Ignored when the token cannot be
/// cancelled. Results then depend on how long the search ran.
/// </summary>
public bool SearchUntilCancelled { get; }
/// <summary>A copy of these options with <see cref="SearchUntilCancelled"/> set.</summary>
public NestJobOptions WithSearchUntilCancelled(bool value) =>
new(PlacementStrategy, MaxPlates, SalvageRate, MinimumSalvageDimension, value, OffcutCut)
{
CommonLineUsage = CommonLineUsage,
CommonLineKerf = CommonLineKerf,
};
/// <summary>
/// When the pipeline may place parts that allow it on common lines, one
/// <see cref="CommonLineKerf"/> apart (see docs/common-line-cutting.md). Off by default;
/// without a positive kerf it acts as off.
/// </summary>
public CommonLineUsage CommonLineUsage
{
get => commonLineUsage;
init => commonLineUsage = Enum.IsDefined(value) ? value : throw new ArgumentOutOfRangeException(nameof(CommonLineUsage));
}
private readonly CommonLineUsage commonLineUsage;
private readonly double commonLineKerf;
/// <summary>The cutting kerf for the job's material and thickness, in job units; zero when unknown.</summary>
public double CommonLineKerf
{
get => commonLineKerf;
init => commonLineKerf = double.IsFinite(value) && value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(CommonLineKerf));
}
/// <summary>True when common lines may be made: a level is on and the kerf is positive.</summary>
public bool UsesCommonLines => CommonLineUsage != CommonLineUsage.Off && CommonLineKerf > 0;
/// <summary>Fraction of eligible edge-offcut area credited by <see cref="NestJobCost.NetSheetCost"/> (0..1).</summary>
public double SalvageRate { get; }
/// <summary>Both offcut dimensions must meet this caller-supplied minimum in job units.
/// Zero disables credit; scraps and holes are never credited.</summary>
public double MinimumSalvageDimension { get; }
public string PlacementStrategy { get; }
/// <summary>Maximum physical sheets to commit, or null for no explicit cap.</summary>
public int? MaxPlates { get; }
}