mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
A hole can now be declared a tabbed cutout in the CAD Converter: pick a circle or any edge of a closed hole, set the tab count and width, and drag the tabs around the hole. The definition persists on the drawing and in the nest file, and stays editable from the source entities. The builder replaces the hole with open cut pieces and uncut spans on a new Tab layer. Tabs are material, so area, nesting and overlap checks still see a closed hole, but they are never cut. Plan Cutting and Assign Lead-ins cut each piece from the slug with the internal lead-in and no lead-out. A lead that would reach back over a tab is mirrored, and an arc lead onto an arc pierces straight in. Planning accounts for each span between tabs and runs the open-cut proof, so its own tabbed cutouts certify; tabs followed by a lead-out are still refused. Pre-post review flags programs that still contain tab motions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
4.9 KiB
C#
137 lines
4.9 KiB
C#
using System.IO.Compression;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
using OpenNest.Bending;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.TabbedCutouts;
|
|
|
|
namespace OpenNest.IO.Tests;
|
|
|
|
public class TabbedCutoutPersistenceTests
|
|
{
|
|
private static Drawing WithCutout()
|
|
{
|
|
var drawing = DrawingPackageFixture.Build();
|
|
drawing.TabbedCutouts.Add(new TabbedCutout
|
|
{
|
|
EdgeIds = [Guid.NewGuid(), Guid.NewGuid()],
|
|
TabWidth = 0.05,
|
|
TabCenters = [0.025, 1.5, 3.2],
|
|
});
|
|
return drawing;
|
|
}
|
|
|
|
[Fact]
|
|
public void TabbedCutouts_RoundTripEveryField()
|
|
{
|
|
var drawing = WithCutout();
|
|
var original = drawing.TabbedCutouts[0];
|
|
|
|
var (restored, warnings) = RoundTrip(Write(drawing));
|
|
|
|
Assert.Empty(warnings);
|
|
var cutout = Assert.Single(Assert.Single(restored.Drawings).TabbedCutouts);
|
|
Assert.Equal(original.EdgeIds, cutout.EdgeIds);
|
|
Assert.Equal(0.05, cutout.TabWidth);
|
|
Assert.Equal(original.TabCenters, cutout.TabCenters);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuiltTabbedCutout_KeepsItsTabMovesAndStaysEditable()
|
|
{
|
|
var hole = new[] { new Line(3, 3, 7, 3), new Line(7, 3, 7, 7), new Line(7, 7, 3, 7), new Line(3, 7, 3, 3) };
|
|
var entities = new List<Entity>
|
|
{
|
|
new Line(0, 0, 10, 0), new Line(10, 0, 10, 10), new Line(10, 10, 0, 10), new Line(0, 10, 0, 0),
|
|
};
|
|
entities.AddRange(hole);
|
|
var drawing = CadImporter.BuildDrawing(new CadImportResult { Entities = entities, Name = "tabbed" },
|
|
entities, new List<Bend>(), 1, null, null, null,
|
|
[new TabbedCutout { EdgeIds = hole.Select(e => e.Id).ToList() }]);
|
|
|
|
var (restored, warnings) = RoundTrip(Write(drawing));
|
|
|
|
Assert.Empty(warnings);
|
|
var reread = Assert.Single(restored.Drawings);
|
|
Assert.Equal(TabLength(drawing.Program), TabLength(reread.Program), 9);
|
|
Assert.Equal(2 * TabbedCutout.DefaultTabWidth, TabLength(reread.Program), 9);
|
|
Assert.Equal(drawing.Area, reread.Area, 9);
|
|
var layout = TabbedCutoutBuilder.Layout(CadImporter.EditableEntities(reread), Assert.Single(reread.TabbedCutouts));
|
|
Assert.True(layout.IsValid, layout.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoTabbedCutouts_AreOmittedFromTheFile()
|
|
{
|
|
var archive = Write(DrawingPackageFixture.Build());
|
|
|
|
Assert.False(NestJson(archive)["drawings"]![0]!.AsObject().ContainsKey("tabbedCutouts"));
|
|
Assert.Empty(Assert.Single(RoundTrip(archive).Nest.Drawings).TabbedCutouts);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnreadableTabbedCutout_IsDroppedWithAWarning_AndTheDrawingLoads()
|
|
{
|
|
var archive = Write(WithCutout());
|
|
var json = NestJson(archive);
|
|
json["drawings"]![0]!["tabbedCutouts"]![0]!["tabWidth"] = 0;
|
|
|
|
var (restored, warnings) = RoundTrip(ReplaceNestJson(archive, json));
|
|
|
|
Assert.Empty(Assert.Single(restored.Drawings).TabbedCutouts);
|
|
Assert.Contains(warnings, w => w.Contains("tabbed cutout 1 could not be read"));
|
|
}
|
|
|
|
// Tab moves may be split or merged by program round trips; their total length is what counts.
|
|
private static double TabLength(OpenNest.CNC.Program program)
|
|
{
|
|
var length = 0.0;
|
|
var position = Vector.Zero;
|
|
foreach (var code in program.Codes.OfType<Motion>())
|
|
{
|
|
var end = program.Mode == Mode.Incremental ? position + code.EndPoint : code.EndPoint;
|
|
if (code is LinearMove { Layer: LayerType.Tab })
|
|
length += position.DistanceTo(end);
|
|
position = end;
|
|
}
|
|
return length;
|
|
}
|
|
|
|
private static byte[] Write(Drawing drawing)
|
|
{
|
|
var nest = new Nest(drawing.Name) { Units = Units.Inches };
|
|
nest.Drawings.Add(drawing);
|
|
using var stream = new MemoryStream();
|
|
Assert.True(new NestWriter(nest).Write(stream));
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static (Nest Nest, IReadOnlyList<string> Warnings) RoundTrip(byte[] archive)
|
|
{
|
|
var reader = new NestReader(new MemoryStream(archive));
|
|
return (reader.Read(), reader.Warnings);
|
|
}
|
|
|
|
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, JsonObject json)
|
|
{
|
|
using var output = new MemoryStream();
|
|
output.Write(archive);
|
|
using (var zip = new ZipArchive(output, ZipArchiveMode.Update, leaveOpen: true))
|
|
{
|
|
zip.GetEntry("nest.json")!.Delete();
|
|
using var writer = new StreamWriter(zip.CreateEntry("nest.json").Open(), new UTF8Encoding(false));
|
|
writer.Write(json.ToJsonString());
|
|
}
|
|
|
|
return output.ToArray();
|
|
}
|
|
}
|