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); diff --git a/src/SpatialViewer.App/Strings/en-US/Resources.resw b/src/SpatialViewer.App/Strings/en-US/Resources.resw index 8802942..0187812 100644 --- a/src/SpatialViewer.App/Strings/en-US/Resources.resw +++ b/src/SpatialViewer.App/Strings/en-US/Resources.resw @@ -156,4 +156,12 @@ Download update Retry Waiting for restart + Export CAD compatibility report + Report is not available yet + The current drawing has not finished loading. + CAD compatibility report exported + Report saved to: {0}\nThe file path was copied to the clipboard. + Compatibility report path copied + CAD compatibility report export failed + The diagnostic report could not be written. Check local file permissions and try again. diff --git a/src/SpatialViewer.App/Strings/ja-JP/Resources.resw b/src/SpatialViewer.App/Strings/ja-JP/Resources.resw index 26c802a..6008482 100644 --- a/src/SpatialViewer.App/Strings/ja-JP/Resources.resw +++ b/src/SpatialViewer.App/Strings/ja-JP/Resources.resw @@ -156,4 +156,12 @@ 更新をダウンロード 再試行 再起動待ち + CAD 互換性レポートを出力 + レポートを出力できません + 現在の図面はまだ読み込みを完了していません。 + CAD 互換性レポートを出力しました + レポートを保存しました:{0}\nファイルパスをクリップボードにコピーしました。 + 互換性レポートのパスをコピーしました + CAD 互換性レポートの出力に失敗しました + 診断レポートを書き込めません。ローカルのファイル権限を確認して再試行してください。 diff --git a/src/SpatialViewer.App/Strings/zh-CN/Resources.resw b/src/SpatialViewer.App/Strings/zh-CN/Resources.resw index 37bbca6..b716927 100644 --- a/src/SpatialViewer.App/Strings/zh-CN/Resources.resw +++ b/src/SpatialViewer.App/Strings/zh-CN/Resources.resw @@ -156,4 +156,12 @@ 下载更新 重试 等待重启 + 导出 CAD 兼容性报告 + 暂时无法导出报告 + 当前图纸尚未完成读取。 + CAD 兼容性报告已导出 + 报告已保存到:{0}\n文件路径已复制到剪贴板。 + 兼容性报告路径已复制 + CAD 兼容性报告导出失败 + 无法写入诊断报告,请检查本机文件权限后重试。 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 @@ - + +