mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-26 22:01:06 -04:00
feat(core,io): drawing library link
A nest drawing copied from the drawing library has to remember exactly which revision it is a copy of, so later slices can tell when the library moved on (D2b) and so a save can target the right drawing. `Drawing.Library` holds the server key, id, revision and a snapshot of that revision's cut type, thickness, material, grade, units, notes and package hash. It is immutable, so copies can share it. `DrawingDto.library` is additive and omitted when null. It is read leniently: anything that cannot be trusted as a link drops the link with a reader warning and the drawing still loads, because a nest must never fail to open over provenance. Enum names are checked as single names since Enum.TryParse would accept numbers and comma lists. Copy paths: converter edits update drawings in place and so keep the link; canonical engine copies carry it; split pieces are new geometry and get none. Packages strip it so linking never changes a drawing's package bytes or hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8b63c042b5
commit
15654c0690
11 files changed
+440
-11
No files matched your search
@@ -112,6 +112,12 @@ namespace OpenNest
|
||||
|
||||
public List<Bend> Bends { get; set; } = new List<Bend>();
|
||||
|
||||
/// <summary>
|
||||
/// The library revision this drawing was copied from, or null when it did not come
|
||||
/// from a drawing library. Editing the drawing in a nest keeps the link as provenance.
|
||||
/// </summary>
|
||||
public DrawingLibraryLink Library { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Complete set of source entities with stable GUIDs.
|
||||
/// Null when the drawing was created from G-code or an older nest file.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Where a nest drawing came from in a server drawing library: the exact revision that
|
||||
/// was copied in and a snapshot of its library details at that time. The nest owns its
|
||||
/// copy; the link never changes the library and the library never changes the copy.
|
||||
/// Immutable, so drawing copies may share one instance.
|
||||
/// </summary>
|
||||
public sealed class DrawingLibraryLink
|
||||
{
|
||||
public DrawingLibraryLink(string serverKey, Guid id, int revision, CutType cutType, double thickness,
|
||||
string materialName, string materialGrade, Units units, string notes, string packageSha256)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serverKey))
|
||||
throw new ArgumentException("A library link needs the server it came from.", nameof(serverKey));
|
||||
if (id == Guid.Empty)
|
||||
throw new ArgumentException("A library link needs a drawing id.", nameof(id));
|
||||
if (revision < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(revision), revision, "Revisions start at 1.");
|
||||
if (!Enum.IsDefined(cutType))
|
||||
throw new ArgumentOutOfRangeException(nameof(cutType), cutType, "Unsupported cut type.");
|
||||
if (!double.IsFinite(thickness) || thickness < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(thickness), thickness, "Thickness must be finite and not negative.");
|
||||
if (!Enum.IsDefined(units))
|
||||
throw new ArgumentOutOfRangeException(nameof(units), units, "Unsupported units.");
|
||||
|
||||
ServerKey = serverKey;
|
||||
Id = id;
|
||||
Revision = revision;
|
||||
CutType = cutType;
|
||||
Thickness = thickness;
|
||||
MaterialName = materialName ?? "";
|
||||
MaterialGrade = materialGrade ?? "";
|
||||
Units = units;
|
||||
Notes = notes ?? "";
|
||||
PackageSha256 = packageSha256 ?? "";
|
||||
}
|
||||
|
||||
/// <summary>Normalized base URL of the server holding the library (no trailing slash).</summary>
|
||||
public string ServerKey { get; }
|
||||
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>The library revision this drawing is a copy of.</summary>
|
||||
public int Revision { get; }
|
||||
|
||||
public CutType CutType { get; }
|
||||
|
||||
/// <summary>Library thickness in <see cref="Units"/>.</summary>
|
||||
public double Thickness { get; }
|
||||
|
||||
public string MaterialName { get; }
|
||||
|
||||
public string MaterialGrade { get; }
|
||||
|
||||
public Units Units { get; }
|
||||
|
||||
public string Notes { get; }
|
||||
|
||||
/// <summary>Lower-case hex SHA-256 of the library package for <see cref="Revision"/>.</summary>
|
||||
public string PackageSha256 { get; }
|
||||
|
||||
/// <summary>True when both links name the same revision of the same library drawing.</summary>
|
||||
public bool IsSameRevision(DrawingLibraryLink other) =>
|
||||
other != null && Id == other.Id && Revision == other.Revision
|
||||
&& string.Equals(ServerKey, other.ServerKey, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,8 @@ public static class DrawingSplitter
|
||||
piece.Material = source.Material;
|
||||
piece.Constraints = source.Constraints;
|
||||
piece.Customer = source.Customer;
|
||||
// A piece is new geometry, not the library revision the source was copied from.
|
||||
piece.Library = null;
|
||||
piece.Source = source.Source;
|
||||
piece.Quantity.Required = source.Quantity.Required;
|
||||
|
||||
@@ -269,15 +271,15 @@ public static class DrawingSplitter
|
||||
|
||||
var regions = new List<Box>();
|
||||
for (var yi = 0; yi < yEdges.Count - 1; yi++)
|
||||
for (var xi = 0; xi < xEdges.Count - 1; xi++)
|
||||
regions.Add(
|
||||
new Box(
|
||||
xEdges[xi],
|
||||
yEdges[yi],
|
||||
xEdges[xi + 1] - xEdges[xi],
|
||||
yEdges[yi + 1] - yEdges[yi]
|
||||
)
|
||||
);
|
||||
for (var xi = 0; xi < xEdges.Count - 1; xi++)
|
||||
regions.Add(
|
||||
new Box(
|
||||
xEdges[xi],
|
||||
yEdges[yi],
|
||||
xEdges[xi + 1] - xEdges[xi],
|
||||
yEdges[yi + 1] - yEdges[yi]
|
||||
)
|
||||
);
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace OpenNest.Engine
|
||||
Priority = drawing.Priority,
|
||||
Customer = drawing.Customer,
|
||||
IsCutOff = drawing.IsCutOff,
|
||||
Library = drawing.Library,
|
||||
Source = new SourceInfo
|
||||
{
|
||||
Path = drawing.Source?.Path,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace OpenNest.IO.Tests;
|
||||
|
||||
public class DrawingLibraryLinkPersistenceTests
|
||||
{
|
||||
private static readonly Guid LibraryId = Guid.Parse("6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10");
|
||||
|
||||
private static DrawingLibraryLink Link() => new(
|
||||
"http://library.test:8090", LibraryId, 3, CutType.Waterjet, 0.1875,
|
||||
"Synthetic Steel", "Grade A", Units.Inches, "Deburr both sides", new string('a', 64));
|
||||
|
||||
[Fact]
|
||||
public void Link_RoundTripsEveryField()
|
||||
{
|
||||
var drawing = DrawingPackageFixture.Build();
|
||||
drawing.Library = Link();
|
||||
|
||||
var (restored, warnings) = RoundTrip(drawing);
|
||||
|
||||
Assert.Empty(warnings);
|
||||
var link = Assert.Single(restored.Drawings).Library;
|
||||
Assert.NotNull(link);
|
||||
Assert.Equal(
|
||||
("http://library.test:8090", LibraryId, 3, CutType.Waterjet, 0.1875, "Synthetic Steel", "Grade A",
|
||||
Units.Inches, "Deburr both sides", new string('a', 64)),
|
||||
(link.ServerKey, link.Id, link.Revision, link.CutType, link.Thickness, link.MaterialName,
|
||||
link.MaterialGrade, link.Units, link.Notes, link.PackageSha256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoLink_IsOmittedFromTheFile()
|
||||
{
|
||||
var drawing = DrawingPackageFixture.Build();
|
||||
|
||||
var json = NestJson(Write(drawing));
|
||||
var (restored, warnings) = RoundTrip(drawing);
|
||||
|
||||
Assert.False(json["drawings"]![0]!.AsObject().ContainsKey("library"));
|
||||
Assert.Empty(warnings);
|
||||
Assert.Null(Assert.Single(restored.Drawings).Library);
|
||||
}
|
||||
|
||||
public static TheoryData<string> MalformedLinks => new()
|
||||
{
|
||||
"\"not an object\"",
|
||||
"[]",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"not-a-guid\",\"revision\":1,\"cutType\":\"Laser\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":\"one\",\"cutType\":\"Laser\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":0,\"cutType\":\"Laser\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":1,\"cutType\":\"Laser\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"00000000-0000-0000-0000-000000000000\",\"revision\":1,\"cutType\":\"Laser\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":1,\"cutType\":\"1\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":1,\"cutType\":\"Laser,Plasma\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":1,\"cutType\":\"Oxyfuel\",\"units\":\"Inches\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":1,\"cutType\":\"Laser\",\"units\":\"Furlongs\"}",
|
||||
"{\"serverKey\":\"http://library.test\",\"id\":\"6d0f1c52-9a43-4f7e-8b8e-2f6f3c1d4a10\",\"revision\":1,\"cutType\":\"Laser\",\"units\":\"Inches\",\"thickness\":-0.25}",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MalformedLinks))]
|
||||
public void MalformedLink_LoadsTheDrawingWithoutItAndWarns(string linkJson)
|
||||
{
|
||||
var drawing = DrawingPackageFixture.Build();
|
||||
var archive = ReplaceLibrary(Write(drawing), JsonNode.Parse(linkJson));
|
||||
|
||||
var reader = new NestReader(new MemoryStream(archive));
|
||||
var restored = reader.Read();
|
||||
|
||||
var copy = Assert.Single(restored.Drawings);
|
||||
Assert.Null(copy.Library);
|
||||
Assert.Equal(NestWriter.GetProgramText(drawing.Program), NestWriter.GetProgramText(copy.Program));
|
||||
var warning = Assert.Single(reader.Warnings);
|
||||
Assert.Contains("'Synthetic bracket'", warning);
|
||||
Assert.Contains("library link", warning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullLink_ReadsAsNoLinkWithoutWarning()
|
||||
{
|
||||
var archive = ReplaceLibrary(Write(DrawingPackageFixture.Build()), null);
|
||||
|
||||
var reader = new NestReader(new MemoryStream(archive));
|
||||
var restored = reader.Read();
|
||||
|
||||
Assert.Null(Assert.Single(restored.Drawings).Library);
|
||||
Assert.Empty(reader.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Package_LeavesTheLinkOutAndKeepsItsBytes()
|
||||
{
|
||||
var drawing = DrawingPackageFixture.Build();
|
||||
var plainPackage = DrawingPackage.Write(drawing, Units.Inches);
|
||||
var link = Link();
|
||||
drawing.Library = link;
|
||||
|
||||
var linkedPackage = DrawingPackage.Write(drawing, Units.Inches);
|
||||
|
||||
Assert.Equal(SHA256.HashData(plainPackage), SHA256.HashData(linkedPackage));
|
||||
Assert.Null(DrawingPackage.Read(linkedPackage).Drawing.Library);
|
||||
Assert.Same(link, drawing.Library);
|
||||
}
|
||||
|
||||
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(Drawing drawing)
|
||||
{
|
||||
var reader = new NestReader(new MemoryStream(Write(drawing)));
|
||||
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[] ReplaceLibrary(byte[] archive, JsonNode? library)
|
||||
{
|
||||
var json = NestJson(archive);
|
||||
json["drawings"]![0]!["library"] = library;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace OpenNest.IO
|
||||
/// plates, written and read by the ordinary <see cref="NestWriter"/>/<see cref="NestReader"/>.
|
||||
/// Packages are canonical, so the same drawing content always has the same bytes and
|
||||
/// SHA-256: archive timestamps and nest dates are fixed, the nest-specific required
|
||||
/// quantity is zero, and derived best-fit caches are left out.
|
||||
/// quantity is zero, and derived best-fit caches and library links are left out.
|
||||
/// </summary>
|
||||
public static class DrawingPackage
|
||||
{
|
||||
@@ -123,8 +123,12 @@ namespace OpenNest.IO
|
||||
if (root["drawings"] is JsonArray drawings)
|
||||
{
|
||||
foreach (var drawing in drawings.OfType<JsonObject>())
|
||||
{
|
||||
if (drawing["quantity"] is JsonObject quantity)
|
||||
quantity["required"] = 0;
|
||||
// A library link is nest-side provenance; the server's record describes the package.
|
||||
drawing.Remove("library");
|
||||
}
|
||||
}
|
||||
|
||||
return root.ToJsonString(NestFormat.JsonOptions);
|
||||
|
||||
@@ -184,6 +184,68 @@ namespace OpenNest.IO
|
||||
public MaterialDto Material { get; init; } = new();
|
||||
public SourceDto Source { get; init; } = new();
|
||||
public List<BendDto> Bends { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The drawing library revision this drawing was copied from. Omitted when the
|
||||
/// drawing did not come from a library. Read leniently: a malformed value reads
|
||||
/// as <see cref="DrawingLibraryLinkDto.Malformed"/> and the reader drops it with a warning.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonConverter(typeof(LenientDrawingLibraryLinkConverter))]
|
||||
public DrawingLibraryLinkDto Library { get; init; }
|
||||
}
|
||||
|
||||
public record DrawingLibraryLinkDto
|
||||
{
|
||||
public string ServerKey { get; init; } = "";
|
||||
public Guid Id { get; init; }
|
||||
public int Revision { get; init; }
|
||||
public string CutType { get; init; } = "";
|
||||
public double Thickness { get; init; }
|
||||
public string MaterialName { get; init; } = "";
|
||||
public string MaterialGrade { get; init; } = "";
|
||||
public string Units { get; init; } = "";
|
||||
public string Notes { get; init; } = "";
|
||||
public string PackageSha256 { get; init; } = "";
|
||||
|
||||
/// <summary>Set only by the lenient reader for a value it could not read.</summary>
|
||||
[JsonIgnore]
|
||||
public bool Malformed { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a wrongly shaped or typed library link as a malformed marker instead of
|
||||
/// failing the whole nest; the nest reader validates the values it receives.
|
||||
/// </summary>
|
||||
public sealed class LenientDrawingLibraryLinkConverter : JsonConverter<DrawingLibraryLinkDto>
|
||||
{
|
||||
public override DrawingLibraryLinkDto Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options
|
||||
)
|
||||
{
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return new DrawingLibraryLinkDto { Malformed = true };
|
||||
|
||||
try
|
||||
{
|
||||
return document.RootElement.Deserialize<DrawingLibraryLinkDto>(options)
|
||||
?? new DrawingLibraryLinkDto { Malformed = true };
|
||||
}
|
||||
catch (Exception ex) when (ex is JsonException || ex is FormatException || ex is InvalidOperationException)
|
||||
{
|
||||
return new DrawingLibraryLinkDto { Malformed = true };
|
||||
}
|
||||
}
|
||||
|
||||
// The converter sits on the property, not the type, so this does not recurse.
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
DrawingLibraryLinkDto value,
|
||||
JsonSerializerOptions options
|
||||
) => JsonSerializer.Serialize(writer, value, options);
|
||||
}
|
||||
|
||||
public record PlateDto
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace OpenNest.IO
|
||||
private readonly Dictionary<int, string> drawingHashes = new();
|
||||
private readonly List<string> warnings = new();
|
||||
|
||||
/// <summary>Part programs that could not be restored; other parts still load.</summary>
|
||||
/// <summary>Part programs and library links that could not be restored; everything else still loads.</summary>
|
||||
public IReadOnlyList<string> Warnings => warnings.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
@@ -273,11 +273,49 @@ namespace OpenNest.IO
|
||||
}
|
||||
}
|
||||
|
||||
drawing.Library = ReadLibraryLink(d);
|
||||
map[d.Id] = drawing;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// A link that cannot be fully trusted is dropped: the drawing loads as a plain nest
|
||||
// drawing, which is what it is without a verifiable library revision.
|
||||
private DrawingLibraryLink ReadLibraryLink(DrawingDto d)
|
||||
{
|
||||
var link = d.Library;
|
||||
if (link == null)
|
||||
return null;
|
||||
|
||||
if (!link.Malformed
|
||||
&& !string.IsNullOrWhiteSpace(link.ServerKey)
|
||||
&& link.Id != Guid.Empty
|
||||
&& link.Revision >= 1
|
||||
&& TryParseName<CutType>(link.CutType, out var cutType)
|
||||
&& TryParseName<Units>(link.Units, out var units)
|
||||
&& double.IsFinite(link.Thickness)
|
||||
&& link.Thickness >= 0)
|
||||
{
|
||||
return new DrawingLibraryLink(link.ServerKey, link.Id, link.Revision, cutType, link.Thickness,
|
||||
link.MaterialName, link.MaterialGrade, units, link.Notes, link.PackageSha256);
|
||||
}
|
||||
|
||||
warnings.Add($"Drawing '{d.Name}': its drawing library link could not be read; "
|
||||
+ "loaded it as a nest drawing without a library revision.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Single enum names only: Enum.TryParse also accepts numbers and comma lists.
|
||||
private static bool TryParseName<T>(string text, out T value)
|
||||
where T : struct, Enum
|
||||
{
|
||||
value = default;
|
||||
return !string.IsNullOrEmpty(text)
|
||||
&& text.All(char.IsAsciiLetter)
|
||||
&& Enum.TryParse(text, ignoreCase: false, out value)
|
||||
&& Enum.IsDefined(value);
|
||||
}
|
||||
|
||||
private void ReadBestFits(Dictionary<int, Drawing> drawingMap)
|
||||
{
|
||||
if (BestFitStore == null)
|
||||
|
||||
@@ -198,12 +198,30 @@ namespace OpenNest.IO
|
||||
})
|
||||
.ToList()
|
||||
?? new List<BendDto>(),
|
||||
Library = ToLibraryLinkDto(d.Library),
|
||||
}
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static DrawingLibraryLinkDto ToLibraryLinkDto(DrawingLibraryLink link) =>
|
||||
link == null
|
||||
? null
|
||||
: new DrawingLibraryLinkDto
|
||||
{
|
||||
ServerKey = link.ServerKey,
|
||||
Id = link.Id,
|
||||
Revision = link.Revision,
|
||||
CutType = link.CutType.ToString(),
|
||||
Thickness = link.Thickness,
|
||||
MaterialName = link.MaterialName,
|
||||
MaterialGrade = link.MaterialGrade,
|
||||
Units = link.Units.ToString(),
|
||||
Notes = link.Notes,
|
||||
PackageSha256 = link.PackageSha256,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// True when a plate created from the saved defaults would be this sheet's clamp
|
||||
/// configuration: same size, quadrant, edge and part spacing, no last-resort
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class DrawingLibraryLinkTests
|
||||
{
|
||||
private static readonly Guid LibraryId = Guid.Parse("0b3c5a8e-41d2-4c6f-9e0a-7d1b2c3e4f50");
|
||||
|
||||
private static DrawingLibraryLink Link(int revision = 2) => new(
|
||||
"http://library.test:8090", LibraryId, revision, CutType.Laser, 0.25,
|
||||
"Synthetic Steel", "Grade A", Units.Inches, "", "");
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RefusesAnIncompleteLink()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new DrawingLibraryLink(
|
||||
" ", LibraryId, 1, CutType.Laser, 0, "", "", Units.Inches, "", ""));
|
||||
Assert.Throws<ArgumentException>(() => new DrawingLibraryLink(
|
||||
"http://library.test", Guid.Empty, 1, CutType.Laser, 0, "", "", Units.Inches, "", ""));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new DrawingLibraryLink(
|
||||
"http://library.test", LibraryId, 0, CutType.Laser, 0, "", "", Units.Inches, "", ""));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new DrawingLibraryLink(
|
||||
"http://library.test", LibraryId, 1, (CutType)99, 0, "", "", Units.Inches, "", ""));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new DrawingLibraryLink(
|
||||
"http://library.test", LibraryId, 1, CutType.Laser, double.NaN, "", "", Units.Inches, "", ""));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new DrawingLibraryLink(
|
||||
"http://library.test", LibraryId, 1, CutType.Laser, -0.1, "", "", Units.Inches, "", ""));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new DrawingLibraryLink(
|
||||
"http://library.test", LibraryId, 1, CutType.Laser, 0, "", "", (Units)99, "", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSameRevision_ComparesServerIdAndRevision()
|
||||
{
|
||||
var link = Link();
|
||||
|
||||
Assert.True(link.IsSameRevision(Link()));
|
||||
Assert.False(link.IsSameRevision(Link(revision: 3)));
|
||||
Assert.False(link.IsSameRevision(new DrawingLibraryLink(
|
||||
"http://other.test:8090", LibraryId, 2, CutType.Laser, 0.25, "", "", Units.Inches, "", "")));
|
||||
Assert.False(link.IsSameRevision(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalCopy_KeepsTheLink()
|
||||
{
|
||||
var drawing = new RectangleShape { Name = "LINKED", Length = 10, Width = 5 }.GetDrawing();
|
||||
drawing.Library = Link();
|
||||
|
||||
var copy = CanonicalFrame.AsCanonicalCopy(drawing);
|
||||
|
||||
Assert.Same(drawing.Library, copy.Library);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitPieces_AreNotTheLibraryDrawing()
|
||||
{
|
||||
var drawing = new RectangleShape { Name = "LINKED", Length = 100, Width = 50 }.GetDrawing();
|
||||
drawing.Library = Link();
|
||||
|
||||
var pieces = DrawingSplitter.Split(drawing,
|
||||
new List<SplitLine> { new(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters { Type = SplitType.Straight });
|
||||
|
||||
Assert.Equal(2, pieces.Count);
|
||||
Assert.All(pieces, piece => Assert.Null(piece.Library));
|
||||
Assert.NotNull(drawing.Library);
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,18 @@ without one, a reader skips them unparsed and a writer writes none. The front
|
||||
ends pass the engine's `BestFitCacheStore.Shared`, which adds loaded sets to
|
||||
the process-wide `BestFitCache` without replacing sets already computed.
|
||||
|
||||
## Drawing library links
|
||||
|
||||
A drawing copied from a server drawing library carries a `library` object:
|
||||
`serverKey` (the normalized server URL), `id`, `revision`, and a snapshot of that
|
||||
revision's `cutType`, `thickness`, `materialName`, `materialGrade`, `units`, `notes`
|
||||
and `packageSha256`. It is omitted for other drawings. Read leniently: a value that
|
||||
is not an object, has the wrong types, a blank server key, an empty id, a revision
|
||||
below 1, a cut type or units that are not one supported name, or a negative
|
||||
thickness loads the drawing without a link and adds a reader warning. Converter
|
||||
edits keep the link as provenance; split pieces get none. Drawing packages leave
|
||||
the link out, so linking a drawing does not change its package bytes.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
The automated tests cover save/reopen/re-save, rotated/manual/locked parts,
|
||||
|
||||
Reference in new issue
Block a user