From 8d10f959b29f29f89983e73f18d2dbef38df9d6c Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Tue, 24 Mar 2026 19:06:50 +0800 Subject: [PATCH] Add fragment up detection and real object pose probes --- .agents/skills/geometry-transform/SKILL.md | 5 +- AGENTS.md | 1026 ++++++----------- NavisworksTransport.UnitTests.csproj | 2 + TransportPlugin.csproj | 2 + .../FragmentRepresentativePoseHelperTests.cs | 123 ++ .../RealObjectPlanarPoseSolverTests.cs | 191 +++ resources/default_config.toml | 20 +- src/Core/Animation/PathAnimationManager.cs | 235 +++- src/Core/Config/SystemConfig.cs | 1 + src/Core/PathPointRenderPlugin.cs | 25 + .../ViewModels/SystemManagementViewModel.cs | 723 +++++++++++- src/UI/WPF/Views/SystemManagementView.xaml | 30 +- .../FragmentRepresentativePoseHelper.cs | 251 ++++ .../CoordinateSystem/HostCoordinateAdapter.cs | 64 +- .../RealObjectPlanarPoseSolver.cs | 235 ++++ 15 files changed, 2178 insertions(+), 755 deletions(-) create mode 100644 UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs create mode 100644 UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs create mode 100644 src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs create mode 100644 src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs diff --git a/.agents/skills/geometry-transform/SKILL.md b/.agents/skills/geometry-transform/SKILL.md index a6007d1..420f22b 100644 --- a/.agents/skills/geometry-transform/SKILL.md +++ b/.agents/skills/geometry-transform/SKILL.md @@ -22,6 +22,8 @@ Use this skill for any work involving coordinate-system transforms, object pose, - `src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs` - `src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs` - `src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs` +- `src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs` +- `src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs` - `src/Utils/ModelItemTransformHelper.cs` - `src/Core/Animation/PathAnimationManager.cs` - `src/Core/VirtualObjectManager.cs` @@ -33,6 +35,8 @@ Use this skill for any work involving coordinate-system transforms, object pose, - UI, logs, and user inputs/output stay in host coordinates. - Internal pose solving stays in canonical space. - Asset coordinates only apply to plugin-owned assets such as the virtual object and the unit cylinder/reference rod. +- Real-object planar pose solving must stay in host coordinates and should keep the reference-pose source injectable so fragment-derived representative pose can be wired in later. +- Real-object planar pose solving should prefer fragment-derived representative pose when available; original `Transform` is only an explicit fallback, not the primary source. - Do not assume `BoundingBox.Center` is a stable pose anchor after rotation unless the flow explicitly proves it. - Do not add temporary force-sync or fallback logic unless it is removed after the root cause is fixed. - Do not mix virtual-object behavior into real-object behavior through shared mutable mode flags. @@ -50,4 +54,3 @@ Use this skill for any work involving coordinate-system transforms, object pose, - Lock baseline pose, rotated pose, and restore behavior. - For passage-space or footprint changes, verify extents after rotation, not just orientation. - For virtual objects, verify scale preservation and CAD restore behavior. - diff --git a/AGENTS.md b/AGENTS.md index 9e55c6a..d5ee793 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,657 +1,385 @@ # AGENTS.md -本文件为AI编码助手提供 NavisworksTransport 项目的完整开发指南。阅读本文件前,请确保你已经了解项目的基本结构和目标。 +本文件面向后续会话中的 AI 编码助手。目标不是写一份“大而全”的历史说明,而是让新会话能快速理解: -## 项目概述 +- 项目现在在做什么 +- 哪些架构已经稳定 +- 哪些开发原则不能再破坏 +- 遇到问题时优先看哪里 -**NavisworksTransport** 是专为 Autodesk Navisworks Manage 2026 开发的物流路径规划插件,用于BIM模型中的运输冲突检测和路径规划。 +如果本文件与更细的专项文档冲突,优先参考: -### 核心功能 - -- **物流属性管理**: 为模型分配类别属性(门、电梯、楼梯、通道、障碍物) -- **自动路径规划**: 基于A*算法的2.5D网格路径规划,支持通道优先策略 -- **碰撞检测**: 与Navisworks ClashDetective集成,实现动态碰撞检测 -- **动画仿真**: TimeLiner集成,支持路径动画播放和仿真 -- **吊装路径**: 支持空中吊装路径规划(两次点击模式) -- **数据导出**: 支持XML、JSON、CSV格式的路径数据导出,以及DELMIA数据格式 - -### 技术栈 - -| 组件 | 版本/说明 | -|------|----------| -| 目标平台 | Navisworks Manage 2026 | -| 框架 | .NET Framework 4.8 | -| 语言 | C# 7.3 | -| 架构 | x64 | -| UI框架 | WPF (MVVM模式) + Windows Forms集成 | - -### 项目文件 - -| 文件 | 说明 | -|------|------| -| `TransportPlugin.csproj` | 主插件项目(旧式csproj格式) | -| `NavisworksTransport.UnitTests.csproj` | 单元测试项目 | -| `TransportPlugin.sln` | Visual Studio 解决方案 | -| `packages.config` | NuGet包配置(旧式包管理) | -| `default_config.toml` | 默认配置文件模板 | -| `compile.bat` | 构建脚本 | -| `run-unit-tests.bat` | 测试脚本 | -| `deploy-plugin.bat` | 部署脚本 | - -## 构建命令 - -### 环境要求 - -- Windows 10 或更高版本 -- Visual Studio 2022(Community/Professional) -- Navisworks Manage 2026(已安装) -- .NET Framework 4.8 Developer Pack - -### 主要构建 - -```bash -./compile.bat -``` - -- 自动检测 Visual Studio 2022 的 MSBuild -- 构建 Release 配置的 x64 平台 -- 输出目录: `bin\x64\Release\` - -### 部署插件 - -```bash -./deploy-plugin.bat -``` - -自动复制插件文件到 Navisworks 插件目录: -`C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\` - -## 项目架构 - -### 目录结构 - -``` -src/ -├── Core/ # 核心插件和业务逻辑 -│ ├── MainPlugin.cs # DockPanePlugin - Ribbon UI + 停靠面板 -│ ├── PathClickToolPlugin.cs # ToolPlugin - 3D鼠标交互工具 -│ ├── PathPointRenderPlugin.cs # RenderPlugin - 3D可视化渲染 -│ ├── PathPlanningManager.cs # 路径规划管理器(与UI解耦) -│ ├── PathDataManager.cs # 路径数据管理 -│ ├── PathDatabase.cs # SQLite数据库操作 -│ ├── PathCurveEngine.cs # 路径曲线化引擎 -│ ├── UIStateManager.cs # 线程安全的UI状态管理 -│ ├── Animation/ # 动画系统 -│ │ ├── PathAnimationManager.cs -│ │ └── TimeLinerIntegrationManager.cs -│ ├── Collision/ # 碰撞检测 -│ │ ├── ClashDetectiveIntegration.cs -│ │ └── BatchCollisionProcessor.cs -│ ├── Spatial/ # 空间索引 -│ │ ├── SpatialHashGrid.cs -│ │ └── SpatialIndexManager.cs -│ ├── Properties/ # 属性管理 -│ │ ├── CategoryAttributeManager.cs -│ │ └── NavisworksComPropertyManager.cs -│ └── Config/ # 配置管理 -│ ├── SystemConfig.cs -│ └── ConfigManager.cs -├── Commands/ # 命令模式实现 -│ ├── CommandBase.cs -│ ├── CommandManager.cs -│ ├── AutoPathPlanningCommand.cs -│ └── ... -├── PathPlanning/ # A*算法和网格地图 -│ ├── GridMap.cs # 网格地图定义 -│ ├── GridMapGenerator.cs # 网格生成器 -│ ├── AutoPathFinder.cs # A*寻路实现 -│ ├── ChannelBasedGridBuilder.cs # 通道优先网格构建 -│ ├── VoxelGrid.cs # 3D体素网格(实验性) -│ └── PathOptimizer.cs # 路径优化 -├── UI/WPF/ # WPF用户界面 -│ ├── Views/ # XAML视图 -│ ├── ViewModels/ # MVVM视图模型 -│ ├── Models/ # 数据模型 -│ ├── Converters/ # 值转换器 -│ ├── Commands/ # WPF命令 -│ └── Services/ # UI服务 -└── Utils/ # 工具类 - ├── UnitsConverter.cs # 单位转换(关键!) - ├── LogManager.cs # 日志管理 - ├── GeometryHelper.cs # 几何计算 - └── ... - -UnitTests/ # 单元测试 -├── Core/ # 核心功能测试 -├── Commands/ # 命令测试 -└── Utils/ # 工具类测试 -``` - -### 插件类型说明 - -| 插件类 | 类型 | 功能 | -|--------|------|------| -| `MainPlugin.cs` | DockPanePlugin | 主UI面板,包含WPF控件宿主 | -| `PathClickToolPlugin.cs` | ToolPlugin | 鼠标点击工具,获取精确的3D坐标 | -| `PathPointRenderPlugin.cs` | RenderPlugin | 3D渲染,显示路径点和连线 | - -### 关键依赖 - -**Navisworks API(必须安装Navisworks 2026):** - -- `Autodesk.Navisworks.Api.dll` -- `Autodesk.Navisworks.ComApi.dll` -- `Autodesk.Navisworks.Interop.ComApi.dll` -- `Autodesk.Navisworks.Timeliner.dll` -- `Autodesk.Navisworks.Clash.dll` -- `Autodesk.Navisworks.Controls.dll` - -**NuGet包:** - -- `RoyT.AStar 3.0.2` - A*寻路算法 -- `geometry4Sharp 1.0.0` - 3D几何计算(体素路径规划) -- `System.Data.SQLite.Core 1.0.118.0` - SQLite数据库 -- `Tomlyn 0.19.0` - TOML配置文件解析 -- `MSTest.TestFramework 3.0.4` - 单元测试框架 - -## 代码规范 - -### 导入顺序 - -```csharp -// 1. System命名空间 -using System; -using System.Collections.Generic; -using System.Linq; - -// 2. 第三方库 -using RoyT.AStar; - -// 3. Navisworks API -using Autodesk.Navisworks.Api; -using Autodesk.Navisworks.ComApi; -using Autodesk.Navisworks.Api.Plugins; - -// 4. 项目命名空间(按字母顺序) -using NavisworksTransport.Commands; -using NavisworksTransport.Core; -using NavisworksTransport.PathPlanning; -using NavisworksTransport.UI.WPF.ViewModels; -using NavisworksTransport.Utils; -``` - -### 命名约定 - -| 类型 | 命名规则 | 示例 | -|------|----------|------| -| 类 | PascalCase | `PathPlanningManager` | -| 接口 | PascalCase + 'I'前缀 | `IPathPlanningCommand` | -| 方法 | PascalCase | `GenerateGridMap` | -| 属性 | PascalCase | `CellSize` | -| 字段 | camelCase + 下划线前缀 | `_uiStateManager` | -| 常量 | PascalCase | `MaxHeightDiff` | -| 枚举 | PascalCase | `GridGenerationMode` | - -### 代码开发原则 - -#### 1. 禁止硬编码默认值 - -函数的参数和初始化时不要使用硬编码的默认值,应从配置读取或使用命名常量: - -```csharp -// ❌ 错误:硬编码默认值 -public GridMap Generate(double cellSize = 0.5) { } - -// ❌ 错误:构造函数中硬编码 -public PathPlanner() { - _cellSize = 0.5; - _maxSlope = 15.0; -} - -// ✅ 正确:从配置读取 -public GridMap Generate(double cellSize) { - cellSize = cellSize > 0 ? cellSize : ConfigManager.Instance.Current.PathEditing.CellSizeMeters; - // ... -} - -// ✅ 正确:使用命名常量 -private const double DEFAULT_CELL_SIZE_METERS = 0.5; -public PathPlanner(double cellSize = DEFAULT_CELL_SIZE_METERS) { } -``` - -#### 2. 不向后兼容 - -本项目专门针对 Navisworks 2026 开发,**程序中不要写向后兼容代码**: - -```csharp -// ❌ 错误:向后兼容代码 -#if NAVISWORKS_2025 - // 2025 specific code -#elif NAVISWORKS_2026 - // 2026 specific code -#endif - -// ❌ 错误:运行时版本检查 -if (NavisworksVersion.Major < 2026) { - // 兼容旧版本的代码 -} - -// ✅ 正确:直接针对2026编写 -var clashResult = Autodesk.Navisworks.Api.Clash.ClashResult.GetAllResults(); -``` - -#### 3. 不随意加回退逻辑 - -程序中不要随意加回退逻辑,避免过度防御性编程导致隐藏问题: - -```csharp -// ❌ 错误:过度回退逻辑 -public double GetCellSize() { - try { - return ConfigManager.Instance.Current.PathEditing.CellSizeMeters; - } - catch { - return 0.5; // 隐藏了配置读取失败的问题! - } -} - -// ❌ 错误:静默回退 -double value = GetConfigValue("cellSize"); -if (value <= 0) value = 0.5; // 为什么<=0?配置验证应该保证这一点 - -// ✅ 正确:让问题暴露出来 -public double GetCellSize() { - return ConfigManager.Instance.Current.PathEditing.CellSizeMeters; - // 如果配置有问题,让它抛出异常,在源头解决 -} -``` - -#### 4. 代码复用优先 - -规划和编写新功能或新模块时,**尽量复用项目中的代码,尤其是工具和辅助类**: - -```csharp -// ❌ 错误:重复造轮子 -public static double MyConvertUnits(double value) { - // 自己写一套单位转换逻辑... -} - -// ✅ 正确:复用现有的 UnitsConverter -using NavisworksTransport.Utils; -double meters = UnitsConverter.ConvertToMeters(distance); - -// ❌ 错误:自己实现集合通知 -public class MyCollection : ObservableCollection { - // 重写一大堆线程安全代码... -} - -// ✅ 正确:复用 ThreadSafeObservableCollection -using NavisworksTransport.UI.WPF.Collections; -var collection = new ThreadSafeObservableCollection(); - -// ❌ 错误:自己写几何计算 -public double Distance(Point3D a, Point3D b) { - return Math.Sqrt(Math.Pow(a.X - b.X, 2) + ...); -} - -// ✅ 正确:复用 GeometryHelper -using NavisworksTransport.Utils; -double distance = GeometryHelper.Distance(pointA, pointB); -``` - -**复用检查清单:** - -- 需要单位转换?→ 使用 `UnitsConverter` -- 需要几何计算?→ 使用 `GeometryHelper` -- 需要日志记录?→ 使用 `LogManager` -- 需要线程安全的集合?→ 使用 `ThreadSafeObservableCollection` -- 需要坐标转换?→ 使用 `CoordinateConverter` -- 需要路径相关工具?→ 使用 `PathHelper` - -#### 5. 临时定位代码必须回收 - -为定位问题临时加入的兜底调用、强制刷新、额外同步、绕过分支等代码,如果后续确认**不是真正根因**,在根因修复后必须恢复或删除,不能把这类临时补丁长期保留在正式代码中。 - -```csharp -// ❌ 错误:为掩盖问题长期保留的强制调用 -DoNormalUpdate(); -ForceRefreshAgain(); // 只是为了“看起来好了” - -// ✅ 正确:先定位根因,再移除临时补丁 -DoNormalUpdate(); -``` - -### 单位系统 - 极其重要 - -**所有网格地图和路径规划计算必须使用模型单位,严禁混用米制单位。** - -#### 变量命名铁律(强制执行) - -| 单位类型 | 命名规则 | 示例 | -|---------|---------|------| -| **米单位** | 变量名必须以 `InMeters` 结尾 | `lengthInMeters`, `heightInMeters` | -| **模型单位** | 变量名**无后缀** | `length`, `height`, `cellSize` | - -```csharp -// ✅ 正确:严格遵循命名规范 -public void SetSize(double lengthInMeters, double widthInMeters, double heightInMeters) -{ - double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(...); - - // 米单位 → 模型单位(无后缀) - double length = lengthInMeters * metersToUnits; - double width = widthInMeters * metersToUnits; - double height = heightInMeters * metersToUnits; - - // 后续计算使用模型单位(无后缀) - boundingBox = new BoundingBox3D(0, 0, 0, length, width, height); -} - -// ❌ 错误:命名不规范,导致单位混乱 -public void SetSize(double length, double width, double height) // 参数是米还是模型单位? -{ - double scale = length / baseSize; // 单位不明确! -} - -// ❌ 错误:后缀使用不一致 -public void SetSize(double lengthMeters, double widthMeters, double heightMeters) // 不要用Meters后缀 -{ - double lengthInModelUnits = lengthMeters * factor; // 不要用InModelUnits后缀 -} -``` - -#### 单位转换方法 - -`UnitsConverter` 提供以下方法: - -- `GetUnitsToMetersConversionFactor()` - 文档单位转米 -- `GetMetersToUnitsConversionFactor()` - 米转文档单位 -- `ConvertToMeters(distance)` - 转换距离为米 -- `ConvertFromMeters(distanceInMeters)` - 从米转换距离 - -#### 完整示例 - -```csharp -// ✅ 正确:在函数入口统一转换,严格遵循命名规范 -public GridMap GenerateFromBIM(BoundingBox3D bounds, double cellSizeInMeters, ...) -{ - double factor = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units); - double cellSize = cellSizeInMeters * factor; // 模型单位,无后缀 - // 后续所有计算使用 cellSize(模型单位) -} - -// ❌ 错误:混用单位 -private const double MAX_HEIGHT_DIFF = 0.35; // 这是米! -if (heightDiff > MAX_HEIGHT_DIFF) // 单位不匹配,严重Bug! -``` - -### 错误处理 - -```csharp -// ✅ 正确:记录并适当处理异常 -try -{ - var result = SomeOperation(); - return result; -} -catch (Exception ex) -{ - LogManager.Error($"Operation failed: {ex.Message}"); - throw; // 或适当处理 -} - -// ❌ 错误:静默吞掉异常 -try -{ - var result = SomeOperation(); - return result; -} -catch -{ - return null; // 隐藏了问题! -} -``` - -### 线程安全和UI更新 - -所有UI操作必须编组到主线程: - -```csharp -// ✅ 正确:使用UIStateManager进行线程安全更新 -_uiStateManager.UpdateStatus("正在处理..."); - -// ✅ 正确:异步事件触发避免死锁 -private void OnStatusChanged(string status) -{ - Task.Run(() => - { - try - { - StatusChanged?.Invoke(this, status); - } - catch (Exception ex) - { - LogManager.Error($"StatusChanged事件失败: {ex.Message}"); - } - }); -} -``` - -### WPF UI开发注意事项 - -#### XAML资源引用原则 - -**原则**:所有 XAML 中引用的资源(Converter、Style、Brush 等)必须在当前文件或合并的资源字典中有定义。 - -**约束**: - -- 禁止复制其他文件的 XAML 代码而不检查资源定义 -- 每添加一个 `{StaticResource xxx}` 引用,立即确认 `x:Key="xxx"` 存在 -- 小步增量开发,每步验证窗口可正常打开 - -**关键错误**:资源键名拼写错误、未定义的 Converter、错误的外部资源字典路径 - -**参考**:详细检查清单和常见错误模式见 `doc/guide/design_principles.md` 第16节 - -#### UI色彩规范 - Material Design - -**项目整体使用 Google Material Design 色系**,确保视觉一致性。 - -**常用颜色定义**(来自 `PathPointRenderPlugin.cs`): - -| 用途 | 颜色名称 | RGB值 | 十六进制 | -|------|---------|-------|---------| -| 起点 | Material Green | (76, 175, 80) | #4CAF50 | -| 通行空间 | Material Light Green | (129, 199, 132) | #81C784 | -| 排除对象 | Material Light Green | (129, 199, 132) | #81C784 | - -**其他高亮颜色**(来自 `ModelHighlightHelper.cs`): - -| 类别 | 颜色 | RGB值 | -|------|------|-------| -| 预计算碰撞 | Material Purple | (156, 39, 176) | -| 手工指定对象 | 橙色 | (255, 170, 0) | -| 动画物体 | Amber/Yellow | (255, 193, 7) | -| ClashDetective结果 | 红色 | Color.Red | -| 通道预览 | 绿色 | Color.Green | - -**使用规范**: - -1. 新增UI元素时优先使用上述Material色系 -2. 如需新颜色,参考 [Material Design Color Palette](https://material.io/resources/color/) -3. 使用 `Color.FromByteRGB(r, g, b)` 定义颜色(Navisworks API) -4. 保持透明度一致:通行空间类用 0.8-0.9,碰撞类用不透明 - -#### 对话框置顶原则 - -**原则**:在Navisworks插件环境中,所有模态对话框必须确保置顶显示,避免被主窗口遮挡。 - -**约束**: - -- 所有模态对话框必须在 XAML 中设置 `Topmost="True"` -- 使用 `DialogHelper` 工具类统一处理 Owner 设置(`src/Utils/DialogHelper.cs`) - -**参考**:详细方案见 `doc/guide/design_principles.md` 第15节、第17节 - -## 配置系统 - -配置文件使用 TOML 格式,默认配置位于 `default_config.toml`: - -```toml -[path_editing] -cell_size_meters = 0.5 # 网格单元大小(米) -max_height_diff_meters = 0.35 # 最大高度差(米) -object_length_meters = 1.5 # 物体长度 -object_width_meters = 1.0 # 物体宽度 -object_height_meters = 2.0 # 物体高度 -safety_margin_meters = 0.1 # 安全间隙 -default_path_turn_radius = 2.5 # 默认转弯半径 -arc_sampling_step = 0.05 # 圆弧采样步长 - -[visualization] -margin_ratio = 0.1 # 地图边距比例 - -[animation] -frame_rate = 30 # 动画帧率 -duration_seconds = 10.0 # 动画持续时间 -detection_tolerance_meters = 0.05 # 检测容差 -spatial_index_cell_size = 1.0 # 空间索引格子大小 - -[logistics] -traversable = true # 默认可通行性 -priority = 5 # 优先级(1-5) -height_limit_meters = 3.0 # 高度限制 -speed_limit_meters_per_second = 0.8 # 速度限制 -width_limit_meters = 3.0 # 宽度限制 -``` - -运行时通过 `ConfigManager.Instance.Current` 访问配置。 - -## 测试策略 - -### 测试项目结构 - -``` -UnitTests/ -├── Core/ -│ ├── PathCurveEngineTests.cs -│ └── UIStateManagerBasicTests.cs -├── Commands/ -│ └── CommandBaseTests.cs -├── Collections/ -│ └── ThreadSafeObservableCollectionBasicTests.cs -└── Utils/ - └── UnitsConverterTests.cs -``` - -### 测试框架 - -- **框架**: MSTest 3.0.4 -- **运行器**: VSTest.Console.exe -- **目标框架**: .NET Framework 4.8 - -### 编写测试 - -```csharp -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace NavisworksTransport.UnitTests -{ - [TestClass] - public class ExampleTests - { - [TestMethod] - public void TestMethod_ShouldDoSomething() - { - // Arrange - var input = "test"; - - // Act - var result = SomeMethod(input); - - // Assert - Assert.AreEqual("expected", result); - } - } -} -``` - -### 注意事项 - -- 独立测试:不依赖Navisworks环境,测试核心算法逻辑 -- 集成测试:需要完整的Navisworks 2026环境 -- 日志位置: `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\debug.log` - -## 开发工作流 - -### 添加新功能的标准流程 - -1. **定义接口**(如果需要) - - 在 `src/Commands/` 或 `src/Core/` 中定义接口 - -2. **实现核心逻辑** - - 业务逻辑放在 `src/Core/` 或 `src/PathPlanning/` - - 确保单位转换正确 - -3. **添加命令封装**(如果需要) - - 在 `src/Commands/` 中实现命令类 - - 继承 `CommandBase` 或实现 `IPathPlanningCommand` - -4. **更新UI** - - ViewModel 在 `src/UI/WPF/ViewModels/` - - View 在 `src/UI/WPF/Views/` - -5. **注册到主插件** - - 在 `MainPlugin.cs` 或相关管理器中集成 - -6. **添加测试** - - 在 `UnitTests/` 对应目录添加测试 - -### 调试技巧 - -1. **查看日志**: 日志文件位于 `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\` -2. **使用 LogManager**: 所有重要操作都应记录日志 -3. **Navisworks插件调试**: - - 附加到 `Roamer.exe` 进程 - - 使用 `LogManager.Debug()` 输出调试信息 - -## 常见问题 - -### 构建失败 - -- **错误**: "MSBuild not found" - - **解决**: 安装 Visual Studio 2022 或 Build Tools - -- **错误**: "无法找到 Autodesk.Navisworks.Api" - - **解决**: 安装 Navisworks Manage 2026 - -### 运行时错误 - -- **错误**: "单位不匹配导致的计算错误" - - **解决**: 检查 `UnitsConverter` 使用是否正确 - -- **错误**: "跨线程UI操作异常" - - **解决**: 使用 `UIStateManager` 或 `Dispatcher.Invoke` - -- **错误**: "插件未加载" - - **解决**: 检查插件是否部署到正确目录,检查依赖项是否存在 - -## 文档资源 - -- **API文档**: `doc/navisworks_api/NET/documentation/NET API.chm` -- **COM API文档**: `doc/navisworks_api/COM/documentation/NavisWorksCOM.chm` -- **设计原则**: `doc/guide/design_principles.md`(线程安全、对话框置顶等设计模式) -- **设计文档**: `doc/design/2026/` -- **架构设计**: `doc/architecture/` -- **迁移指南**: `doc/migration/`(2017到2026的API变更) - -## 版本信息 - -- **当前版本**: 2.0.0.0 -- **程序集**: TransportPlugin -- **作者**: Tian -- **版权**: Copyright © 2024 +1. `doc/working/current-engineering-state.md` +2. `doc/design/2026/coordinate-system-canonical-space-design.md` +3. `doc/design/2026/NavisworksAPI使用方法.md` --- -**注意**: 本插件专门针对 Navisworks 2026 开发,不考虑向后兼容。 +## 1. 项目现状 + +**NavisworksTransport** 是一个面向 **Autodesk Navisworks Manage 2026** 的物流路径规划与动画仿真插件。 + +当前已经不只是“自动寻路”项目,而是一套同时覆盖以下能力的工程: + +- 物流属性与对象分类 +- 地面路径、吊装路径、Rail 路径编辑与可视化 +- 终端安装仿真 +- 动画播放、起点落位、终点诊断 +- ClashDetective 碰撞检测与恢复 +- 路径、检测记录、批处理相关数据存储 + +### 当前重点功能 + +- `Ground / Hoisting / Rail` 三类路径 +- 真实物体与虚拟物体的起点摆放、动画姿态、通行空间 +- `YUp / ZUp` 两类宿主模型坐标系支持 +- 终端安装仿真中的: + - 端面三点分析 + - 光轴辅助线 + - 安装面双点确定 + - 安装点与 Rail 法向联动 + +--- + +## 2. 技术栈与构建 + +- 平台:Navisworks Manage 2026 +- 框架:.NET Framework 4.8 +- 语言:C# 7.3 +- 架构:x64 +- UI:WPF + Navisworks DockPane +- 测试:MSTest + +### 关键脚本 + +- `compile.bat` +- `run-unit-tests.bat` +- `deploy-plugin.bat` + +### 极重要的执行顺序 + +构建和部署必须严格按顺序执行: + +1. `./run-unit-tests.bat`(需要时) +2. `./compile.bat` +3. **等待编译完整结束并确认成功** +4. `./deploy-plugin.bat` + +不要并行执行编译和部署。否则很容易把旧 DLL 部署到插件目录。 + +### 插件部署目录 + +- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\` + +日志目录: + +- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\debug.log` + +--- + +## 3. 目录与职责 + +### 核心目录 + +- `src/Core/` + - 插件主入口、路径管理、动画、碰撞、渲染、配置 +- `src/UI/WPF/` + - 视图、ViewModel、交互命令 +- `src/Utils/` + - 单位、几何、坐标、变换、日志等公共工具 +- `src/PathPlanning/` + - 网格、A*、路径几何与优化 +- `UnitTests/` + - 数学层、工具层、姿态层的回归测试 + +### 当前最关键的文件 + +- `src/Core/Animation/PathAnimationManager.cs` +- `src/Core/VirtualObjectManager.cs` +- `src/Core/PathPointRenderPlugin.cs` +- `src/UI/WPF/ViewModels/AnimationControlViewModel.cs` +- `src/UI/WPF/ViewModels/PathEditingViewModel.cs` +- `src/Utils/CoordinateSystem/HostCoordinateAdapter.cs` +- `src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs` +- `src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs` +- `src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs` +- `src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs` +- `src/Utils/RailPathPoseHelper.cs` +- `src/Utils/ModelItemTransformHelper.cs` + +--- + +## 4. 当前稳定架构 + +### 4.1 坐标系三层语义 + +以后统一只使用这三种说法: + +- **宿主坐标系** + - Navisworks 文档坐标系 + - `YUp` 或 `ZUp` + - UI 输入输出、日志、拾取结果都按这一层解释 + +- **内部坐标系** + - 项目内部统一使用的 `Canonical Space` + - 固定 `ZUp` + - 纯数学姿态和几何计算优先在这里完成 + +- **资产坐标系** + - 只属于插件自带资源 + - 当前主要是: + - 虚拟物体 `unit_cube.nwc` + - 参考杆 `unit_cylinder.nwc` + +禁止再使用“本地坐标系”这种含糊说法。 + +### 4.2 真实物体 vs 虚拟物体 + +这是当前架构里最容易被改坏的地方。 + +#### 真实物体 + +- 没有独立资产坐标系 +- 可以视为直接生活在宿主坐标系里 +- 角度调整对话框里的 `X/Y/Z`,对真实物体应按**宿主坐标系**消费 + +#### 虚拟物体 + +- 有明确资产坐标系 +- 当前依赖 `unit_cube.nwc` 资源 +- 资源必须满足: + - 几何中心在原点 + - 原点处 `BoundingBox.Center == (0,0,0)` +- 如果生产环境里虚拟物体固定偏移,先检查部署的 `unit_cube.nwc` 是否为最新资源,不要先怀疑代码 + +### 4.3 路径姿态链 + +#### Ground / Hoisting + +- 走平面姿态链 +- 已禁止偷偷退回旧 `yaw` 方案 +- 起点、逐帧、终点、通行空间,必须共享同一套尺寸语义 + +#### Rail + +- 不能像平面路径那样在宿主空间随意补旋转 +- 必须并入 `canonical -> rail pose` 链 +- `Rail 0°` 基线必须稳定,不能被角度修正逻辑污染 + +### 4.4 通行空间与物体姿态的关系 + +通行空间、起点落位、逐帧位置、终点诊断,必须尽量共用同一套尺寸/法线语义。 + +典型错误信号: + +- 通行空间正确,但物体陷入地面 +- 物体姿态正确,但通行空间轴搞反 +- `YUp` 下 `Y/Z` 表现互换 + +遇到这类问题,优先检查: + +- 是否一条链用了“原始高度” +- 另一条链用了“旋转后法线尺寸” + +### 4.5 终端安装仿真 + +当前稳定链路是: + +1. 捕获终点箱体 +2. 分析端面(3点) +3. 选择安装点(当前已是 2 点确定安装面) +4. 生成辅助线 / 安装面 / 安装点 +5. 取起点并生成路径 + +当前 UI 上: + +- 终端安装仿真是独立区块 +- 不再夹在路径编辑中间 +- 安装方式选择只保留一处 + +--- + +## 5. 开发原则 + +### 5.1 不向后兼容 + +项目只针对 Navisworks 2026。不要写旧版本兼容代码。 + +### 5.2 不要随意加 fallback + +不要为了“先跑起来”就: + +- 偷偷退回旧 `yaw` +- 偷偷用硬编码 `Z-up` +- 偷偷在错误时给默认值掩盖问题 + +如果完整姿态链失败,应优先暴露问题并修根因。 + +### 5.3 临时补丁不是正式实现 + +为定位问题临时加入的: + +- 强制刷新 +- 额外同步 +- 再调一次方法 +- UI 和稀泥补丁 + +如果最后证明它不是真正根因,修完后必须删掉,不能残留在正式代码里。 + +### 5.4 优先复用现有工具 + +尤其优先看: + +- `UnitsConverter` +- `GeometryHelper` +- `LogManager` +- `HostCoordinateAdapter` +- `Canonical*` 姿态工具 +- `ModelItemTransformHelper` +- `RailPathPoseHelper` + +不要在业务层手搓一套新的矩阵、坐标变换或尺寸投影公式。 + +### 5.5 测试优先于猜测 + +对几何/旋转/坐标问题,优先顺序应是: + +1. 看日志 +2. 日志不够就补日志 +3. 先补单元测试 +4. 再改代码 + +不要在没有锁住语义之前反复试错改实现。 + +--- + +## 6. 单位原则 + +所有路径计算、网格计算、包络尺寸、位移偏移,内部一律使用**模型单位**。 + +命名规则: + +- 米单位:变量名以 `InMeters` 结尾 +- 模型单位:变量名不加后缀 + +不要混用。 + +优先使用: + +- `UnitsConverter.GetMetersToUnitsConversionFactor()` +- `UnitsConverter.GetUnitsToMetersConversionFactor()` +- `UnitsConverter.ConvertToMeters(...)` +- `UnitsConverter.ConvertFromMeters(...)` + +--- + +## 7. 常见问题的排查入口 + +### 7.1 虚拟物体固定偏差 + +先查: + +1. 部署目录下的 `resources\\unit_cube.nwc` +2. 原点处 `BoundingBox.Center` 是否是 `(0,0,0)` +3. 再查起点/归位代码 + +### 7.2 真实物体旋转轴不对 + +先区分: + +- 目标姿态算错 +- 还是 Navisworks 应用姿态错 + +优先看: + +- `[动画姿态入口]` +- `[模型增量姿态]` + +### 7.3 吊装路径不显示 + +优先检查渲染链里是否有退化段/零长度段导致整条渲染失败。 + +### 7.4 “设为终点直接结束”后列表还是空 + +优先看 `UIStateManager` 队列消费是否吃掉了后续 UI 事件,不要先怀疑坐标系。 + +--- + +## 8. 资源与部署注意事项 + +### 8.1 虚拟物体资源 + +当前部署脚本会部署: + +- `resources\\unit_cube.nwc` +- `resources\\unit_cylinder.nwc` + +虚拟物体和参考杆问题,必须同时检查: + +- 仓库里的资源 +- `bin\\x64\\Release\\resources` +- 插件部署目录下的 `resources` + +### 8.2 deploy-plugin.bat 当前规则 + +- 走白名单部署 +- 不部署测试 DLL +- 不部署 Navisworks 自带 API DLL + +不要把它改回“复制所有 dll”。 + +--- + +## 9. 推荐阅读顺序 + +新会话接手本项目时,推荐顺序: + +1. 本文件 `AGENTS.md` +2. `doc/working/current-engineering-state.md` +3. `doc/design/2026/coordinate-system-canonical-space-design.md` +4. `doc/design/2026/NavisworksAPI使用方法.md` +5. 相关专项 skill: + - `.agents/skills/nw-api/SKILL.md` + - `.agents/skills/geometry-transform/SKILL.md` + +--- + +## 10. 当前对子代理和 skill 的约定 + +仓库中已经有几何/变换专项 skill: + +- `.agents/skills/geometry-transform/SKILL.md` + +它负责沉淀以下内容: + +- 坐标系变换 +- 物体姿态 +- 物体位移与归位 +- 虚拟物体资源定位 +- 通行空间几何 +- Navisworks 变换 API 使用规则 + +后续凡是几何/旋转/位移问题,优先沿这套 skill 与工具链继续维护,不要重新发明一套术语和流程。 + +--- + +## 11. 最后的硬约束 + +1. 不要混淆宿主坐标系、内部坐标系、资产坐标系 +2. 不要在宿主空间随意补旋转,先判断是否应并入现有姿态链 +3. 不要让虚拟物体和真实物体共享含糊的状态分支 +4. 不要让通行空间和真实物体使用两套不同的尺寸语义 +5. 不要把临时补丁留在正式实现里 +6. 不要绕过测试直接改几何核心逻辑 + +如果你要改动: + +- `PathAnimationManager` +- `VirtualObjectManager` +- `PathPointRenderPlugin` +- `AnimationControlViewModel` +- `HostCoordinateAdapter` +- `Canonical*PoseBuilder` +- `ModelItemTransformHelper` + +请默认这是高风险改动,先补测试,再动实现。 diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index d79b769..dcde989 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -59,9 +59,11 @@ + + diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 0b5454e..f515279 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -340,6 +340,8 @@ + + diff --git a/UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs b/UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs new file mode 100644 index 0000000..6efb96f --- /dev/null +++ b/UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs @@ -0,0 +1,123 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class FragmentRepresentativePoseHelperTests + { + [TestMethod] + public void ShouldAverageFragmentMatrices_AndIgnoreTranslation() + { + Quaternion expectedRotation = Quaternion.Normalize( + Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)(Math.PI / 6.0))); + + double[] fragmentMatrix1 = CreateFragmentMatrix(expectedRotation, new Vector3(10.0f, 20.0f, 30.0f)); + double[] fragmentMatrix2 = CreateFragmentMatrix(expectedRotation, new Vector3(-3.0f, 8.0f, 1.0f)); + + bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeRotation( + new[] { fragmentMatrix1, fragmentMatrix2 }, + out Quaternion representativeRotation); + + Assert.IsTrue(ok); + AssertVector( + Vector3.Transform(Vector3.UnitX, representativeRotation), + Vector3.Transform(Vector3.UnitX, expectedRotation), + 1e-4); + AssertVector( + Vector3.Transform(Vector3.UnitY, representativeRotation), + Vector3.Transform(Vector3.UnitY, expectedRotation), + 1e-4); + AssertVector( + Vector3.Transform(Vector3.UnitZ, representativeRotation), + Vector3.Transform(Vector3.UnitZ, expectedRotation), + 1e-4); + } + + [TestMethod] + public void ShouldIgnoreQuaternionSignWhenAveragingFragments() + { + Quaternion expectedRotation = Quaternion.Normalize( + Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)(Math.PI / 4.0))); + Quaternion oppositeSignRotation = new Quaternion( + -expectedRotation.X, + -expectedRotation.Y, + -expectedRotation.Z, + -expectedRotation.W); + + bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeRotation( + new[] { expectedRotation, oppositeSignRotation }, + out Quaternion representativeRotation); + + Assert.IsTrue(ok); + AssertVector( + Vector3.Transform(Vector3.UnitY, representativeRotation), + Vector3.Transform(Vector3.UnitY, expectedRotation), + 1e-4); + AssertVector( + Vector3.Transform(Vector3.UnitZ, representativeRotation), + Vector3.Transform(Vector3.UnitZ, expectedRotation), + 1e-4); + } + + [TestMethod] + public void ShouldFail_WhenNoFragmentRotationsAreAvailable() + { + bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeRotation( + Array.Empty(), + out Quaternion representativeRotation); + + Assert.IsFalse(ok); + Assert.AreEqual(Quaternion.Identity, representativeRotation); + } + + [TestMethod] + public void ShouldPreserveAxes_ForObservedYUpRealObjectFragmentBasis() + { + Vector3 axisX = new Vector3(1.0f, 0.0f, 0.0f); + Vector3 axisY = new Vector3(0.0f, 0.0f, 1.0f); + Vector3 axisZ = new Vector3(0.0f, -1.0f, 0.0f); + + double[] fragmentMatrix = + { + axisX.X, axisX.Y, axisX.Z, 0.0, + axisY.X, axisY.Y, axisY.Z, 0.0, + axisZ.X, axisZ.Y, axisZ.Z, 0.0, + 0.0, 0.0, 0.0, 1.0 + }; + + bool ok = FragmentRepresentativePoseHelper.TryExtractRotationFromFragmentMatrix( + fragmentMatrix, + out Quaternion rotation); + + Assert.IsTrue(ok); + AssertVector(Vector3.Transform(Vector3.UnitX, rotation), axisX, 1e-4); + AssertVector(Vector3.Transform(Vector3.UnitY, rotation), axisY, 1e-4); + AssertVector(Vector3.Transform(Vector3.UnitZ, rotation), axisZ, 1e-4); + } + + private static double[] CreateFragmentMatrix(Quaternion rotation, Vector3 translation) + { + Vector3 axisX = Vector3.Transform(Vector3.UnitX, rotation); + Vector3 axisY = Vector3.Transform(Vector3.UnitY, rotation); + Vector3 axisZ = Vector3.Transform(Vector3.UnitZ, rotation); + + return new[] + { + (double)axisX.X, (double)axisX.Y, (double)axisX.Z, 0.0, + (double)axisY.X, (double)axisY.Y, (double)axisY.Z, 0.0, + (double)axisZ.X, (double)axisZ.Y, (double)axisZ.Z, 0.0, + (double)translation.X, (double)translation.Y, (double)translation.Z, 1.0 + }; + } + + private static void AssertVector(Vector3 actual, Vector3 expected, double tolerance) + { + Assert.AreEqual(expected.X, actual.X, tolerance); + Assert.AreEqual(expected.Y, actual.Y, tolerance); + Assert.AreEqual(expected.Z, actual.Z, tolerance); + } + } +} diff --git a/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs new file mode 100644 index 0000000..a9679c0 --- /dev/null +++ b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs @@ -0,0 +1,191 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System; +using System.Numerics; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class RealObjectPlanarPoseSolverTests + { + [TestMethod] + public void ShouldChooseClosestCandidateAxis_FromRotatedReferencePose() + { + Quaternion referenceRotation = Quaternion.Normalize( + Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)(Math.PI / 6.0))); + + Vector3 desiredForward = new Vector3(1.0f, 0.2f, 0.1f); + Vector3 desiredUp = Vector3.UnitZ; + + bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose( + referenceRotation, + desiredForward, + desiredUp, + out RealObjectPlanarPoseSolution solution); + + Assert.IsTrue(ok); + AssertVector(solution.SelectedReferenceAxisLocal, 1.0, 0.0, 0.0); + AssertVector(solution.ProjectedForward, 0.97590, 0.19518, 0.09759, 1e-4); + + Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation); + AssertVector(transformedSelectedAxis, solution.ProjectedForward.X, solution.ProjectedForward.Y, solution.ProjectedForward.Z, 1e-4); + } + + [TestMethod] + public void ShouldPreferNegativeAxisWhenItMatchesForwardBest() + { + Quaternion referenceRotation = Quaternion.Identity; + Vector3 desiredForward = new Vector3(-0.1f, -1.0f, 0.05f); + Vector3 desiredUp = Vector3.UnitZ; + + bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose( + referenceRotation, + desiredForward, + desiredUp, + out RealObjectPlanarPoseSolution solution); + + Assert.IsTrue(ok); + AssertVector(solution.SelectedReferenceAxisLocal, 0.0, -1.0, 0.0); + + Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation); + AssertVector(transformedSelectedAxis, solution.ProjectedForward.X, solution.ProjectedForward.Y, solution.ProjectedForward.Z, 1e-4); + } + + [TestMethod] + public void ShouldRotateWholeReferencePose_NotFlattenItToHostUpPlane() + { + Quaternion referenceRotation = Quaternion.Identity; + Vector3 desiredForward = Vector3.Normalize(new Vector3(1.0f, 1.0f, 1.0f)); + + bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose( + referenceRotation, + desiredForward, + Vector3.UnitY, + out RealObjectPlanarPoseSolution solution); + + Assert.IsTrue(ok); + AssertVector(solution.SelectedReferenceAxisLocal, 1.0, 0.0, 0.0); + + Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation); + AssertVector(transformedSelectedAxis, desiredForward.X, desiredForward.Y, desiredForward.Z, 1e-4); + } + + [TestMethod] + public void ShouldPreserveReferenceOrthogonalityByApplyingSingleRotationDelta() + { + Quaternion referenceRotation = Quaternion.Normalize( + Quaternion.CreateFromYawPitchRoll( + (float)(Math.PI / 7.0), + (float)(Math.PI / 9.0), + (float)(Math.PI / 11.0))); + + Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.3f, 0.8f, 0.52f)); + + bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose( + referenceRotation, + desiredForward, + Vector3.UnitZ, + out RealObjectPlanarPoseSolution solution); + + Assert.IsTrue(ok); + + Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation); + AssertVector(transformedSelectedAxis, desiredForward.X, desiredForward.Y, desiredForward.Z, 1e-4); + + Vector3 transformedReferenceUp = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, solution.BaselineRotation)); + Assert.AreEqual(1.0, transformedReferenceUp.Length(), 1e-4); + Assert.AreEqual(0.0, Vector3.Dot(transformedSelectedAxis, transformedReferenceUp), 1e-4); + } + + [TestMethod] + public void YUp_HorizontalRealObject_ToAnotherHorizontalDirection_ShouldKeepVerticalAxis() + { + Vector3 referenceX = new Vector3(1.0f, 0.0f, 0.0f); + Vector3 referenceY = new Vector3(0.0f, 0.0f, 1.0f); + Vector3 referenceZ = new Vector3(0.0f, -1.0f, 0.0f); + Quaternion referenceRotation = CreateQuaternionFromBasis(referenceX, referenceY, referenceZ); + + Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f)); + Vector3 desiredUp = Vector3.UnitY; + + bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose( + referenceRotation, + desiredForward, + desiredUp, + out RealObjectPlanarPoseSolution solution); + + Assert.IsTrue(ok); + AssertVector(solution.SelectedReferenceAxisLocal, -1.0, 0.0, 0.0); + + Vector3 transformedForward = Vector3.Normalize( + Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation)); + Vector3 transformedVertical = Vector3.Normalize( + Vector3.Transform(-Vector3.UnitZ, solution.BaselineRotation)); + + AssertVector(transformedForward, desiredForward.X, desiredForward.Y, desiredForward.Z, 1e-4); + AssertVector(transformedVertical, 0.0, 1.0, 0.0, 1e-4); + } + + private static Quaternion CreateQuaternionFromBasis(Vector3 axisX, Vector3 axisY, Vector3 axisZ) + { + float m00 = axisX.X; + float m01 = axisY.X; + float m02 = axisZ.X; + float m10 = axisX.Y; + float m11 = axisY.Y; + float m12 = axisZ.Y; + float m20 = axisX.Z; + float m21 = axisY.Z; + float m22 = axisZ.Z; + + float trace = m00 + m11 + m22; + float qx; + float qy; + float qz; + float qw; + + if (trace > 0.0f) + { + float s = (float)Math.Sqrt(trace + 1.0f) * 2.0f; + qw = 0.25f * s; + qx = (m21 - m12) / s; + qy = (m02 - m20) / s; + qz = (m10 - m01) / s; + } + else if (m00 > m11 && m00 > m22) + { + float s = (float)Math.Sqrt(1.0f + m00 - m11 - m22) * 2.0f; + qw = (m21 - m12) / s; + qx = 0.25f * s; + qy = (m01 + m10) / s; + qz = (m02 + m20) / s; + } + else if (m11 > m22) + { + float s = (float)Math.Sqrt(1.0f + m11 - m00 - m22) * 2.0f; + qw = (m02 - m20) / s; + qx = (m01 + m10) / s; + qy = 0.25f * s; + qz = (m12 + m21) / s; + } + else + { + float s = (float)Math.Sqrt(1.0f + m22 - m00 - m11) * 2.0f; + qw = (m10 - m01) / s; + qx = (m02 + m20) / s; + qy = (m12 + m21) / s; + qz = 0.25f * s; + } + + return Quaternion.Normalize(new Quaternion(qx, qy, qz, qw)); + } + + private static void AssertVector(Vector3 vector, double x, double y, double z, double tolerance = 1e-6) + { + Assert.AreEqual(x, vector.X, tolerance); + Assert.AreEqual(y, vector.Y, tolerance); + Assert.AreEqual(z, vector.Z, tolerance); + } + } +} diff --git a/resources/default_config.toml b/resources/default_config.toml index 96529e8..da3e2b4 100644 --- a/resources/default_config.toml +++ b/resources/default_config.toml @@ -65,16 +65,16 @@ speed_limit_meters_per_second = 0.8 width_limit_meters = 3.0 [coordinate_system] -# 坐标系类型 -# 可选值: "AutoDetect", "ZUp", "YUp" -# AutoDetect: 自动检测(推荐) -# ZUp: 强制使用 Z-Up 坐标系(Navisworks 默认) -# YUp: 强制使用 Y-Up 坐标系(常见于 Revit 导出) -type = "AutoDetect" - -# 自定义物流类别 -# 用于扩展内置类别,只需填写类别名称 -# + # 坐标系类型 + # 可选值: "AutoDetect", "ZUp", "YUp" + # AutoDetect: 自动检测(推荐) + # ZUp: 强制使用 Z-Up 坐标系(Navisworks 默认) + # YUp: 强制使用 Y-Up 坐标系(常见于 Revit 导出) + type = "AutoDetect" + + # 自定义物流类别 + # 用于扩展内置类别,只需填写类别名称 + # # 示例: # [[custom_category]] # name = "狭窄通道" diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 7e0fb20..2d24ba7 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -2,10 +2,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; using System.Text; +using System.Windows; using System.Windows.Threading; using Autodesk.Navisworks.Api; using Autodesk.Navisworks.Api.Clash; +using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge; using NavisworksTransport.Core.Config; using NavisworksTransport.Core.Spatial; using NavisworksTransport.Utils; @@ -107,6 +110,8 @@ namespace NavisworksTransport.Core.Animation private double _realObjectLength = 0; // 真实物体长度(模型单位,固定物理尺寸) private double _realObjectWidth = 0; // 真实物体宽度(模型单位,固定物理尺寸) private double _realObjectHeight = 0; // 真实物体高度(模型单位,固定物理尺寸) + private Quaternion _realObjectReferenceRotation = Quaternion.Identity; + private bool _hasRealObjectReferenceRotation = false; private List _pathPoints; private List _manualCollisionTargets = new List(); private bool _manualCollisionOverrideEnabled = false; @@ -474,6 +479,7 @@ namespace NavisworksTransport.Core.Animation _animatedObjectMode = animatedObject == null ? AnimatedObjectMode.None : AnimatedObjectMode.RealObject; + ResetRealObjectReferenceRotation(); if (animatedObject != null) { @@ -483,6 +489,7 @@ namespace NavisworksTransport.Core.Animation _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform); _trackedRotation = _originalTransform.Factor().Rotation; _hasTrackedRotation = true; + TryCaptureRealObjectReferenceRotation(animatedObject, out _); } } @@ -495,6 +502,10 @@ namespace NavisworksTransport.Core.Animation _animatedObjectMode = isVirtualObject ? AnimatedObjectMode.VirtualObject : (_animatedObject != null ? AnimatedObjectMode.RealObject : AnimatedObjectMode.None); + if (isVirtualObject) + { + ResetRealObjectReferenceRotation(); + } _virtualObjectLength = length; // 模型单位 _virtualObjectWidth = width; // 模型单位 _virtualObjectHeight = height; // 模型单位 @@ -636,6 +647,7 @@ namespace NavisworksTransport.Core.Animation _animatedObjectMode = isVirtualObject ? AnimatedObjectMode.VirtualObject : AnimatedObjectMode.RealObject; + ResetRealObjectReferenceRotation(); _originalTransform = animatedObject.Transform; _originalCenter = animatedObject.BoundingBox().Center; _trackedPosition = GetTrackedObjectPosition(animatedObject); @@ -645,6 +657,11 @@ namespace NavisworksTransport.Core.Animation _trackedRotation = _originalTransform.Factor().Rotation; _hasTrackedRotation = true; + if (!isVirtualObject) + { + TryCaptureRealObjectReferenceRotation(animatedObject, out _); + } + LogManager.Info( $"[移动到起点] 更新动画对象内部状态: 对象={animatedObject.DisplayName}, 虚拟物体={IsVirtualObjectMode}"); } @@ -3571,6 +3588,17 @@ namespace NavisworksTransport.Core.Animation rotation = Rotation3D.Identity; var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + if (IsRealObjectMode) + { + Vector3 hostForward = ToNumerics(nextPoint) - ToNumerics(previousPoint); + if (hostForward.LengthSquared() < 1e-6f) + { + hostForward = ToNumerics(nextPoint) - ToNumerics(currentPoint); + } + + return TryCreateRealObjectPlanarRotationFromHostForward(hostForward, out rotation); + } + var convention = GetCurrentModelAxisConvention(); Vector3 canonicalPrevious = ToNumerics(adapter.ToCanonicalPoint(previousPoint)); Vector3 canonicalCurrent = ToNumerics(adapter.ToCanonicalPoint(currentPoint)); @@ -3582,24 +3610,6 @@ namespace NavisworksTransport.Core.Animation forward = canonicalNext - canonicalCurrent; } - if (IsRealObjectMode) - { - if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( - forward, - HostCoordinateAdapter.CanonicalUpVector3, - convention, - out var baselineQuaternion)) - { - return false; - } - - Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(Matrix4x4.CreateFromQuaternion(baselineQuaternion)); - Quaternion hostBaselineQuaternion = Quaternion.CreateFromRotationMatrix(hostLinear); - Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(hostBaselineQuaternion, _objectRotationCorrection); - rotation = adapter.FromHostQuaternion(hostComposedQuaternion); - return true; - } - Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection); if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( forward, @@ -3620,26 +3630,15 @@ namespace NavisworksTransport.Core.Animation rotation = Rotation3D.Identity; var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); - var convention = GetCurrentModelAxisConvention(); - Vector3 canonicalForward = adapter.ToCanonicalVector3(new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z)); if (IsRealObjectMode) { - if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( - canonicalForward, - HostCoordinateAdapter.CanonicalUpVector3, - convention, - out var baselineQuaternion)) - { - return false; - } - - Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(Matrix4x4.CreateFromQuaternion(baselineQuaternion)); - Quaternion hostBaselineQuaternion = Quaternion.CreateFromRotationMatrix(hostLinear); - Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(hostBaselineQuaternion, _objectRotationCorrection); - rotation = adapter.FromHostQuaternion(hostComposedQuaternion); - return true; + return TryCreateRealObjectPlanarRotationFromHostForward( + new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z), + out rotation); } + var convention = GetCurrentModelAxisConvention(); + Vector3 canonicalForward = adapter.ToCanonicalVector3(new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z)); Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection); if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( canonicalForward, @@ -3655,6 +3654,174 @@ namespace NavisworksTransport.Core.Animation return true; } + private bool TryCreateRealObjectPlanarRotationFromHostForward(Vector3 hostForward, out Rotation3D rotation) + { + rotation = Rotation3D.Identity; + + if (!TryGetRealObjectReferenceRotation(out var referenceRotation)) + { + return false; + } + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + if (!RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose( + referenceRotation, + hostForward, + adapter.HostUpVector3, + out var solution)) + { + return false; + } + + Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(solution.BaselineRotation, _objectRotationCorrection); + rotation = adapter.FromHostQuaternion(hostComposedQuaternion); + + LogManager.Info( + $"[真实物体起点姿态] 选中参考轴=({solution.SelectedReferenceAxisLocal.X:F3},{solution.SelectedReferenceAxisLocal.Y:F3},{solution.SelectedReferenceAxisLocal.Z:F3}), " + + $"投影前进=({solution.ProjectedForward.X:F3},{solution.ProjectedForward.Y:F3},{solution.ProjectedForward.Z:F3}), " + + $"基姿态已计算"); + return true; + } + + private bool TryGetRealObjectReferenceRotation(out Quaternion referenceRotation) + { + referenceRotation = Quaternion.Identity; + + if (_hasRealObjectReferenceRotation) + { + referenceRotation = _realObjectReferenceRotation; + return true; + } + + if (_originalTransform == null) + { + return false; + } + + try + { + if (TryCaptureRealObjectReferenceRotation(_animatedObject, out referenceRotation)) + { + return true; + } + + NotifyRealObjectReferencePoseFailure(_animatedObject); + return false; + } + catch (Exception ex) + { + LogManager.Error($"[真实物体参考姿态] 读取原始参考位姿失败: {ex.Message}"); + return false; + } + } + + private void NotifyRealObjectReferencePoseFailure(ModelItem sourceObject) + { + string objectName = sourceObject?.DisplayName ?? "未命名对象"; + string message = + $"无法从 fragment 提取真实物体参考姿态,已中止起点姿态计算。\n\n" + + $"对象:{objectName}\n" + + $"建议:\n" + + $"1. 检查该真实物体是否存在可用的 fragment 几何。\n" + + $"2. 重新选择该对象后再试。\n" + + $"3. 如果是 Revit 导入件,请确认 fragment 结构没有异常简化或丢失。"; + + LogManager.Error($"[真实物体参考姿态] {objectName} fragment 代表姿态提取失败,已中止。"); + DialogHelper.ShowMessageBox( + message, + "fragment 代表姿态提取失败", + MessageBoxButton.OK, + MessageBoxImage.Error); + } + + private bool TryCaptureRealObjectReferenceRotation(ModelItem sourceObject, out Quaternion referenceRotation) + { + referenceRotation = Quaternion.Identity; + + if (sourceObject == null) + { + return false; + } + + try + { + var selection = ComApiBridge.ToInwOpSelection(new ModelItemCollection { sourceObject }); + var fragmentInfos = GeometryHelper.GetAllFragments(selection); + try + { + var fragmentMatrices = fragmentInfos + .Where(fragmentInfo => fragmentInfo.TransformMatrix != null && fragmentInfo.TransformMatrix.Length == 16) + .Select(fragmentInfo => fragmentInfo.TransformMatrix) + .ToList(); + + if (fragmentMatrices.Count == 0) + { + LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} 未提取到 fragment 代表姿态"); + return false; + } + + if (!FragmentRepresentativePoseHelper.TryGetRepresentativeRotation(fragmentMatrices, out referenceRotation)) + { + LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} fragment 代表姿态求解失败"); + return false; + } + + _realObjectReferenceRotation = referenceRotation; + _hasRealObjectReferenceRotation = true; + + Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(referenceRotation); + LogManager.Info( + $"[真实物体参考姿态] {sourceObject.DisplayName} 使用 fragment 代表姿态: " + + $"X=({linear.M11:F4},{linear.M21:F4},{linear.M31:F4}), " + + $"Y=({linear.M12:F4},{linear.M22:F4},{linear.M32:F4}), " + + $"Z=({linear.M13:F4},{linear.M23:F4},{linear.M33:F4}), " + + $"fragment数量={fragmentMatrices.Count}"); + return true; + } + finally + { + if (fragmentInfos != null) + { + foreach (var fragmentInfo in fragmentInfos) + { + if (fragmentInfo?.Fragment != null) + { + try + { + Marshal.ReleaseComObject(fragmentInfo.Fragment); + } + catch + { + } + } + } + } + + if (selection != null) + { + try + { + Marshal.ReleaseComObject(selection); + } + catch + { + } + } + } + } + catch (Exception ex) + { + LogManager.Warning($"[真实物体参考姿态] 从 fragment 提取代表姿态失败: {ex.Message}"); + return false; + } + } + + private void ResetRealObjectReferenceRotation() + { + _realObjectReferenceRotation = Quaternion.Identity; + _hasRealObjectReferenceRotation = false; + } + /// /// 设置物体绕宿主 X/Y/Z 轴的角度修正 /// diff --git a/src/Core/Config/SystemConfig.cs b/src/Core/Config/SystemConfig.cs index c45e4e2..024d340 100644 --- a/src/Core/Config/SystemConfig.cs +++ b/src/Core/Config/SystemConfig.cs @@ -278,5 +278,6 @@ namespace NavisworksTransport.Core.Config /// YUp: 强制使用 Y-Up 坐标系 /// public string Type { get; set; } = "AutoDetect"; + } } diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index a06c3d2..e6b5efd 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -142,6 +142,22 @@ namespace NavisworksTransport /// 吊装路径样式(紫色) /// HoistingLine + , + + /// + /// 真实物体原始位姿 X 轴样式(红色) + /// + TransformAxisX, + + /// + /// 真实物体原始位姿 Y 轴样式(绿色) + /// + TransformAxisY, + + /// + /// 真实物体原始位姿 Z 轴样式(蓝色) + /// + TransformAxisZ } /// @@ -2379,6 +2395,15 @@ namespace NavisworksTransport case RenderStyleName.HoistingLine: return new RenderStyle(Color.FromByteRGB(156, 39, 176), 0.8); // Material Purple吊装路径,20%透明 + case RenderStyleName.TransformAxisX: + return new RenderStyle(Color.FromByteRGB(244, 67, 54), 0.9); // Material Red + + case RenderStyleName.TransformAxisY: + return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.9); // Material Green + + case RenderStyleName.TransformAxisZ: + return new RenderStyle(Color.FromByteRGB(33, 150, 243), 0.9); // Material Blue + default: return new RenderStyle(Color.White, 1.0); // 默认白色,完全不透明 } diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs index f24006e..432b116 100644 --- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs @@ -1,9 +1,15 @@ using System; using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Windows.Input; using System.Windows.Threading; +using Autodesk.Navisworks.Api; +using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge; using NavisworksTransport.UI.WPF.Collections; using NavisworksTransport.Core; using NavisworksTransport.Core.Config; @@ -21,6 +27,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels #region 私有字段 private readonly UIStateManager _uiStateManager; + private readonly Dictionary _documentFragmentDefaultUpAxes = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _documentRotationHintShown = new HashSet(StringComparer.OrdinalIgnoreCase); // 日志管理字段 private ObservableCollection _logLevels; @@ -174,10 +182,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand TestVoxelPathFindingCommand { get; private set; } public ICommand ReadTransformTestCommand { get; private set; } public ICommand CoordinateSystemExplorerCommand { get; private set; } + public ICommand CaptureRealObjectTransformCommand { get; private set; } + public ICommand DetectFragmentDefaultUpCommand { get; private set; } // 坐标系设置 public ObservableCollection CoordinateSystemOptions { get; private set; } + public ObservableCollection FragmentDefaultUpAxisOptions { get; private set; } private string _selectedCoordinateSystem = "AutoDetect"; public string SelectedCoordinateSystem @@ -199,6 +210,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels set => SetProperty(ref _currentCoordinateSystemInfo, value); } + private string _selectedFragmentDefaultUpAxis = "Y"; + public string SelectedFragmentDefaultUpAxis + { + get => _selectedFragmentDefaultUpAxis; + set + { + if (SetProperty(ref _selectedFragmentDefaultUpAxis, value)) + { + SaveCurrentDocumentFragmentDefaultUpAxis(value); + UpdateMainStatus($"当前文档 fragment 默认Up已设置为: {value}"); + LogManager.Info($"[fragment默认Up] 当前文档设置为: {value}"); + } + } + } + #endregion #region 构造函数 @@ -311,14 +337,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels TestVoxelPathFindingCommand = new RelayCommand(() => ExecuteTestVoxelPathFinding()); ReadTransformTestCommand = new RelayCommand(() => ExecuteReadTransformTest()); CoordinateSystemExplorerCommand = new RelayCommand(() => ExecuteCoordinateSystemExplorer()); + CaptureRealObjectTransformCommand = new RelayCommand(() => ExecuteCaptureRealObjectTransform()); + DetectFragmentDefaultUpCommand = new RelayCommand(() => ExecuteDetectFragmentDefaultUp()); // 初始化坐标系选项 CoordinateSystemOptions = new ObservableCollection { "AutoDetect", "ZUp", "YUp" }; + FragmentDefaultUpAxisOptions = new ObservableCollection { "Y", "Z" }; // 从配置加载当前坐标系设置 var configType = ConfigManager.Instance.Current.CoordinateSystem?.Type ?? "AutoDetect"; _selectedCoordinateSystem = configType; + _selectedFragmentDefaultUpAxis = GetCurrentDocumentFragmentDefaultUpAxis(); // 订阅文档事件(用于自动检测坐标系) SubscribeToDocumentEvents(); @@ -389,6 +419,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 更新显示信息 UpdateCoordinateSystemInfo(); + ShowRootModelRotationHintIfNeeded(); LogManager.Info($"坐标系初始化完成: {configType}"); } @@ -436,6 +467,156 @@ namespace NavisworksTransport.UI.WPF.ViewModels { var info = CoordinateSystemManager.Instance.GetCurrentInfo(); CurrentCoordinateSystemInfo = $"当前坐标系: {info}"; + RefreshFragmentDefaultUpAxisSelectionFromCurrentDocument(); + } + + private void RefreshFragmentDefaultUpAxisSelectionFromCurrentDocument() + { + string resolvedAxis = GetCurrentDocumentFragmentDefaultUpAxis(); + if (_selectedFragmentDefaultUpAxis != resolvedAxis) + { + SetProperty(ref _selectedFragmentDefaultUpAxis, resolvedAxis, nameof(SelectedFragmentDefaultUpAxis)); + } + } + + private string GetCurrentDocumentFragmentDefaultUpAxis() + { + string documentKey = GetCurrentDocumentKey(); + if (!string.IsNullOrWhiteSpace(documentKey) && + _documentFragmentDefaultUpAxes.TryGetValue(documentKey, out string storedAxis) && + IsSupportedFragmentDefaultUpAxis(storedAxis)) + { + return storedAxis; + } + + return GetCurrentHostUpAxis(); + } + + private void SaveCurrentDocumentFragmentDefaultUpAxis(string axisName) + { + if (!IsSupportedFragmentDefaultUpAxis(axisName)) + { + return; + } + + string documentKey = GetCurrentDocumentKey(); + if (string.IsNullOrWhiteSpace(documentKey)) + { + return; + } + + _documentFragmentDefaultUpAxes[documentKey] = axisName; + } + + private string GetCurrentDocumentKey() + { + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + if (doc == null || doc.IsClear) + { + return string.Empty; + } + + if (!string.IsNullOrWhiteSpace(doc.FileName)) + { + return doc.FileName; + } + + return doc.Title ?? string.Empty; + } + + private string GetCurrentHostUpAxis() + { + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + if (doc != null && !doc.IsClear) + { + UnitVector3D worldUp = doc.CurrentViewpoint.Value.WorldUpVector; + if (!worldUp.IsZero) + { + return Math.Abs(worldUp.Y) >= Math.Abs(worldUp.Z) ? "Y" : "Z"; + } + + Vector3D documentUp = doc.UpVector; + if (!documentUp.IsZero) + { + return Math.Abs(documentUp.Y) >= Math.Abs(documentUp.Z) ? "Y" : "Z"; + } + } + + if (string.Equals(_selectedCoordinateSystem, "YUp", StringComparison.OrdinalIgnoreCase)) + { + return "Y"; + } + + return "Z"; + } + + private static bool IsSupportedFragmentDefaultUpAxis(string axisName) + { + return string.Equals(axisName, "Y", StringComparison.OrdinalIgnoreCase) || + string.Equals(axisName, "Z", StringComparison.OrdinalIgnoreCase); + } + + private void ShowRootModelRotationHintIfNeeded() + { + string documentKey = GetCurrentDocumentKey(); + if (string.IsNullOrWhiteSpace(documentKey) || _documentRotationHintShown.Contains(documentKey)) + { + return; + } + + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + if (doc == null || doc.IsClear || doc.Models == null || doc.Models.Count == 0) + { + return; + } + + if (!TryGetRootModelRotationHint(doc, out string hintMessage)) + { + return; + } + + _documentRotationHintShown.Add(documentKey); + LogManager.Info($"[模型整体旋转提示] {hintMessage}"); + System.Windows.MessageBox.Show( + hintMessage, + "坐标系提示", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Information); + } + + private static bool TryGetRootModelRotationHint(Document doc, out string hintMessage) + { + hintMessage = null; + if (doc == null || doc.Models == null || doc.Models.Count == 0) + { + return false; + } + + const double minAngleDegrees = 1.0; + + for (int i = 0; i < doc.Models.Count; i++) + { + Model model = doc.Models[i]; + ModelItem rootItem = model?.RootItem; + if (rootItem == null) + { + continue; + } + + Transform3DComponents components = rootItem.Transform.Factor(); + var axisAngle = components.Rotation.ToAxisAndAngle(); + double angleDegrees = Math.Abs(axisAngle.Angle) * 180.0 / Math.PI; + if (angleDegrees < minAngleDegrees) + { + continue; + } + + hintMessage = + $"检测到根模型存在整体旋转(约 {angleDegrees:F1}°),模型可能经过坐标系转换;如后续真实物体姿态异常,请检查 Fragment默认Up 设置。"; + return true; + } + + return false; } #region 文档事件处理 @@ -566,6 +747,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels } }); } + + _ = _uiStateManager.ExecuteUIUpdateAsync(() => + { + RefreshFragmentDefaultUpAxisSelectionFromCurrentDocument(); + }); } #endregion @@ -1315,6 +1501,541 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "体素路径规划测试"); } + /// + /// 捕获选中的真实物体,显示原始变换信息并可视化三轴。 + /// + private void ExecuteCaptureRealObjectTransform() + { + SafeExecute(() => + { + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + if (doc == null || doc.IsClear) + { + System.Windows.MessageBox.Show( + "没有活动的文档!请先打开一个模型。", + "错误", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Error); + return; + } + + var selection = doc.CurrentSelection.SelectedItems; + if (selection.Count == 0) + { + ClearCapturedRealObjectTransformVisualization(); + System.Windows.MessageBox.Show( + "请先选择一个真实物体!", + "提示", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Information); + return; + } + + ModelItem item = selection[0]; + BoundingBox3D bounds = item.BoundingBox(); + Point3D boundsCenter = bounds.Center; + TransformDiagnosticInfo selectedInfo = CreateTransformDiagnosticInfo(item, "选中节点"); + List geometryDescendants = CollectGeometryDescendants(item); + TransformDiagnosticInfo geometryInfo = geometryDescendants.Count > 0 + ? CreateTransformDiagnosticInfo(geometryDescendants[0], "首个几何后代") + : null; + FragmentOrientationAnalysis fragmentAnalysis = AnalyzeFragmentOrientations(item); + + Quaternion visualizationQuaternion = fragmentAnalysis != null && fragmentAnalysis.HasRepresentativeOrientation + ? fragmentAnalysis.RepresentativeRotation + : geometryInfo?.RotationQuaternion ?? selectedInfo.RotationQuaternion; + + double axisLength = GetTransformAxisVisualizationLength(bounds); + RenderCapturedRealObjectTransformAxes(boundsCenter, visualizationQuaternion, axisLength); + + var info = new StringBuilder(); + info.AppendLine("=== 真实物体原始变换信息 ==="); + info.AppendLine(); + info.AppendLine($"对象: {item.DisplayName}"); + info.AppendLine($"ClassName: {item.ClassName}"); + info.AppendLine($"HasGeometry: {item.HasGeometry}"); + info.AppendLine(); + info.AppendLine("【包围盒】"); + info.AppendLine($"Min = ({bounds.Min.X:F3}, {bounds.Min.Y:F3}, {bounds.Min.Z:F3})"); + info.AppendLine($"Max = ({bounds.Max.X:F3}, {bounds.Max.Y:F3}, {bounds.Max.Z:F3})"); + info.AppendLine($"Center = ({boundsCenter.X:F3}, {boundsCenter.Y:F3}, {boundsCenter.Z:F3})"); + info.AppendLine(); + + AppendTransformDiagnosticInfo(info, selectedInfo); + + info.AppendLine("【几何后代】"); + info.AppendLine($"几何后代数量 = {geometryDescendants.Count}"); + if (geometryInfo != null) + { + AppendTransformDiagnosticInfo(info, geometryInfo); + } + else + { + info.AppendLine("未找到 HasGeometry=True 的后代节点"); + info.AppendLine(); + } + + AppendFragmentAnalysis(info, fragmentAnalysis); + info.AppendLine(); + info.AppendLine("【可视化说明】"); + info.AppendLine($"三轴原点使用包围盒中心: ({boundsCenter.X:F3}, {boundsCenter.Y:F3}, {boundsCenter.Z:F3})"); + info.AppendLine($"轴长度 = {axisLength:F3}"); + info.AppendLine($"三轴方向来源 = {(fragmentAnalysis != null && fragmentAnalysis.HasRepresentativeOrientation ? "fragment代表姿态" : geometryInfo != null ? "首个几何后代Transform" : "选中节点Transform")}"); + info.AppendLine("红色=X轴,绿色=Y轴,蓝色=Z轴"); + + string message = info.ToString(); + LogManager.Info("[真实物体原始位姿]\n" + message); + + var resultDialog = new NavisworksTransport.UI.WPF.Views.CoordinateSystemResultDialog + { + Title = "真实物体原始变换信息", + ResultText = message + }; + DialogHelper.SetOwnerSafely(resultDialog); + resultDialog.ShowDialog(); + + UpdateMainStatus($"已捕获真实物体原始位姿:{item.DisplayName}"); + }, "捕获真实物体原始位姿"); + } + + private void ExecuteDetectFragmentDefaultUp() + { + SafeExecute(() => + { + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + if (doc == null || doc.IsClear) + { + System.Windows.MessageBox.Show( + "没有活动的文档!请先打开一个模型。", + "错误", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Error); + return; + } + + var selection = doc.CurrentSelection.SelectedItems; + if (selection.Count == 0) + { + System.Windows.MessageBox.Show( + "请先选择一个明显水平放置的真实物体,再执行 fragment 默认Up检测。", + "提示", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Information); + return; + } + + ModelItem item = selection[0]; + FragmentOrientationAnalysis fragmentAnalysis = AnalyzeFragmentOrientations(item); + if (fragmentAnalysis == null || !fragmentAnalysis.HasRepresentativeOrientation) + { + System.Windows.MessageBox.Show( + "未能读取选中物体的 fragment 代表姿态,无法检测默认Up。", + "提示", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Warning); + return; + } + + string hostUpAxis = GetCurrentHostUpAxis(); + Vector3 hostUpVector = hostUpAxis == "Y" ? Vector3.UnitY : Vector3.UnitZ; + double yAlignment = Math.Abs(Vector3.Dot(fragmentAnalysis.RepresentativeYAxis, hostUpVector)); + double zAlignment = Math.Abs(Vector3.Dot(fragmentAnalysis.RepresentativeZAxis, hostUpVector)); + string detectedAxis = zAlignment > yAlignment ? "Z" : "Y"; + string currentAxis = SelectedFragmentDefaultUpAxis; + LogManager.Info( + $"[fragment默认Up检测] 对象={item.DisplayName}, 宿主Up={hostUpAxis}, 当前值={currentAxis}, 检测值={detectedAxis}, " + + $"Y对齐度={yAlignment:F4}, Z对齐度={zAlignment:F4}"); + + if (string.Equals(detectedAxis, currentAxis, StringComparison.OrdinalIgnoreCase)) + { + System.Windows.MessageBox.Show( + $"检测完成:当前文档 Fragment默认Up 已是 {currentAxis}。", + "Fragment默认Up检测", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Information); + UpdateMainStatus($"Fragment默认Up检测完成:当前值 {currentAxis} 已匹配"); + return; + } + + SetProperty(ref _selectedFragmentDefaultUpAxis, detectedAxis, nameof(SelectedFragmentDefaultUpAxis)); + SaveCurrentDocumentFragmentDefaultUpAxis(detectedAxis); + + System.Windows.MessageBox.Show( + $"检测完成:当前文档 Fragment默认Up 已自动改为 {detectedAxis}。", + "Fragment默认Up检测", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Information); + + UpdateMainStatus($"当前文档 fragment 默认Up已自动更新为: {detectedAxis}"); + LogManager.Info($"[fragment默认Up] 检测后自动更新为: {detectedAxis}"); + }, "检测Fragment默认Up"); + } + + private void ClearCapturedRealObjectTransformVisualization() + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.ClearRailBaseline("system_transform_axis_x"); + renderPlugin.ClearRailBaseline("system_transform_axis_y"); + renderPlugin.ClearRailBaseline("system_transform_axis_z"); + } + + private void RenderCapturedRealObjectTransformAxes(Point3D origin, Quaternion rotationQuaternion, double axisLength) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + ClearCapturedRealObjectTransformVisualization(); + + RenderTransformAxis(renderPlugin, "system_transform_axis_x", origin, Vector3.Transform(Vector3.UnitX, rotationQuaternion), axisLength, RenderStyleName.TransformAxisX); + RenderTransformAxis(renderPlugin, "system_transform_axis_y", origin, Vector3.Transform(Vector3.UnitY, rotationQuaternion), axisLength, RenderStyleName.TransformAxisY); + RenderTransformAxis(renderPlugin, "system_transform_axis_z", origin, Vector3.Transform(Vector3.UnitZ, rotationQuaternion), axisLength, RenderStyleName.TransformAxisZ); + } + + private static void RenderTransformAxis( + PathPointRenderPlugin renderPlugin, + string pathId, + Point3D origin, + Vector3 direction, + double axisLength, + RenderStyleName renderStyleName) + { + if (direction.LengthSquared() < 1e-8f) + { + return; + } + + Vector3 normalizedDirection = Vector3.Normalize(direction); + Point3D endPoint = new Point3D( + origin.X + normalizedDirection.X * (float)axisLength, + origin.Y + normalizedDirection.Y * (float)axisLength, + origin.Z + normalizedDirection.Z * (float)axisLength); + + renderPlugin.RenderRailBaseline( + pathId, + new System.Collections.Generic.List { origin, endPoint }, + renderStyleName); + } + + private static double GetTransformAxisVisualizationLength(BoundingBox3D bounds) + { + double xSpan = bounds.Max.X - bounds.Min.X; + double ySpan = bounds.Max.Y - bounds.Min.Y; + double zSpan = bounds.Max.Z - bounds.Min.Z; + double maxSpan = Math.Max(xSpan, Math.Max(ySpan, zSpan)); + double minLength = UnitsConverter.ConvertFromMeters(0.5); + return Math.Max(minLength, maxSpan * 0.3); + } + + private static Quaternion ToQuaternion(Rotation3D rotation) + { + var axisAngle = rotation.ToAxisAndAngle(); + if (axisAngle.Axis.IsZero || Math.Abs(axisAngle.Angle) < 1e-9) + { + return Quaternion.Identity; + } + + return Quaternion.Normalize(Quaternion.CreateFromAxisAngle( + new Vector3((float)axisAngle.Axis.X, (float)axisAngle.Axis.Y, (float)axisAngle.Axis.Z), + (float)axisAngle.Angle)); + } + + private static TransformDiagnosticInfo CreateTransformDiagnosticInfo(ModelItem item, string label) + { + Transform3DComponents components = item.Transform.Factor(); + var axisAngle = components.Rotation.ToAxisAndAngle(); + Quaternion rotationQuaternion = ToQuaternion(components.Rotation); + + return new TransformDiagnosticInfo + { + Label = label, + DisplayName = item.DisplayName, + ClassName = item.ClassName, + HasGeometry = item.HasGeometry, + Translation = components.Translation, + Scale = components.Scale, + RotationAxis = axisAngle.Axis, + RotationAngleRadians = axisAngle.Angle, + RotationQuaternion = rotationQuaternion, + HostXAxis = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, rotationQuaternion)), + HostYAxis = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, rotationQuaternion)), + HostZAxis = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, rotationQuaternion)) + }; + } + + private static void AppendTransformDiagnosticInfo(StringBuilder info, TransformDiagnosticInfo diagnostic) + { + info.AppendLine($"【{diagnostic.Label}】"); + info.AppendLine($"对象 = {diagnostic.DisplayName}"); + info.AppendLine($"ClassName = {diagnostic.ClassName}"); + info.AppendLine($"HasGeometry = {diagnostic.HasGeometry}"); + info.AppendLine($"Translation = ({diagnostic.Translation.X:F3}, {diagnostic.Translation.Y:F3}, {diagnostic.Translation.Z:F3})"); + info.AppendLine($"Scale = ({diagnostic.Scale.X:F3}, {diagnostic.Scale.Y:F3}, {diagnostic.Scale.Z:F3})"); + info.AppendLine($"Rotation.Axis = ({diagnostic.RotationAxis.X:F4}, {diagnostic.RotationAxis.Y:F4}, {diagnostic.RotationAxis.Z:F4})"); + info.AppendLine($"Rotation.Angle = {diagnostic.RotationAngleRadians:F6} rad ({diagnostic.RotationAngleRadians * 180.0 / Math.PI:F2}°)"); + info.AppendLine($"X轴 = ({diagnostic.HostXAxis.X:F4}, {diagnostic.HostXAxis.Y:F4}, {diagnostic.HostXAxis.Z:F4})"); + info.AppendLine($"Y轴 = ({diagnostic.HostYAxis.X:F4}, {diagnostic.HostYAxis.Y:F4}, {diagnostic.HostYAxis.Z:F4})"); + info.AppendLine($"Z轴 = ({diagnostic.HostZAxis.X:F4}, {diagnostic.HostZAxis.Y:F4}, {diagnostic.HostZAxis.Z:F4})"); + info.AppendLine(); + } + + private static List CollectGeometryDescendants(ModelItem root) + { + var result = new List(); + if (root == null) + { + return result; + } + + CollectGeometryDescendantsRecursive(root, result); + return result; + } + + private static void CollectGeometryDescendantsRecursive(ModelItem item, List result) + { + if (item == null) + { + return; + } + + if (item.HasGeometry) + { + result.Add(item); + } + + foreach (ModelItem child in item.Children) + { + CollectGeometryDescendantsRecursive(child, result); + } + } + + private static FragmentOrientationAnalysis AnalyzeFragmentOrientations(ModelItem item) + { + if (item == null) + { + return null; + } + + Autodesk.Navisworks.Api.ModelItemCollection collection = new Autodesk.Navisworks.Api.ModelItemCollection { item }; + var comSelection = ComApiBridge.ToInwOpSelection(collection); + List fragments = null; + try + { + fragments = GeometryHelper.GetAllFragments(comSelection); + var validMatrices = fragments + .Where(f => f.TransformMatrix != null && f.TransformMatrix.Length == 16) + .Select(f => f.TransformMatrix) + .ToList(); + + var analysis = new FragmentOrientationAnalysis + { + FragmentCount = fragments.Count, + ValidTransformCount = validMatrices.Count + }; + + if (validMatrices.Count == 0) + { + return analysis; + } + + Vector3 referenceX = NormalizeOrUnitX(GetMatrixAxis(validMatrices[0], 0)); + Vector3 referenceY = NormalizeOrUnitY(GetMatrixAxis(validMatrices[0], 1)); + Vector3 referenceZ = NormalizeOrUnitZ(GetMatrixAxis(validMatrices[0], 2)); + + Vector3 sumX = referenceX; + Vector3 sumY = referenceY; + Vector3 sumZ = referenceZ; + double xDotSum = 1.0; + double yDotSum = 1.0; + double zDotSum = 1.0; + + for (int i = 1; i < validMatrices.Count; i++) + { + Vector3 axisX = NormalizeWithReference(GetMatrixAxis(validMatrices[i], 0), referenceX); + Vector3 axisY = NormalizeWithReference(GetMatrixAxis(validMatrices[i], 1), referenceY); + Vector3 axisZ = NormalizeWithReference(GetMatrixAxis(validMatrices[i], 2), referenceZ); + + xDotSum += Math.Abs(Vector3.Dot(axisX, referenceX)); + yDotSum += Math.Abs(Vector3.Dot(axisY, referenceY)); + zDotSum += Math.Abs(Vector3.Dot(axisZ, referenceZ)); + + sumX += axisX; + sumY += axisY; + sumZ += axisZ; + } + + Vector3 representativeX = NormalizeOrUnitX(sumX); + Vector3 representativeY = sumY - Vector3.Dot(sumY, representativeX) * representativeX; + representativeY = NormalizeOrUnitY(representativeY); + Vector3 representativeZ = Vector3.Normalize(Vector3.Cross(representativeX, representativeY)); + if (Vector3.Dot(representativeZ, NormalizeOrUnitZ(sumZ)) < 0) + { + representativeZ = -representativeZ; + } + representativeY = Vector3.Normalize(Vector3.Cross(representativeZ, representativeX)); + + Matrix4x4 linear = new Matrix4x4( + representativeX.X, representativeY.X, representativeZ.X, 0f, + representativeX.Y, representativeY.Y, representativeZ.Y, 0f, + representativeX.Z, representativeY.Z, representativeZ.Z, 0f, + 0f, 0f, 0f, 1f); + + analysis.HasRepresentativeOrientation = true; + analysis.RepresentativeRotation = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(linear)); + analysis.RepresentativeXAxis = representativeX; + analysis.RepresentativeYAxis = representativeY; + analysis.RepresentativeZAxis = representativeZ; + analysis.AverageXAxisConsistency = xDotSum / validMatrices.Count; + analysis.AverageYAxisConsistency = yDotSum / validMatrices.Count; + analysis.AverageZAxisConsistency = zDotSum / validMatrices.Count; + analysis.SampleTranslation = GetMatrixTranslation(validMatrices[0]); + return analysis; + } + finally + { + if (fragments != null) + { + foreach (var fragment in fragments) + { + if (fragment?.Fragment != null) + { + try + { + Marshal.ReleaseComObject(fragment.Fragment); + } + catch + { + } + } + } + } + + if (comSelection != null) + { + try + { + Marshal.ReleaseComObject(comSelection); + } + catch + { + } + } + } + } + + private static void AppendFragmentAnalysis(StringBuilder info, FragmentOrientationAnalysis analysis) + { + info.AppendLine("【Fragment 姿态分析】"); + if (analysis == null) + { + info.AppendLine("未能执行 fragment 分析"); + info.AppendLine(); + return; + } + + info.AppendLine($"Fragment 总数 = {analysis.FragmentCount}"); + info.AppendLine($"有效变换矩阵数 = {analysis.ValidTransformCount}"); + if (!analysis.HasRepresentativeOrientation) + { + info.AppendLine("未得到可用的 fragment 代表姿态"); + info.AppendLine(); + return; + } + + info.AppendLine($"示例平移 = ({analysis.SampleTranslation.X:F3}, {analysis.SampleTranslation.Y:F3}, {analysis.SampleTranslation.Z:F3})"); + info.AppendLine($"代表X轴 = ({analysis.RepresentativeXAxis.X:F4}, {analysis.RepresentativeXAxis.Y:F4}, {analysis.RepresentativeXAxis.Z:F4})"); + info.AppendLine($"代表Y轴 = ({analysis.RepresentativeYAxis.X:F4}, {analysis.RepresentativeYAxis.Y:F4}, {analysis.RepresentativeYAxis.Z:F4})"); + info.AppendLine($"代表Z轴 = ({analysis.RepresentativeZAxis.X:F4}, {analysis.RepresentativeZAxis.Y:F4}, {analysis.RepresentativeZAxis.Z:F4})"); + info.AppendLine($"X轴一致性 = {analysis.AverageXAxisConsistency:F4}"); + info.AppendLine($"Y轴一致性 = {analysis.AverageYAxisConsistency:F4}"); + info.AppendLine($"Z轴一致性 = {analysis.AverageZAxisConsistency:F4}"); + info.AppendLine(); + } + + private static Vector3 GetMatrixAxis(double[] matrix, int axisIndex) + { + switch (axisIndex) + { + case 0: + return new Vector3((float)matrix[0], (float)matrix[1], (float)matrix[2]); + case 1: + return new Vector3((float)matrix[4], (float)matrix[5], (float)matrix[6]); + case 2: + return new Vector3((float)matrix[8], (float)matrix[9], (float)matrix[10]); + default: + throw new ArgumentOutOfRangeException(nameof(axisIndex)); + } + } + + private static Vector3 GetMatrixTranslation(double[] matrix) + { + return new Vector3((float)matrix[12], (float)matrix[13], (float)matrix[14]); + } + + private static Vector3 NormalizeWithReference(Vector3 axis, Vector3 reference) + { + Vector3 normalized = NormalizeOrFallback(axis, reference); + return Vector3.Dot(normalized, reference) < 0 ? -normalized : normalized; + } + + private static Vector3 NormalizeOrUnitX(Vector3 value) + { + return NormalizeOrFallback(value, Vector3.UnitX); + } + + private static Vector3 NormalizeOrUnitY(Vector3 value) + { + return NormalizeOrFallback(value, Vector3.UnitY); + } + + private static Vector3 NormalizeOrUnitZ(Vector3 value) + { + return NormalizeOrFallback(value, Vector3.UnitZ); + } + + private static Vector3 NormalizeOrFallback(Vector3 value, Vector3 fallback) + { + return value.LengthSquared() < 1e-8f ? fallback : Vector3.Normalize(value); + } + + private sealed class TransformDiagnosticInfo + { + public string Label { get; set; } + public string DisplayName { get; set; } + public string ClassName { get; set; } + public bool HasGeometry { get; set; } + public Vector3D Translation { get; set; } + public Vector3D Scale { get; set; } + public Vector3D RotationAxis { get; set; } + public double RotationAngleRadians { get; set; } + public Quaternion RotationQuaternion { get; set; } + public Vector3 HostXAxis { get; set; } + public Vector3 HostYAxis { get; set; } + public Vector3 HostZAxis { get; set; } + } + + private sealed class FragmentOrientationAnalysis + { + public int FragmentCount { get; set; } + public int ValidTransformCount { get; set; } + public bool HasRepresentativeOrientation { get; set; } + public Quaternion RepresentativeRotation { get; set; } + public Vector3 RepresentativeXAxis { get; set; } + public Vector3 RepresentativeYAxis { get; set; } + public Vector3 RepresentativeZAxis { get; set; } + public double AverageXAxisConsistency { get; set; } + public double AverageYAxisConsistency { get; set; } + public double AverageZAxisConsistency { get; set; } + public Vector3 SampleTranslation { get; set; } + } + /// /// 读取选中对象的Transform信息,并测试旋转 /// @@ -1864,4 +2585,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels #endregion } -} \ No newline at end of file +} diff --git a/src/UI/WPF/Views/SystemManagementView.xaml b/src/UI/WPF/Views/SystemManagementView.xaml index 6b05f58..94f3a06 100644 --- a/src/UI/WPF/Views/SystemManagementView.xaml +++ b/src/UI/WPF/Views/SystemManagementView.xaml @@ -178,6 +178,29 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav ToolTip="AutoDetect: 自动检测, ZUp: Z轴向上, YUp: Y轴向上"/> + +