From 106976ce3b18df5926dc3ba41c503cec9bb06630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiy=C5=8D=20Jo?= Date: Sat, 5 Sep 2026 21:53:18 +0800 Subject: [PATCH 1/7] feat: add privacy-safe CAD compatibility report builder --- .../CadCompatibilityReportBuilder.cs | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 src/SpatialViewer.App/CadCompatibilityReportBuilder.cs diff --git a/src/SpatialViewer.App/CadCompatibilityReportBuilder.cs b/src/SpatialViewer.App/CadCompatibilityReportBuilder.cs new file mode 100644 index 0000000..adef574 --- /dev/null +++ b/src/SpatialViewer.App/CadCompatibilityReportBuilder.cs @@ -0,0 +1,224 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text.Json; +using SpatialViewer.Formats.Cad; +using SpatialViewer.Formats.Cad.ACadSharp; + +namespace SpatialViewer.Product; + +internal static class CadCompatibilityReportBuilder +{ + public const int SchemaVersion = 1; + + private static readonly string[] SafeDocumentMetadataKeys = + [ + "Reader", + "ReaderVersion", + "SourceFormat", + "CadVersion", + "Units", + "CustomClassCount", + "CustomEntityCount", + "CustomProxyGraphicEntityCount", + "XiangyuanDetected", + "XiangyuanClassCount", + "XiangyuanEntityCount", + "RawProxyCommandCaptureSupported", + "RawProxyCommandCaptureFailed", + "RawProxyCommandCapturedEntityCount", + "RawProxyCommandMalformedEntityCount", + "RawProxyUnknownCommandEntityCount", + "RawProxyUnknownCommandCount", + "RawProxyUnknownTypeIds" + ]; + + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + { + WriteIndented = true + }; + + public static string Build(CadDocument document) + { + ArgumentNullException.ThrowIfNull(document); + + var customEntities = EnumerateEntities(document) + .OfType() + .ToArray(); + + var groups = customEntities + .GroupBy(EntityGroupKey.From) + .OrderBy(group => group.Key.Vendor, StringComparer.Ordinal) + .ThenBy(group => group.Key.ApplicationName, StringComparer.OrdinalIgnoreCase) + .ThenBy(group => group.Key.CppClassName, StringComparer.OrdinalIgnoreCase) + .ThenBy(group => group.Key.DxfName, StringComparer.OrdinalIgnoreCase) + .ThenBy(group => group.Key.SourceEntityType, StringComparer.OrdinalIgnoreCase) + .Select(group => BuildGroup(group.Key, group.ToArray())) + .ToArray(); + + var report = new CadCompatibilityReport( + SchemaVersion, + DateTimeOffset.UtcNow, + AppVersionProvider.Version, + typeof(CadDocument).Assembly.GetName().Version?.ToString() ?? "unknown", + typeof(ACadSharpCadImporter).Assembly.GetName().Version?.ToString() ?? "unknown", + document.SourceFormat, + document.Version, + document.Units.ToString(), + document.CustomClasses.Count, + customEntities.Length, + FilterMetadata(document.Metadata, SafeDocumentMetadataKeys), + groups); + + return JsonSerializer.Serialize(report, JsonOptions); + } + + private static CadCompatibilityCustomGroup BuildGroup( + EntityGroupKey key, + IReadOnlyList entities) + { + var primitiveKinds = entities + .SelectMany(entity => entity.ProxyGraphicKinds) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.Ordinal) + .OrderBy(value => value, StringComparer.Ordinal) + .ToArray(); + + var commandSignatures = DistinctMetadata(entities, "RawProxyCommandTypeSignature"); + var unknownTypeIds = entities + .SelectMany(entity => ParseIntegerList(Metadata(entity, "RawProxyUnknownTypeIds"))) + .Distinct() + .OrderBy(value => value) + .ToArray(); + + return new( + key.DxfName, + key.CppClassName, + key.ApplicationName, + key.SourceEntityType, + key.Vendor, + key.Representation, + entities.Count, + primitiveKinds, + SumMetadata(entities, "ProxyGraphicCount"), + SumMetadata(entities, "ProxyGraphicTranslatedCount"), + SumMetadata(entities, "ProxyGraphicUnsupportedCount"), + SumMetadata(entities, "RawProxyCommandDeclaredCount"), + SumMetadata(entities, "RawProxyCommandScannedCount"), + SumMetadata(entities, "RawProxyCommandKnownCount"), + SumMetadata(entities, "RawProxyCommandUnknownCount"), + entities.Count(entity => MetadataBoolean(entity, "RawProxyCommandMalformed")), + entities.Count(entity => MetadataBoolean(entity, "RawProxyCommandTruncated")), + unknownTypeIds, + commandSignatures); + } + + private static IEnumerable EnumerateEntities(CadDocument document) + { + foreach (var entity in document.ModelSpace) yield return entity; + foreach (var block in document.Blocks) + foreach (var entity in block.Entities) + yield return entity; + foreach (var layout in document.Layouts.Where(layout => layout.IsPaperSpace)) + foreach (var entity in layout.Entities) + yield return entity; + } + + private static IReadOnlyDictionary FilterMetadata( + IReadOnlyDictionary source, + IEnumerable allowList) + { + var safe = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in allowList) + if (source.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) + safe[key] = value; + return new ReadOnlyDictionary(safe); + } + + private static string[] DistinctMetadata( + IEnumerable entities, + string key) + => entities + .Select(entity => Metadata(entity, key)) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.Ordinal) + .OrderBy(value => value, StringComparer.Ordinal) + .ToArray(); + + private static string Metadata(CadCustomEntity entity, string key) + => entity.Metadata.TryGetValue(key, out var value) ? value : string.Empty; + + private static bool MetadataBoolean(CadCustomEntity entity, string key) + => bool.TryParse(Metadata(entity, key), out var value) && value; + + private static long SumMetadata( + IEnumerable entities, + string key) + { + long sum = 0; + foreach (var entity in entities) + if (long.TryParse(Metadata(entity, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0) + sum = checked(sum + value); + return sum; + } + + private static IEnumerable ParseIntegerList(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) yield break; + foreach (var token in raw.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + if (int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + yield return value; + } + + private sealed record EntityGroupKey( + string DxfName, + string CppClassName, + string ApplicationName, + string SourceEntityType, + string Vendor, + string Representation) + { + public static EntityGroupKey From(CadCustomEntity entity) + => new( + entity.ClassDefinition?.DxfName ?? string.Empty, + entity.ClassDefinition?.CppClassName ?? string.Empty, + entity.ClassDefinition?.ApplicationName ?? string.Empty, + entity.SourceEntityType, + entity.Vendor.ToString(), + entity.Representation.ToString()); + } +} + +internal sealed record CadCompatibilityReport( + int SchemaVersion, + DateTimeOffset GeneratedUtc, + string AppVersion, + string CadCoreAssemblyVersion, + string CadAdapterAssemblyVersion, + string SourceFormat, + string CadVersion, + string Units, + int CustomClassCount, + int CustomEntityCount, + IReadOnlyDictionary AggregateMetadata, + IReadOnlyList CustomGroups); + +internal sealed record CadCompatibilityCustomGroup( + string DxfName, + string CppClassName, + string ApplicationName, + string SourceEntityType, + string Vendor, + string Representation, + int EntityCount, + IReadOnlyList ProxyGraphicKinds, + long ProxyGraphicCommandCount, + long ProxyGraphicTranslatedCount, + long ProxyGraphicUnsupportedCount, + long RawCommandDeclaredCount, + long RawCommandScannedCount, + long RawCommandKnownCount, + long RawCommandUnknownCount, + int RawCommandMalformedEntityCount, + int RawCommandTruncatedEntityCount, + IReadOnlyList RawUnknownTypeIds, + IReadOnlyList RawCommandTypeSignatures); From b842d47a4e2fdd8b29800401c473daf649ed2955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiy=C5=8D=20Jo?= Date: Sat, 5 Sep 2026 21:53:33 +0800 Subject: [PATCH 2/7] feat: add CAD compatibility report export action --- src/SpatialViewer.App/Views/CadViewerView.xaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/SpatialViewer.App/Views/CadViewerView.xaml b/src/SpatialViewer.App/Views/CadViewerView.xaml index 5ed2572..19af612 100644 --- a/src/SpatialViewer.App/Views/CadViewerView.xaml +++ b/src/SpatialViewer.App/Views/CadViewerView.xaml @@ -30,7 +30,13 @@ - + +