diff --git a/ExportDXF/Utilities/ContentHasher.cs b/ExportDXF/Utilities/ContentHasher.cs index 0be2612..4b08925 100644 --- a/ExportDXF/Utilities/ContentHasher.cs +++ b/ExportDXF/Utilities/ContentHasher.cs @@ -1,26 +1,32 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Linq; using System.Security.Cryptography; using System.Text; +using ACadSharp.Entities; +using ACadSharp.IO; namespace ExportDXF.Utilities { public static class ContentHasher { /// - /// Computes a SHA256 hash of DXF file content, skipping the HEADER section - /// which contains timestamps that change on every save. + /// Computes a SHA256 hash of DXF geometry, ignoring entity ordering, + /// handle assignments, style names, and floating-point epsilon differences + /// that SolidWorks changes between re-exports of identical geometry. + /// Falls back to a raw file hash if ACadSharp parsing fails. /// public static string ComputeDxfContentHash(string filePath) { - var text = File.ReadAllText(filePath); - var contentStart = FindEndOfHeader(text); - var content = contentStart >= 0 ? text.Substring(contentStart) : text; - - using (var sha = SHA256.Create()) + try { - var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content)); - return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); + return ComputeGeometricHash(filePath); + } + catch + { + return ComputeFileHash(filePath); } } @@ -37,82 +43,95 @@ namespace ExportDXF.Utilities } } - /// - /// Finds the position immediately after the HEADER section's ENDSEC marker. - /// DXF HEADER format: - /// 0\nSECTION\n2\nHEADER\n...variables...\n0\nENDSEC\n - /// Returns -1 if no HEADER section is found. - /// - private static int FindEndOfHeader(string text) + private static string ComputeGeometricHash(string filePath) { - // Find the HEADER section start - var headerIndex = FindGroupCode(text, 0, "2", "HEADER"); - if (headerIndex < 0) - return -1; - - // Advance past the HEADER value line so pair scanning stays aligned - var headerLineEnd = text.IndexOf('\n', headerIndex); - if (headerLineEnd < 0) - return -1; - - // Find the ENDSEC that closes the HEADER section - var pos = headerLineEnd + 1; - while (pos < text.Length) + using (var reader = new DxfReader(filePath)) { - var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC"); - if (endsecIndex < 0) - return -1; + var doc = reader.Read(); + var signatures = new List(); - // Move past the ENDSEC line - var lineEnd = text.IndexOf('\n', endsecIndex); - return lineEnd >= 0 ? lineEnd + 1 : text.Length; + foreach (var entity in doc.Entities) + { + signatures.Add(GetEntitySignature(entity)); + } + + signatures.Sort(StringComparer.Ordinal); + var combined = string.Join("\n", signatures); + + using (var sha = SHA256.Create()) + { + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combined)); + return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); + } } - - return -1; } - /// - /// Finds a DXF group code pair (code line followed by value line) starting from the given position. - /// Returns the position of the value line, or -1 if not found. - /// - private static int FindGroupCode(string text, int startIndex, string groupCode, string value) + private static string GetEntitySignature(Entity entity) { - var pos = startIndex; - while (pos < text.Length) + var layer = entity.Layer?.Name ?? ""; + + switch (entity) { - // Skip whitespace/newlines to find the group code - while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' ')) - pos++; + case Line line: + return GetLineSignature(line, layer); + case Arc arc: + return GetArcSignature(arc, layer); + case Circle circle: + return GetCircleSignature(circle, layer); + case MText mtext: + return GetMTextSignature(mtext, layer); + default: + return $"{entity.GetType().Name}|{layer}"; + } + } - if (pos >= text.Length) - break; + private static string GetLineSignature(Line line, string layer) + { + var p1 = FormatPoint(line.StartPoint.X, line.StartPoint.Y); + var p2 = FormatPoint(line.EndPoint.X, line.EndPoint.Y); - // Read the group code line - var codeLineEnd = text.IndexOf('\n', pos); - if (codeLineEnd < 0) - break; - - var codeLine = text.Substring(pos, codeLineEnd - pos).Trim(); - - // Move to the value line - var valueStart = codeLineEnd + 1; - if (valueStart >= text.Length) - break; - - var valueLineEnd = text.IndexOf('\n', valueStart); - if (valueLineEnd < 0) - valueLineEnd = text.Length; - - var valueLine = text.Substring(valueStart, valueLineEnd - valueStart).Trim(); - - if (codeLine == groupCode && string.Equals(valueLine, value, StringComparison.OrdinalIgnoreCase)) - return valueStart; - - // Move to the next pair - pos = valueLineEnd + 1; + // Normalize endpoint order so direction doesn't affect the hash + if (string.Compare(p1, p2, StringComparison.Ordinal) > 0) + { + var tmp = p1; + p1 = p2; + p2 = tmp; } - return -1; + return $"LINE|{layer}|{p1}|{p2}"; + } + + private static string GetArcSignature(Arc arc, string layer) + { + var center = FormatPoint(arc.Center.X, arc.Center.Y); + var r = R(arc.Radius); + var sa = R(arc.StartAngle); + var ea = R(arc.EndAngle); + return $"ARC|{layer}|{center}|{r}|{sa}|{ea}"; + } + + private static string GetCircleSignature(Circle circle, string layer) + { + var center = FormatPoint(circle.Center.X, circle.Center.Y); + var r = R(circle.Radius); + return $"CIRCLE|{layer}|{center}|{r}"; + } + + private static string GetMTextSignature(MText mtext, string layer) + { + var point = FormatPoint(mtext.InsertPoint.X, mtext.InsertPoint.Y); + var text = mtext.Value ?? ""; + return $"MTEXT|{layer}|{point}|{text}"; + } + + private static string R(double value) + { + return Math.Round(value, 4).ToString(CultureInfo.InvariantCulture); + } + + private static string FormatPoint(double x, double y) + { + return $"{R(x)},{R(y)}"; } } }