feat: replace text-based DXF hash with geometric content hash
SolidWorks re-exports produce files with identical geometry but different entity ordering, handle assignments, style names, and floating-point epsilon values. This caused hash mismatches and unnecessary API updates. Uses ACadSharp to parse DXF entities and build canonical, sorted signatures (LINE, ARC, CIRCLE, MTEXT) with coordinates rounded to 4 decimal places. Falls back to raw file hash if parsing fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,32 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using ACadSharp.Entities;
|
||||||
|
using ACadSharp.IO;
|
||||||
|
|
||||||
namespace ExportDXF.Utilities
|
namespace ExportDXF.Utilities
|
||||||
{
|
{
|
||||||
public static class ContentHasher
|
public static class ContentHasher
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes a SHA256 hash of DXF file content, skipping the HEADER section
|
/// Computes a SHA256 hash of DXF geometry, ignoring entity ordering,
|
||||||
/// which contains timestamps that change on every save.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string ComputeDxfContentHash(string filePath)
|
public static string ComputeDxfContentHash(string filePath)
|
||||||
{
|
{
|
||||||
var text = File.ReadAllText(filePath);
|
try
|
||||||
var contentStart = FindEndOfHeader(text);
|
|
||||||
var content = contentStart >= 0 ? text.Substring(contentStart) : text;
|
|
||||||
|
|
||||||
using (var sha = SHA256.Create())
|
|
||||||
{
|
{
|
||||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
|
return ComputeGeometricHash(filePath);
|
||||||
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return ComputeFileHash(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,82 +43,95 @@ namespace ExportDXF.Utilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static string ComputeGeometricHash(string filePath)
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
private static int FindEndOfHeader(string text)
|
|
||||||
{
|
{
|
||||||
// Find the HEADER section start
|
using (var reader = new DxfReader(filePath))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC");
|
var doc = reader.Read();
|
||||||
if (endsecIndex < 0)
|
var signatures = new List<string>();
|
||||||
return -1;
|
|
||||||
|
|
||||||
// Move past the ENDSEC line
|
foreach (var entity in doc.Entities)
|
||||||
var lineEnd = text.IndexOf('\n', endsecIndex);
|
{
|
||||||
return lineEnd >= 0 ? lineEnd + 1 : text.Length;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static string GetEntitySignature(Entity entity)
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
private static int FindGroupCode(string text, int startIndex, string groupCode, string value)
|
|
||||||
{
|
{
|
||||||
var pos = startIndex;
|
var layer = entity.Layer?.Name ?? "";
|
||||||
while (pos < text.Length)
|
|
||||||
|
switch (entity)
|
||||||
{
|
{
|
||||||
// Skip whitespace/newlines to find the group code
|
case Line line:
|
||||||
while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' '))
|
return GetLineSignature(line, layer);
|
||||||
pos++;
|
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)
|
private static string GetLineSignature(Line line, string layer)
|
||||||
break;
|
{
|
||||||
|
var p1 = FormatPoint(line.StartPoint.X, line.StartPoint.Y);
|
||||||
|
var p2 = FormatPoint(line.EndPoint.X, line.EndPoint.Y);
|
||||||
|
|
||||||
// Read the group code line
|
// Normalize endpoint order so direction doesn't affect the hash
|
||||||
var codeLineEnd = text.IndexOf('\n', pos);
|
if (string.Compare(p1, p2, StringComparison.Ordinal) > 0)
|
||||||
if (codeLineEnd < 0)
|
{
|
||||||
break;
|
var tmp = p1;
|
||||||
|
p1 = p2;
|
||||||
var codeLine = text.Substring(pos, codeLineEnd - pos).Trim();
|
p2 = tmp;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user