mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-07 03:15:43 -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>
114 lines
4.6 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|