Files
OpenNest/OpenNest.Engine/Jobs/NestJobOptions.cs
T
aj 561f5be06c feat(engine): choose which edge strips count as offcut
Equal-cost plans break ties by the largest edge offcut, from any of the
four edges. A caller that wants the leftover off one way, such as a
shop that keeps only end drops, or a benchmark scoring the used sheet
height, had no way to say so. On the converted Nestbolt jobs the
extended Irregular search then found plans whose larger offcut is a
strip down the side, which Nestbolt scores as worse: b-blaz1-strip
3.601 -> 3.867, b-shirts-strip 2.674 -> 2.995.

NestJobOptions.OffcutCut (a CutOffAxis, default null) counts only the
strips one cut along that axis frees, for salvage credit and the
compactness tie-break alike: Horizontal the full-width bottom and top
strips, Vertical the full-length sides. PlateRemnant.LargestOffcut takes
the axis; NestJobCost, the time limit and clamp-removal recovery pass
it on. Unset, nothing changes.

Benchmark manifests accept "offcutCut": "horizontal" or "vertical".
With horizontal, one CPU, 20 s limit, Default:
  b-blaz1-strip  3.934 -> 3.333
  b-shirts-strip 2.995 -> 2.610
  m-box-deep     1.918 -> 1.276 (Nestbolt 1.251)

Manifests accept only the words horizontal or vertical (any case); a
list such as "horizontal,vertical" is rejected. PlateRemnant rejects an
undefined axis. Fixed strategies keep their own no-salvage tie-break
and ignore the option; desktop, Console and MCP do not offer it yet.

Mutation reds: 14/14 (offcut-mutations.py).
2026-10-31 13:29:50 -04:00

70 lines
3.2 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 = "Fill",
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);
/// <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; }
}