mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 14:35:44 -05:00
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>
92 lines
3.9 KiB
C#
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);
|
|
}
|