mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
- Add Drawing.HasCustomColor, set when a color is picked in Drawing Properties or comes from a library record with a stored color. - Persist it as customColor in .nest files (omitted when false) and keep it out of drawing packages; split pieces inherit it. - Recolor non-custom drawings from the active palette when a nest opens and when the scheme changes; custom colors keep their palette slot. - Library settings store a color only when it is custom. - Document drawing colors in nest-file-format.md and nesting-view-colors.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
8.0 KiB
C#
190 lines
8.0 KiB
C#
using System.IO.Compression;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.IO.Tests;
|
|
|
|
public class DrawingPackageTests
|
|
{
|
|
[Fact]
|
|
public void Package_RoundTripsTheDrawingAndUnits()
|
|
{
|
|
var drawing = DrawingPackageFixture.Build();
|
|
|
|
var contents = DrawingPackage.Read(DrawingPackage.Write(drawing, Units.Millimeters));
|
|
|
|
Assert.Empty(contents.Warnings);
|
|
Assert.Equal(Units.Millimeters, contents.Units);
|
|
var copy = contents.Drawing;
|
|
Assert.NotSame(drawing, copy);
|
|
Assert.Equal(NestWriter.GetProgramText(drawing.Program), NestWriter.GetProgramText(copy.Program));
|
|
Assert.Equal(NestWriter.GetSubProgramsText(drawing.Program), NestWriter.GetSubProgramsText(copy.Program));
|
|
// Record-owned fields are left at their defaults; the library record supplies them.
|
|
Assert.Equal(("", ""), (copy.Name, copy.Customer));
|
|
Assert.Equal(("", "", drawing.Material.Density), (copy.Material.Name, copy.Material.Grade, copy.Material.Density));
|
|
Assert.Equal((0, false), (copy.Priority, copy.CutFirst));
|
|
Assert.Equal(drawing.Source.Offset, copy.Source.Offset);
|
|
Assert.Equal(drawing.SourceEntities.Select(e => (e.Id, e.Type, e.Layer?.Name)),
|
|
copy.SourceEntities.Select(e => (e.Id, e.Type, e.Layer?.Name)));
|
|
Assert.True(drawing.SuppressedEntityIds.SetEquals(copy.SuppressedEntityIds));
|
|
var bend = Assert.Single(copy.Bends);
|
|
Assert.Equal((new Vector(0, 3), new Vector(10, 3), "UP 90"), (bend.StartPoint, bend.EndPoint, bend.NoteText));
|
|
// A library drawing has no nest demand of its own.
|
|
Assert.Equal(0, copy.Quantity.Required);
|
|
}
|
|
|
|
[Fact]
|
|
public void Package_DoesNotChangeTheDrawing()
|
|
{
|
|
var drawing = DrawingPackageFixture.Build();
|
|
var program = NestWriter.GetProgramText(drawing.Program);
|
|
|
|
DrawingPackage.Write(drawing, Units.Inches);
|
|
|
|
Assert.Equal(12, drawing.Quantity.Required);
|
|
Assert.Equal(program, NestWriter.GetProgramText(drawing.Program));
|
|
Assert.Equal("Synthetic bracket", drawing.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Package_IsCanonical_SameContentSameBytes()
|
|
{
|
|
// Source entity ids are content, so the same drawing instance is written twice.
|
|
var drawing = DrawingPackageFixture.Build();
|
|
var first = DrawingPackage.Write(drawing, Units.Inches);
|
|
drawing.Quantity.Required = 40;
|
|
Thread.Sleep(2100); // ZIP times have two-second resolution; prove they are not recorded.
|
|
|
|
var second = DrawingPackage.Write(drawing, Units.Inches);
|
|
|
|
Assert.Equal(Sha(first), Sha(second));
|
|
Assert.Equal(first, DrawingPackage.Write(DrawingPackage.Read(first).Drawing, Units.Inches));
|
|
using var archive = new ZipArchive(new MemoryStream(first));
|
|
Assert.All(archive.Entries, e => Assert.Equal(new DateTime(1980, 1, 1), e.LastWriteTime.DateTime));
|
|
Assert.DoesNotContain(archive.Entries, e => e.FullName.StartsWith("bestfits/", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void Package_ChangedContentOrUnits_ChangesBytes()
|
|
{
|
|
var drawing = DrawingPackageFixture.Build();
|
|
var baseline = Sha(DrawingPackage.Write(drawing, Units.Inches));
|
|
|
|
Assert.NotEqual(baseline, Sha(DrawingPackage.Write(drawing, Units.Millimeters)));
|
|
drawing.Bends[0].EndPoint = new Vector(10, 3.5);
|
|
Assert.NotEqual(baseline, Sha(DrawingPackage.Write(drawing, Units.Inches)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Package_RecordOwnedFields_DoNotChangeTheHash()
|
|
{
|
|
var drawing = DrawingPackageFixture.Build();
|
|
var baseline = Sha(DrawingPackage.Write(drawing, Units.Inches));
|
|
|
|
drawing.Name = "Renamed bracket";
|
|
drawing.Customer = "Other customer";
|
|
drawing.Material = new Material("Other Steel") { Grade = "Grade B", Density = drawing.Material.Density };
|
|
drawing.Color = System.Drawing.Color.FromArgb(255, 1, 2, 3);
|
|
drawing.HasCustomColor = true;
|
|
drawing.Priority = 9;
|
|
drawing.CutFirst = true;
|
|
drawing.Constraints = new NestConstraints { StepAngle = 0.5, StartAngle = 0.25, EndAngle = 3, Allow180Equivalent = true };
|
|
|
|
Assert.Equal(baseline, Sha(DrawingPackage.Write(drawing, Units.Inches)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Write_RejectsUndefinedUnits()
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => DrawingPackage.Write(DrawingPackageFixture.Build(), (Units)9));
|
|
}
|
|
|
|
[Fact]
|
|
public void Read_RefusesNestsThatAreNotOneDrawingWithoutPlates()
|
|
{
|
|
var empty = new Nest("empty");
|
|
var two = new Nest("two");
|
|
two.Drawings.Add(DrawingPackageFixture.Build());
|
|
var second = DrawingPackageFixture.Build();
|
|
second.Name = "Second";
|
|
two.Drawings.Add(second);
|
|
// The writer omits an empty plate, so the plate carries a part.
|
|
var withPlate = new Nest("plate");
|
|
var placed = DrawingPackageFixture.Build();
|
|
withPlate.Drawings.Add(placed);
|
|
var plate = new Plate(48, 96);
|
|
plate.Parts.Add(new Part(placed));
|
|
withPlate.Plates.Add(plate);
|
|
|
|
Assert.Throws<InvalidDataException>(() => DrawingPackage.Read(Archive(empty)));
|
|
Assert.Throws<InvalidDataException>(() => DrawingPackage.Read(Archive(two)));
|
|
Assert.Throws<InvalidDataException>(() => DrawingPackage.Read(Archive(withPlate)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Feet")]
|
|
[InlineData("1")]
|
|
[InlineData("Inches,Millimeters")]
|
|
[InlineData("")]
|
|
public void Read_RefusesUnsupportedUnits(string units)
|
|
{
|
|
var package = EditNestJson(DrawingPackage.Write(DrawingPackageFixture.Build(), Units.Inches),
|
|
root => root["units"] = units);
|
|
|
|
Assert.Throws<InvalidDataException>(() => DrawingPackage.Read(package));
|
|
}
|
|
|
|
[Fact]
|
|
public void Read_RefusesMalformedArchives()
|
|
{
|
|
var valid = DrawingPackage.Write(DrawingPackageFixture.Build(), Units.Inches);
|
|
var noNestJson = Rewrite(valid, name => name == "nest.json" ? null : name);
|
|
var badJson = Rewrite(valid, name => name, (name, bytes) => name == "nest.json" ? Encoding.UTF8.GetBytes("{") : bytes);
|
|
|
|
Assert.Throws<InvalidDataException>(() => DrawingPackage.Read(Encoding.UTF8.GetBytes("not a zip")));
|
|
Assert.Throws<InvalidDataException>(() => DrawingPackage.Read(noNestJson));
|
|
Assert.Throws<InvalidDataException>(() => DrawingPackage.Read(badJson));
|
|
}
|
|
|
|
private static string Sha(byte[] bytes) => Convert.ToHexString(SHA256.HashData(bytes));
|
|
|
|
private static byte[] Archive(Nest nest)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
new NestWriter(nest).Write(stream);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static byte[] EditNestJson(byte[] package, Action<JsonObject> edit) =>
|
|
Rewrite(package, name => name, (name, bytes) =>
|
|
{
|
|
if (name != "nest.json") return bytes;
|
|
var root = JsonNode.Parse(Encoding.UTF8.GetString(bytes))!.AsObject();
|
|
edit(root);
|
|
return Encoding.UTF8.GetBytes(root.ToJsonString());
|
|
});
|
|
|
|
private static byte[] Rewrite(byte[] package, Func<string, string?> rename, Func<string, byte[], byte[]>? transform = null)
|
|
{
|
|
using var source = new ZipArchive(new MemoryStream(package));
|
|
using var output = new MemoryStream();
|
|
using (var target = new ZipArchive(output, ZipArchiveMode.Create, leaveOpen: true))
|
|
{
|
|
foreach (var entry in source.Entries)
|
|
{
|
|
var name = rename(entry.FullName);
|
|
if (name is null) continue;
|
|
using var input = entry.Open();
|
|
using var copy = new MemoryStream();
|
|
input.CopyTo(copy);
|
|
var bytes = transform is null ? copy.ToArray() : transform(entry.FullName, copy.ToArray());
|
|
using var stream = target.CreateEntry(name).Open();
|
|
stream.Write(bytes);
|
|
}
|
|
}
|
|
return output.ToArray();
|
|
}
|
|
}
|