diff --git a/OpenNest.IO/CadImporter.cs b/OpenNest.IO/CadImporter.cs
index 0350ea2a..397c0a74 100644
--- a/OpenNest.IO/CadImporter.cs
+++ b/OpenNest.IO/CadImporter.cs
@@ -1,6 +1,9 @@
+using System;
+using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security.Cryptography;
using OpenNest.Bending;
using OpenNest.Breakoffs;
using OpenNest.Converters;
@@ -77,6 +80,8 @@ namespace OpenNest.IO
repairReports = BendRepair.Apply(dxf.Entities, bends, options.BendRepair);
}
+ AssignFileIds(path, dxf.Entities);
+
return new CadImportResult
{
Entities = dxf.Entities,
@@ -89,6 +94,31 @@ namespace OpenNest.IO
};
}
+ ///
+ /// Gives each imported entity an id derived from the file's bytes and the entity's
+ /// position in the import, so importing an unchanged file again yields the same ids
+ /// (and the same library package), while any change to the file yields entirely new
+ /// ids: ids key suppression, roles and edge selections, so one must never move to
+ /// another entity.
+ ///
+ private static void AssignFileIds(string path, IList entities)
+ {
+ Span input = stackalloc byte[32 + 4];
+ Span digest = stackalloc byte[32];
+ using (var stream = File.OpenRead(path))
+ SHA256.HashData(stream, input[..32]);
+
+ for (var i = 0; i < entities.Count; i++)
+ {
+ BinaryPrimitives.WriteInt32LittleEndian(input[32..], i);
+ SHA256.HashData(input, digest);
+ // RFC 9562 version 8 (custom) layout.
+ digest[7] = (byte)((digest[7] & 0x0F) | 0x80);
+ digest[8] = (byte)((digest[8] & 0x3F) | 0x80);
+ entities[i].Id = new Guid(digest[..16]);
+ }
+ }
+
///
/// Convenience for headless callers: Import a file and build a Drawing
/// in a single call, using all loaded entities and detected bends.
diff --git a/OpenNest.Tests/IO/CadImporterTests.cs b/OpenNest.Tests/IO/CadImporterTests.cs
index 4569f0a1..0d30999b 100644
--- a/OpenNest.Tests/IO/CadImporterTests.cs
+++ b/OpenNest.Tests/IO/CadImporterTests.cs
@@ -156,5 +156,51 @@ namespace OpenNest.Tests.IO
Assert.NotNull(drawing.Program);
Assert.NotNull(drawing.SourceEntities);
}
+
+ [Fact]
+ public void ImportDrawing_UnchangedFile_GivesTheSameLibraryPackage()
+ {
+ var first = CadImporter.ImportDrawing(TestDxf);
+ var second = CadImporter.ImportDrawing(TestDxf);
+
+ Assert.Equal(
+ first.SourceEntities.Select(e => e.Id),
+ second.SourceEntities.Select(e => e.Id)
+ );
+ Assert.Equal(
+ DrawingPackage.Write(first, Units.Inches),
+ DrawingPackage.Write(second, Units.Inches)
+ );
+ }
+
+ [Fact]
+ public void Import_EntityIdsAreUniqueAndNonempty()
+ {
+ var ids = CadImporter.Import(TestDxf).Entities.Select(e => e.Id).ToList();
+
+ Assert.DoesNotContain(System.Guid.Empty, ids);
+ Assert.Equal(ids.Count, ids.Distinct().Count());
+ }
+
+ [Fact]
+ public void Import_ChangedFile_ReusesNoEntityIds()
+ {
+ // Ids key suppression, roles and edge selections, so a changed file must not
+ // hand an old id to whatever entity now sits at that position.
+ var changed = Path.Combine(Path.GetTempPath(), $"cadimporter-changed-{System.Guid.NewGuid():N}.dxf");
+ try
+ {
+ File.WriteAllText(changed, "999\nchanged\n" + File.ReadAllText(TestDxf));
+ var original = CadImporter.Import(TestDxf).Entities.Select(e => e.Id).ToHashSet();
+ var edited = CadImporter.Import(changed).Entities.Select(e => e.Id).ToList();
+
+ Assert.NotEmpty(edited);
+ Assert.DoesNotContain(edited, original.Contains);
+ }
+ finally
+ {
+ File.Delete(changed);
+ }
+ }
}
}