Files
OpenNest/OpenNest.IO.Tests/CommonLineUsagePersistenceTests.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

106 lines
3.8 KiB
C#

using System.IO.Compression;
using System.Text;
using System.Text.Json.Nodes;
namespace OpenNest.IO.Tests;
public class CommonLineUsagePersistenceTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void AllowCommonLine_RoundTrips_AndIsOmittedWhenFalse(bool allow)
{
var drawing = DrawingPackageFixture.Build();
drawing.AllowCommonLine = allow;
var nest = new Nest(drawing.Name);
nest.Drawings.Add(drawing);
var bytes = Write(nest);
var reader = new NestReader(new MemoryStream(bytes));
var copy = Assert.Single(reader.Read().Drawings);
Assert.Empty(reader.Warnings);
Assert.Equal(allow, copy.AllowCommonLine);
var json = NestJson(bytes)["drawings"]!.AsArray().Single()!.AsObject();
Assert.Equal(allow, json.ContainsKey("allowCommonLine"));
}
[Theory]
[InlineData(CommonLineUsage.WhenItAddsParts, "whenItAddsParts")]
[InlineData(CommonLineUsage.Whenever, "whenever")]
public void CommonLineUsage_RoundTrips(CommonLineUsage usage, string saved)
{
var nest = new Nest("usage") { CommonLineUsage = usage };
var bytes = Write(nest);
Assert.Equal(saved, NestJson(bytes)["commonLineUsage"]!.GetValue<string>());
Assert.Equal(usage, new NestReader(new MemoryStream(bytes)).Read().CommonLineUsage);
}
[Fact]
public void CommonLineUsage_Off_IsOmitted()
{
var bytes = Write(new Nest("off"));
Assert.False(NestJson(bytes).ContainsKey("commonLineUsage"));
Assert.Equal(CommonLineUsage.Off, new NestReader(new MemoryStream(bytes)).Read().CommonLineUsage);
}
[Theory]
[InlineData("\"sometimes\"")]
[InlineData("\"\"")]
[InlineData("null")]
public void CommonLineUsage_Unknown_ReadsOff(string value)
{
var bytes = Write(new Nest("unknown") { CommonLineUsage = CommonLineUsage.Whenever });
var edited = ReplaceNestJson(bytes, json => json.Replace("\"commonLineUsage\":\"whenever\"",
$"\"commonLineUsage\":{value}"));
Assert.Equal(CommonLineUsage.Off, new NestReader(new MemoryStream(edited)).Read().CommonLineUsage);
}
private static byte[] Write(Nest nest)
{
using var stream = new MemoryStream();
Assert.True(new NestWriter(nest).Write(stream));
return stream.ToArray();
}
private static JsonObject NestJson(byte[] archive)
{
using var zip = new ZipArchive(new MemoryStream(archive), ZipArchiveMode.Read);
using var reader = new StreamReader(zip.GetEntry("nest.json")!.Open(), Encoding.UTF8);
return JsonNode.Parse(reader.ReadToEnd())!.AsObject();
}
private static byte[] ReplaceNestJson(byte[] archive, Func<string, string> edit)
{
using var output = new MemoryStream();
using (var source = new ZipArchive(new MemoryStream(archive), ZipArchiveMode.Read))
using (var target = new ZipArchive(output, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var entry in source.Entries)
{
using var input = entry.Open();
using var buffer = new MemoryStream();
input.CopyTo(buffer);
var bytes = buffer.ToArray();
if (entry.FullName == "nest.json")
{
var text = new StreamReader(new MemoryStream(bytes), Encoding.UTF8).ReadToEnd();
var compact = JsonNode.Parse(text)!.ToJsonString();
var changed = edit(compact);
Assert.NotEqual(compact, changed);
bytes = Encoding.UTF8.GetBytes(changed);
}
var copy = target.CreateEntry(entry.FullName);
using var write = copy.Open();
write.Write(bytes);
}
}
return output.ToArray();
}
}