Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## [0.4.2] - 2026-09-06

### Added

- Added a native CAD compatibility-report export action at the bottom of the existing right-side Properties pane.
- Added a privacy-safe JSON report generated from the loaded reader-independent `CadDocument`, including app/kernel/adapter versions, safe aggregate reader metadata, structural custom-object groups, proxy primitive coverage and raw proxy-command diagnostics when available.
- Reports are saved to `Desktop/SpatialViewer Diagnostics` with a LocalAppData fallback, and the resulting path is copied to the clipboard.
- Added Simplified Chinese, Japanese and English UI feedback for report availability, success and failure states.

### Privacy and integration

- The report uses an explicit allow-list and does not export the drawing filename/path, entity or target handles, layer names, coordinates, raw property values, colors, text content, raw DWG/DXF bytes or complete entity metadata.
- SpatialViewer v0.4.2 is released against the latest stable compatible SpatialViewer.CadCore; v0.12.10 supplies the raw ObjectARX proxy-command inventory used by the new report.

### Preserved

- CAD rendering behavior, toolbar geometry, layer/properties pane layout, title bar, tabs, NavigationView, Projects, Favorites, Rhino 3DM rendering and the v0.4.1 responsiveness fixes remain unchanged.


## [0.4.1] - 2026-09-06

### Fixed
Expand Down
26 changes: 26 additions & 0 deletions docs/RELEASE-NOTES-v0.4.2.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SpatialViewer v0.4.2

[简体中文](RELEASE-NOTES-v0.4.2.md) | [日本語](RELEASE-NOTES-v0.4.2.ja.md) | English

v0.4.2 completes the formal CAD-kernel diagnostics integration and connects the previously prepared CAD compatibility-report exporter to the existing viewer UI.

## CAD compatibility report

- Adds **Export CAD compatibility report** at the bottom of the existing right-side Properties pane, preserving the current WinUI 3 controls, pane widths, toolbar geometry and canvas layout.
- Generates privacy-safe JSON directly from the loaded reader-independent `CadDocument`, including application/CadCore/adapter versions, allow-listed aggregate document metadata, structural custom-entity grouping, Proxy Graphics coverage, and the raw proxy-command structural diagnostics supplied by CadCore v0.12.10.
- Saves to `Desktop/SpatialViewer Diagnostics` by default with a LocalAppData fallback.
- Copies the exported path to the clipboard and reuses the existing InfoBar for success/failure feedback.

## Privacy boundary

The exporter uses an explicit allow-list. It does not serialize the drawing filename/path, entity or target handles, layer names, coordinates, raw property values, colors, text content, raw DWG/DXF bytes, or complete entity metadata.

Unknown raw proxy commands remain structural evidence only and are not promoted to Xiangyuan or any other proprietary semantics.

## Kernel

The v0.4.2 release pipeline continues to resolve and embed the latest stable compatible CadCore through the independent Host Contract. The corresponding formal kernel release is SpatialViewer.CadCore v0.12.10, with CLR ABI `1.0.0.0` unchanged.

## Preserved

Existing CAD rendering, CAD toolbar geometry, responsive left/right pane behavior, title bar, tabs, NavigationView, Projects/Favorites, and the Rhino 3DM rendering/responsiveness fixes from v0.4.1 remain unchanged.
26 changes: 26 additions & 0 deletions docs/RELEASE-NOTES-v0.4.2.ja.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SpatialViewer v0.4.2

[简体中文](RELEASE-NOTES-v0.4.2.md) | 日本語 | [English](RELEASE-NOTES-v0.4.2.en.md)

v0.4.2 では CAD カーネル診断チェーンを正式リリースに接続し、既に準備していた CAD 互換性レポート出力を既存のビューア UI に統合しました。

## CAD 互換性レポート

- CAD ページ右側の「プロパティ」サイドバー下部に「CAD 互換性レポートを出力」を追加しました。既存の WinUI 3 コントロール、サイドバー幅、レイアウト規則を維持し、ツールバーやキャンバス配置は変更しません。
- 現在読み込み済みの reader-independent `CadDocument` から JSON を生成し、アプリ/CadCore/adapter のバージョン、安全な集計 metadata、custom entity の構造分類、Proxy Graphics coverage、v0.12.10 の raw proxy-command structural diagnostics を含めます。
- 既定ではデスクトップの `SpatialViewer Diagnostics` に保存し、利用できない場合は LocalAppData にフォールバックします。
- 成功時は出力パスをクリップボードへコピーし、既存の InfoBar で結果を通知します。

## プライバシー境界

明示的な allow-list を使用し、図面ファイル名/パス、entity/target handle、layer 名、座標、raw property value、色、テキスト、raw DWG/DXF bytes、完全な entity metadata は出力しません。

unknown proxy command は構造証拠としてのみ扱い、湘源などの proprietary semantic を推測しません。

## カーネル

v0.4.2 の正式リリースは独立 Host Contract に基づいて最新の安定 CadCore を解決・同梱します。対応する正式カーネルは SpatialViewer.CadCore v0.12.10、CLR ABI は引き続き `1.0.0.0` です。

## 維持事項

既存の CAD レンダリング、CAD ツールバー形状、左右サイドバーのレスポンシブ規則、タイトルバー、タブ、NavigationView、Projects/Favorites、および v0.4.1 の Rhino 3DM 表示・応答性修正は変更しません。
31 changes: 31 additions & 0 deletions docs/RELEASE-NOTES-v0.4.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# SpatialViewer v0.4.2

简体中文 | [日本語](RELEASE-NOTES-v0.4.2.ja.md) | [English](RELEASE-NOTES-v0.4.2.en.md)

v0.4.2 完成 CAD 内核诊断链的正式发布接入,并把已经起草的 CAD 兼容性报告导出能力接到现有看图 UI。

## CAD 兼容性报告

- 在 CAD 页面右侧“属性”侧栏底部新增“导出 CAD 兼容性报告”入口,沿用现有 WinUI 3 控件、侧栏宽度和交互层级,不改变工具栏及画布布局。
- 报告直接从当前已载入的 reader-independent `CadDocument` 生成 JSON,包含:
- SpatialViewer / CadCore / CAD adapter 版本;
- 安全的文档级汇总元数据;
- custom entity 的结构类别、vendor 与 representation;
- Proxy Graphics primitive 覆盖与 translated / unsupported 汇总;
- CadCore v0.12.10 提供的 raw proxy-command structural signature、unknown type IDs 与 malformed / truncated 汇总。
- 默认保存到桌面的 `SpatialViewer Diagnostics` 文件夹;桌面路径不可用时回退到 LocalAppData。
- 导出成功后自动复制报告路径,并通过现有 InfoBar 显示成功/失败状态。

## 隐私边界

兼容性报告采用显式 allow-list,不导出源图纸文件名/路径、entity handle、target handle、图层名、坐标、原始属性值、颜色、文字内容、raw DWG/DXF bytes 或完整 entity metadata。

raw proxy-command 仅输出结构证据,不把 unknown command 猜测为湘源或其他 proprietary semantic。

## 内核

v0.4.2 的正式发布流程继续按照独立 Host Contract 解析并嵌入最新稳定 CadCore。对应正式内核为 SpatialViewer.CadCore v0.12.10,CLR ABI 保持 `1.0.0.0`。

## 保持不变

本次不修改既有 CAD 渲染结果、CAD 工具栏几何、左右侧栏响应式规则、标题栏、标签页、NavigationView、项目/收藏,也不回退 v0.4.1 已完成的 Rhino 3DM 黑屏和响应性修复。
2 changes: 1 addition & 1 deletion external/SpatialViewer.CadCore
10 changes: 5 additions & 5 deletions release/release.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
"schemaVersion": 1,
"product": {
"name": "SpatialViewer",
"version": "0.4.1",
"packageVersion": "0.4.1.0"
"version": "0.4.2",
"packageVersion": "0.4.2.0"
},
"channels": {
"github": {
"publish": true
}
},
"title": {
"zh-CN": "SpatialViewer v0.4.1 Rhino 显示与响应性热修",
"ja-JP": "SpatialViewer v0.4.1 Rhino 表示・応答性ホットフィックス",
"en-US": "SpatialViewer v0.4.1 Rhino Rendering and Responsiveness Hotfix"
"zh-CN": "SpatialViewer v0.4.2 CAD 兼容性报告与内核诊断",
"ja-JP": "SpatialViewer v0.4.2 CAD 互換性レポートとカーネル診断",
"en-US": "SpatialViewer v0.4.2 CAD Compatibility Report and Kernel Diagnostics"
}
}
6 changes: 3 additions & 3 deletions src/SpatialViewer.App/AppVersionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ namespace SpatialViewer.Product;

internal static class AppVersionProvider
{
public const string Version = "0.4.1";
public const string DisplayVersion = "v0.4.1";
public const string Version = "0.4.2";
public const string DisplayVersion = "v0.4.2";

public static Version GetCurrentVersion()
{
Expand All @@ -16,7 +16,7 @@ public static Version GetCurrentVersion()
}
catch (Exception) when (OperatingSystem.IsWindows())
{
var assemblyVersion = typeof(AppVersionProvider).Assembly.GetName().Version ?? new Version(0, 4, 1, 0);
var assemblyVersion = typeof(AppVersionProvider).Assembly.GetName().Version ?? new Version(0, 4, 2, 0);
return new Version(assemblyVersion.Major, assemblyVersion.Minor, Math.Max(0, assemblyVersion.Build), Math.Max(0, assemblyVersion.Revision));
}
}
Expand Down
234 changes: 234 additions & 0 deletions src/SpatialViewer.App/CadCompatibilityReportBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
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<CadCustomEntity>()
.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,
GetProductVersion(typeof(CadDocument)),
GetProductVersion(typeof(ACadSharpCadImporter)),
document.SourceFormat,
document.Version,
document.Units.ToString(),
document.CustomClasses.Count,
customEntities.Length,
FilterMetadata(document.Metadata, SafeDocumentMetadataKeys),
groups);

return JsonSerializer.Serialize(report, JsonOptions);
}

private static string GetProductVersion(Type type)
{
var fileVersion = System.Diagnostics.FileVersionInfo
.GetVersionInfo(type.Assembly.Location)
.FileVersion;
return string.IsNullOrWhiteSpace(fileVersion)
? type.Assembly.GetName().Version?.ToString() ?? "unknown"
: fileVersion;
}

private static CadCompatibilityCustomGroup BuildGroup(
EntityGroupKey key,
CadCustomEntity[] 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.Length,
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<CadEntity> 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 ReadOnlyDictionary<string, string> FilterMetadata(
IReadOnlyDictionary<string, string> source,
IEnumerable<string> allowList)
{
var safe = new SortedDictionary<string, string>(StringComparer.Ordinal);
foreach (var key in allowList)
if (source.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
safe[key] = value;
return new ReadOnlyDictionary<string, string>(safe);
}

private static string[] DistinctMetadata(
IEnumerable<CadCustomEntity> 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<CadCustomEntity> 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<int> 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 CadCoreVersion,
string CadAdapterVersion,
string SourceFormat,
string CadVersion,
string Units,
int CustomClassCount,
int CustomEntityCount,
IReadOnlyDictionary<string, string> AggregateMetadata,
IReadOnlyList<CadCompatibilityCustomGroup> CustomGroups);

internal sealed record CadCompatibilityCustomGroup(
string DxfName,
string CppClassName,
string ApplicationName,
string SourceEntityType,
string Vendor,
string Representation,
int EntityCount,
IReadOnlyList<string> ProxyGraphicKinds,
long ProxyGraphicCommandCount,
long ProxyGraphicTranslatedCount,
long ProxyGraphicUnsupportedCount,
long RawCommandDeclaredCount,
long RawCommandScannedCount,
long RawCommandKnownCount,
long RawCommandUnknownCount,
int RawCommandMalformedEntityCount,
int RawCommandTruncatedEntityCount,
IReadOnlyList<int> RawUnknownTypeIds,
IReadOnlyList<string> RawCommandTypeSignatures);
Loading
Loading