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); + } + } } }