Files
OpenNest/OpenNest.IO.Tests/SupportGridPersistenceTests.cs
T
ajandClaude Opus 5.5 69a83b3ad9 feat(support): let a tab alone hold short parts the bed can't
Small parts such as 1.5" test squares on a 2" laser grid rest on at most
one grid point, so no single support tab can bring their center of
gravity inside the contacts and Plan Cutting warned "no single tab can
steady it" on every one. In practice a part that short stays in the
sheet on its tab, so the bed's contacts don't matter.

Cutting Settings > Tabs gets "Tab alone holds parts up to N long"
(CuttingParameters.TabHeldMaxLength, 0 = off, the default). On a plate
with a bed, a part the bed can't hold level whose length (long side of
its minimum-area bounding rectangle) is within the limit is still
tabbed, but any tab clear of inside corners steadies it, so Plan
Cutting, hand-placed lead-ins and the pre-post review warn only when it
has no tab at all. The setting persists with the cutting parameters and
is part of the plan state fingerprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 13:20:03 -04:00

172 lines
6.3 KiB
C#

using System.IO.Compression;
using System.Text;
using System.Text.Json.Nodes;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Geometry;
using OpenNest.Support;
namespace OpenNest.IO.Tests;
/// <summary>
/// The nest's support grid persists in <c>plateDefaults.supportBed</c> and every plate follows
/// it on load; a nest without one writes no key, and a malformed one loads as none.
/// </summary>
public class SupportGridPersistenceTests
{
private static readonly SupportGrid Grid = new(2, 1.5, 0.5, -0.25, 0.125, 0.25);
private static Nest NestWith(SupportBed bed)
{
var nest = new Nest("grid");
nest.PlateDefaults.Size = new Size(48, 96);
nest.SetSupportBed(bed);
var plate = nest.CreatePlate();
plate.CutOffs.Add(new CutOff(new Vector(40, 0), CutOffAxis.Vertical));
return nest;
}
private static MemoryStream Write(Nest nest)
{
var stream = new MemoryStream();
Assert.True(new NestWriter(nest).Write(stream));
stream.Position = 0;
return stream;
}
private static JsonNode NestJson(MemoryStream zip)
{
zip.Position = 0;
using var archive = new ZipArchive(zip, ZipArchiveMode.Read, leaveOpen: true);
using var reader = new StreamReader(archive.GetEntry("nest.json")!.Open(), Encoding.UTF8);
var json = JsonNode.Parse(reader.ReadToEnd())!;
zip.Position = 0;
return json;
}
private static MemoryStream Rewrite(MemoryStream zip, Action<JsonNode> edit)
{
var json = NestJson(zip);
edit(json);
var copy = new MemoryStream();
zip.Position = 0;
zip.CopyTo(copy);
using (var archive = new ZipArchive(copy, ZipArchiveMode.Update, leaveOpen: true))
{
archive.GetEntry("nest.json")!.Delete();
using var writer = new StreamWriter(archive.CreateEntry("nest.json").Open(), new UTF8Encoding(false));
writer.Write(json.ToJsonString());
}
copy.Position = 0;
return copy;
}
[Fact]
public void GridRoundTripsAndEveryPlateFollowsIt()
{
var reopened = new NestReader(Write(NestWith(Grid))).Read();
Assert.Equal(Grid, reopened.SupportBed);
Assert.Same(reopened.SupportBed, Assert.Single(reopened.Plates).SupportBed);
Assert.Same(reopened.SupportBed, reopened.CreatePlate().SupportBed);
}
[Fact]
public void GridIsWrittenWithItsType()
{
var bed = NestJson(Write(NestWith(Grid)))["plateDefaults"]!["supportBed"]!;
Assert.Equal("grid", bed["type"]!.GetValue<string>());
Assert.Equal(1.5, bed["pitchY"]!.GetValue<double>());
Assert.Equal(-0.25, bed["offsetY"]!.GetValue<double>());
}
[Fact]
public void SlatsRoundTripWithTheirTypeAndAxis()
{
var slats = new SupportSlats(SlatAxis.Y, 4, 0.125, 1.5, 0.125, 0.25);
var file = Write(NestWith(slats));
var bed = NestJson(file)["plateDefaults"]!["supportBed"]!;
var reopened = new NestReader(file).Read();
Assert.Equal("slats", bed["type"]!.GetValue<string>());
Assert.Equal("y", bed["axis"]!.GetValue<string>());
Assert.Equal(0.125, bed["thickness"]!.GetValue<double>());
Assert.False(bed.AsObject().ContainsKey("pitchX"));
Assert.Equal(slats, reopened.SupportBed);
Assert.Same(reopened.SupportBed, Assert.Single(reopened.Plates).SupportBed);
}
[Theory]
[InlineData("""{"type":"slats","axis":"z","pitch":4,"thickness":0.125}""")]
[InlineData("""{"type":"slats","axis":"x","pitch":0,"thickness":0.125}""")]
[InlineData("""{"type":"slats","axis":"x","pitch":4}""")]
[InlineData("""{"type":"slats","axis":"x","pitch":4,"thickness":0.125,"offset":"one"}""")]
public void MalformedSlats_LoadAsNone(string bed)
{
var file = Rewrite(Write(NestWith(Grid)), json => json["plateDefaults"]!["supportBed"] = JsonNode.Parse(bed));
Assert.Null(new NestReader(file).Read().SupportBed);
}
[Fact]
public void NestWithoutGrid_WritesNoKey()
{
var defaults = NestJson(Write(NestWith(null)))["plateDefaults"]!.AsObject();
Assert.False(defaults.ContainsKey("supportBed"));
}
[Theory]
[InlineData("""{"type":"slats","pitchX":2,"pitchY":2}""")]
[InlineData("""{"type":"grid","pitchX":0,"pitchY":2}""")]
[InlineData("""{"type":"grid","pitchX":"two","pitchY":2}""")]
[InlineData("""[1,2]""")]
public void MalformedOrUnknownGrid_LoadsAsNone(string bed)
{
var file = Rewrite(Write(NestWith(Grid)), json => json["plateDefaults"]!["supportBed"] = JsonNode.Parse(bed));
var reopened = new NestReader(file).Read();
Assert.Null(reopened.SupportBed);
Assert.Null(Assert.Single(reopened.Plates).SupportBed);
}
[Fact]
public void SupportTabsRoundTripInCuttingParameters()
{
var parameters = new CuttingParameters { SupportTabs = true, TabConfig = new NormalTab { Size = 0.1 } };
var restored = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
Assert.True(restored.SupportTabs);
}
[Fact]
public void NoBedTabRangeRoundTripsInCuttingParameters()
{
var parameters = new CuttingParameters { SupportTabs = true, NoBedTabMinWidth = 0.5, NoBedTabMaxWidth = 4 };
var restored = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
var snapshot = CuttingParametersSerializer.FromDto(CuttingParametersSerializer.ToDto(parameters));
Assert.Equal(0.5, restored.NoBedTabMinWidth);
Assert.Equal(4, restored.NoBedTabMaxWidth);
Assert.Equal(4, snapshot.NoBedTabMaxWidth);
Assert.Equal(0, CuttingParametersSerializer.Deserialize("{}").NoBedTabMaxWidth);
}
[Fact]
public void TabHeldLengthRoundTripsInCuttingParameters()
{
var parameters = new CuttingParameters { SupportTabs = true, TabHeldMaxLength = 2 };
var restored = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
var snapshot = CuttingParametersSerializer.FromDto(CuttingParametersSerializer.ToDto(parameters));
Assert.Equal(2, restored.TabHeldMaxLength);
Assert.Equal(2, snapshot.TabHeldMaxLength);
Assert.Equal(0, CuttingParametersSerializer.Deserialize("{}").TabHeldMaxLength);
}
}