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

114 lines
4.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
namespace OpenNest.Engine.Jobs;
/// <summary>Alternate representation of checked physical sheets; never changes raw output.
/// Equality includes ordered requirement IDs and scalar bits, not instance indices.</summary>
internal static class RepeatedPlateGrouping
{
internal sealed record Group(NestJobPlateResult Sheet, int Quantity);
internal static IReadOnlyList<Group> Create(IReadOnlyList<NestJobPlateResult> sheets,
bool consolidate, CancellationToken token = default)
{
var groups = new List<Group>();
var firstByLayout = new Dictionary<NestJobPlateResult, int>(LayoutComparer.Instance);
foreach (var sheet in sheets)
{
token.ThrowIfCancellationRequested();
if (consolidate && sheet.Placements.Count > 0
&& firstByLayout.TryGetValue(sheet, out var first))
groups[first] = groups[first] with { Quantity = checked(groups[first].Quantity + 1) };
else
{
if (consolidate && sheet.Placements.Count > 0)
firstByLayout.Add(sheet, groups.Count);
groups.Add(new Group(sheet, 1));
}
}
return groups.AsReadOnly();
}
internal static bool SameStock(NestPlateStock a, NestPlateStock b) =>
string.Equals(a.Id, b.Id, StringComparison.Ordinal)
&& Bits(a.Size.Width) == Bits(b.Size.Width) && Bits(a.Size.Length) == Bits(b.Size.Length)
&& a.Quadrant == b.Quadrant && Bits(a.PartSpacing) == Bits(b.PartSpacing)
&& Bits(a.EdgeSpacing.Left) == Bits(b.EdgeSpacing.Left)
&& Bits(a.EdgeSpacing.Right) == Bits(b.EdgeSpacing.Right)
&& Bits(a.EdgeSpacing.Top) == Bits(b.EdgeSpacing.Top)
&& Bits(a.EdgeSpacing.Bottom) == Bits(b.EdgeSpacing.Bottom)
&& Bits(a.ClampClearance) == Bits(b.ClampClearance)
&& a.Quantity == b.Quantity
&& Equals(a.Outline, b.Outline)
&& a.Cost.HasValue == b.Cost.HasValue
&& (!a.Cost.HasValue || Bits(a.Cost.Value) == Bits(b.Cost.Value))
&& SameZones(a.ClampZones, b.ClampZones);
// C8: a layout solved with clamps is never reused for a plate without them - and a
// different clamp set is different stock. Ordered; ids and exact bits participate.
private static bool SameZones(
System.Collections.Generic.IReadOnlyList<ClampZone> x,
System.Collections.Generic.IReadOnlyList<ClampZone> y
)
{
if (x.Count != y.Count)
return false;
for (var i = 0; i < x.Count; i++)
{
if (
!string.Equals(x[i].Id, y[i].Id, StringComparison.Ordinal)
|| Bits(x[i].Bounds.X) != Bits(y[i].Bounds.X)
|| Bits(x[i].Bounds.Y) != Bits(y[i].Bounds.Y)
|| Bits(x[i].Bounds.Length) != Bits(y[i].Bounds.Length)
|| Bits(x[i].Bounds.Width) != Bits(y[i].Bounds.Width)
)
return false;
}
return true;
}
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
private sealed class LayoutComparer : IEqualityComparer<NestJobPlateResult>
{
internal static readonly LayoutComparer Instance = new();
public bool Equals(NestJobPlateResult a, NestJobPlateResult b)
{
if (ReferenceEquals(a, b))
return true;
if (a == null || b == null || !SameStock(a.Stock, b.Stock)
|| a.Placements.Count != b.Placements.Count
|| !System.Linq.Enumerable.SequenceEqual(a.CommonLines, b.CommonLines))
return false;
for (var i = 0; i < a.Placements.Count; i++)
{
var x = a.Placements[i];
var y = b.Placements[i];
if (!string.Equals(x.PartId, y.PartId, StringComparison.Ordinal)
|| Bits(x.X) != Bits(y.X) || Bits(x.Y) != Bits(y.Y)
|| Bits(x.Rotation) != Bits(y.Rotation))
return false;
}
return true;
}
public int GetHashCode(NestJobPlateResult sheet)
{
// Hash only accelerates lookup; Equals checks every stock field and ordered pose.
var hash = new HashCode();
hash.Add(sheet.StockId, StringComparer.Ordinal);
foreach (var pose in sheet.Placements)
{
hash.Add(pose.PartId, StringComparer.Ordinal);
hash.Add(Bits(pose.X));
hash.Add(Bits(pose.Y));
hash.Add(Bits(pose.Rotation));
}
return hash.ToHashCode();
}
}
}