Files
OpenNest/OpenNest.IO.Tests/SupportGridPersistenceTests.cs
T
ajandClaude Opus 5.5 a7c0747e11 feat(planning): tab parts the laser support grid cannot hold level
Model the machine's support bed as a grid of contact points measured
from the plate origin, and check each part's center of gravity against
the convex hull of the grid points under its material. The bed is
extensible so straight and CAD-imported curved slats can follow.

- Core: SupportBed/SupportGrid, SupportMaterial and SupportAnalyzer
  (margin with registration tolerance, best single tab point)
- Nest-wide support bed that every plate follows, saved in
  plateDefaults.supportBed; machine defaults in support-grid.json
- CuttingParameters.SupportTabs: Plan Cutting tabs only unsupported
  parts and limits their perimeter entries to points where the tab
  steadies the part, falling back to any fitting entry
- Desktop: Nest > Support Grid..., View > Draw Support overlay and a
  "Tab parts the support grid cannot hold level" tab option
- docs/support-tabs.md, nest file format and planner notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:55:24 -04:00

116 lines
3.8 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 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);
}
}