fix(io): give re-imported unchanged DXFs the same entity ids

Entity ids came from a process-random sequence, so importing the same
DXF twice produced different source-entity ids and therefore a
different drawing library package (different SHA-256), and an importer
could not tell an unchanged file from a changed one.

CadImporter.Import now derives each entity id from the file's SHA-256
and the entity's position in the import. An unchanged file gives the
same ids and the same package bytes; any change to the file gives
entirely new ids, so suppression, roles and edge selections keyed by id
never move to a different entity.
This commit is contained in:
aj committed 2026-10-24 09:12:48 -04:00
1 parent ba2102c793
commit bc21382c5d
2 files changed
+76

No files matched your search

+30
View File
@@ -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
};
}
/// <summary>
/// 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.
/// </summary>
private static void AssignFileIds(string path, IList<Entity> entities)
{
Span<byte> input = stackalloc byte[32 + 4];
Span<byte> 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]);
}
}
/// <summary>
/// Convenience for headless callers: Import a file and build a Drawing
/// in a single call, using all loaded entities and detected bends.
+46
View File
@@ -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);
}
}
}
}