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

92 lines
3.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
namespace OpenNest.Engine.Jobs.CommonLines;
/// <summary>
/// Decides whether a common-line layout replaces the best layout without common lines (or an
/// earlier common-line trial), by the job's <see cref="CommonLineUsage"/>. Both must be valid,
/// keepable proposals of the same job; an invalid incumbent always loses to a valid candidate.
/// The engines' own ranking (and <c>FillScore</c>) is untouched: this only chooses between the
/// layouts they returned.
/// </summary>
internal static class CommonLineRanking
{
/// <summary>Differences below this are rounding, never a reason to prefer either layout.</summary>
private const double Epsilon = 1e-6;
internal static bool Prefers(CommonLineUsage usage, NestPipelineResult candidate, NestPipelineResult incumbent)
{
ArgumentNullException.ThrowIfNull(candidate);
ArgumentNullException.ThrowIfNull(incumbent);
if (usage == CommonLineUsage.Off || !Keepable(candidate))
return false;
if (!Keepable(incumbent))
return true;
var placed = Placed(candidate).CompareTo(Placed(incumbent));
if (placed != 0)
return placed > 0;
var sheets = Sheets(candidate).CompareTo(Sheets(incumbent));
if (sheets != 0)
return sheets < 0;
// Equal parts and sheets: "when it adds parts" keeps the incumbent.
if (usage != CommonLineUsage.Whenever)
return false;
var material = Material(candidate, incumbent);
var cut = Compare(CutLength(candidate), CutLength(incumbent));
// Better on one measure and no worse on the other.
return (material > 0 && cut <= 0) || (cut < 0 && material >= 0);
}
private static bool Keepable(NestPipelineResult result) => result.CanKeep && result.IsValid;
private static int Placed(NestPipelineResult result) => result.Raw.Plates.Sum(p => p.Placements.Count);
private static int Sheets(NestPipelineResult result) => result.Raw.Plates.Count;
/// <summary>Positive when <paramref name="a"/> uses less material than <paramref name="b"/>:
/// a lower net cost (a larger salvage-credited remnant), then the larger compactness offcut
/// (a shorter used length).</summary>
internal static int Material(NestPipelineResult a, NestPipelineResult b)
{
var cost = Compare(NestJobCost.Evaluate(a.Job, a.Raw), NestJobCost.Evaluate(b.Job, b.Raw));
if (cost != 0)
return -cost;
return Compare(NestJobCost.CompactnessOffcut(a.Job, a.Raw), NestJobCost.CompactnessOffcut(b.Job, b.Raw));
}
/// <summary>
/// Total cut length: every part's cut contours, less each shared span once (a common line is
/// cut once). Repeated sheets count each copy.
/// </summary>
internal static double CutLength(NestPipelineResult result)
{
var lengths = new Dictionary<Drawing, double>(ReferenceEqualityComparer.Instance);
var total = 0.0;
foreach (var plate in result.Plates)
{
var length = plate.Parts.Sum(part => ContourLength(part.BaseDrawing, lengths));
foreach (var group in plate.CommonLines)
length -= CommonLineGeometry.Check(group).Spans.Sum(span => span.Length);
total += plate.Quantity * length;
}
return total;
}
private static double ContourLength(Drawing drawing, Dictionary<Drawing, double> lengths)
{
if (!lengths.TryGetValue(drawing, out var length))
{
length = ConvertProgram.ToGeometry(drawing.Program).ForNesting().Sum(entity => entity.Length);
lengths.Add(drawing, length);
}
return length;
}
private static int Compare(double a, double b) =>
System.Math.Abs(a - b) <= Epsilon * System.Math.Max(1, System.Math.Max(System.Math.Abs(a), System.Math.Abs(b)))
? 0 : a.CompareTo(b);
}