From 0c83c739176a0a3eb186ca6f66563e46db4bab57 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 19 Mar 2026 00:18:24 +0800 Subject: [PATCH 01/87] feat: add linear assembly rail path workflow --- TransportPlugin.csproj | 4 +- .../2026/double-rail-mount-path-design.md | 299 +++++++++ .../2026/linear-assembly-reference-path.md | 299 +++++++++ doc/design/2026/直线装配路径_用户操作说明.md | 142 ++++ resources/unit_cylinder.obj | 78 +++ src/Core/Animation/PathAnimationManager.cs | 137 +++- src/Core/AssemblyReferencePathManager.cs | 408 ++++++++++++ src/Core/PathDataManager.cs | 54 +- src/Core/PathDatabase.cs | 67 +- src/Core/PathPlanningManager.cs | 87 ++- src/Core/PathPlanningModels.cs | 86 ++- src/Core/PathPointRenderPlugin.cs | 84 ++- src/PathPlanning/RailGeometryHelper.cs | 412 +++++++++++- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 629 +++++++++++++++++- src/UI/WPF/Views/PathEditingView.xaml | 181 ++++- src/Utils/ModelItemTransformHelper.cs | 228 ++++++- src/Utils/RailPathPoseHelper.cs | 279 ++++++++ 17 files changed, 3384 insertions(+), 90 deletions(-) create mode 100644 doc/design/2026/double-rail-mount-path-design.md create mode 100644 doc/design/2026/linear-assembly-reference-path.md create mode 100644 doc/design/2026/直线装配路径_用户操作说明.md create mode 100644 resources/unit_cylinder.obj create mode 100644 src/Core/AssemblyReferencePathManager.cs create mode 100644 src/Utils/RailPathPoseHelper.cs diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 9fe4324..919214e 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -133,6 +133,7 @@ + @@ -338,6 +339,7 @@ + @@ -496,4 +498,4 @@ - \ No newline at end of file + diff --git a/doc/design/2026/double-rail-mount-path-design.md b/doc/design/2026/double-rail-mount-path-design.md new file mode 100644 index 0000000..aa9211a --- /dev/null +++ b/doc/design/2026/double-rail-mount-path-design.md @@ -0,0 +1,299 @@ +# 双轨安装头空轨路径扩展设计 + +## 背景 + +当前项目中的 `PathType.Rail` 语义实际等同于“轨下悬挂运输”: + +- 基准路径来自空轨下表面中心线 +- 动画默认将物体沿世界 `Z` 方向向下偏移一个物体高度 +- 通行空间也默认按“路径点在物体上方”渲染 + +这套模型无法准确表达新的双轨安装头场景: + +- 轨道为两根平行轨道 +- 安装头在两轨中间滑动 +- 箱型构件与安装头刚性连接 +- 构件以顶面中心或底面中心与安装头对接 +- 安装头支持轨上、轨下两种安装方式 +- 轨道支持斜向上、斜向下 + +## 目标 + +将现有“空轨路径”升级为“轨道导向装配运输路径”,支持以下 4 种基本构型: + +1. 轨上 + 斜向上 +2. 轨上 + 斜向下 +3. 轨下 + 斜向上 +4. 轨下 + 斜向下 + +并支持两种构件对接方式: + +1. 顶面中心对接安装头 +2. 底面中心对接安装头 + +## 设计原则 + +1. 不再以“轨下中心线”作为 Rail 路径的唯一几何语义。 +2. 路径数据应表达“安装头中心的参考路径”。 +3. 构件实际动画位置和碰撞包络,应从“参考路径 + 安装构型”推导。 +4. 对斜轨路径,偏移和姿态必须基于轨道局部坐标系,而不是简单沿世界 `Z` 轴处理。 +5. 保持旧 Rail 路径可加载,并为旧数据提供默认兼容语义。 + +## 新数据模型 + +### RailMountMode + +```csharp +public enum RailMountMode +{ + UnderRail = 0, + OverRail = 1 +} +``` + +含义:安装头位于双轨参考平面的下方或上方。 + +### RailPayloadAnchorMode + +```csharp +public enum RailPayloadAnchorMode +{ + TopCenter = 0, + BottomCenter = 1 +} +``` + +含义:构件以顶面中心或底面中心与安装头刚性对接。 + +### RailPathDefinitionMode + +```csharp +public enum RailPathDefinitionMode +{ + LegacyBottomCenterLine = 0, + InstallationHeadCenterLine = 1 +} +``` + +含义: + +- `LegacyBottomCenterLine` 用于兼容旧空轨路径 +- `InstallationHeadCenterLine` 表示新的安装头中心参考路径 + +### PathRoute 新增字段 + +建议为 `PathRoute` 增加以下字段: + +```csharp +public RailMountMode RailMountMode { get; set; } +public RailPayloadAnchorMode RailPayloadAnchorMode { get; set; } +public RailPathDefinitionMode RailPathDefinitionMode { get; set; } +public double RailHeadToPayloadAnchorOffset { get; set; } +public double RailGaugeCenterOffset { get; set; } +``` + +说明: + +- `RailHeadToPayloadAnchorOffset` + - 安装头中心到构件对接中心的距离 + - 使用模型单位 + - 用于精确表达安装头结构厚度和连接件高度 +- `RailGaugeCenterOffset` + - 双轨中心线到安装头中心线的附加偏移 + - 默认通常为 `0` + - 为后续非对称安装场景留扩展位 + +## 局部坐标系 + +每一段 Rail 路径在采样时建立局部坐标系: + +- `T`:沿轨道方向的单位向量 +- `N`:轨道平面法向,用于区分轨上 / 轨下 +- `B`:横向向量,满足右手系 + +建议约束: + +- 双轨所在平面由轨道主方向和全局竖直方向共同确定 +- 当轨道接近竖直时,仍应优先走显式异常,而不是隐式回退 + +构件实际锚点位置: + +```text +PayloadAnchorPosition = RailReferencePoint + sign(mountMode) * N * offset +``` + +其中: + +- `sign(UnderRail) = -1` +- `sign(OverRail) = +1` + +构件几何中心再根据 `TopCenter` / `BottomCenter` 与物体高度换算得到。 + +## 参考路径定义 + +### 旧逻辑 + +当前 `RailGeometryHelper` 输出的是: + +- 空轨下表面中心线 + +### 新逻辑 + +建议升级为输出: + +- 双轨中间的安装头参考中心线 + +实现上分两层: + +1. `ExtractRailReferencePath` + - 负责提取轨道导向参考线 +2. `ResolvePayloadPath` + - 根据安装构型计算构件实际运动参考路径 + +## 兼容策略 + +### 历史路径 + +旧路径未包含 `RailMountMode`、`RailPayloadAnchorMode` 等信息时,按以下默认值兼容: + +```text +RailMountMode = UnderRail +RailPayloadAnchorMode = TopCenter +RailPathDefinitionMode = LegacyBottomCenterLine +RailHeadToPayloadAnchorOffset = 0 +RailGaugeCenterOffset = 0 +``` + +兼容语义: + +- 保持旧项目行为尽量不变 +- 旧路径仍按“轨下悬挂”解释 + +### 新路径 + +新创建的 Rail 路径统一采用: + +```text +RailPathDefinitionMode = InstallationHeadCenterLine +``` + +## 需要修改的模块 + +### 1. 模型层 + +文件: + +- `src/Core/PathPlanningModels.cs` +- `src/Commands/CreateAerialPathCommand.cs` + +工作: + +- 增加 Rail 相关枚举 +- 为 `PathRoute` 增加 Rail 构型字段 +- 新建 Rail 路径时写入默认配置 + +### 2. Rail 几何提取 + +文件: + +- `src/PathPlanning/RailGeometryHelper.cs` + +工作: + +- 将“下表面中心线”抽象为“参考路径提取” +- 区分旧路径提取和新路径提取 +- 为双轨场景预留“轨道中心参考线”提取接口 + +### 3. 交互吸附 + +文件: + +- `src/Core/PathPlanningManager.cs` +- `src/UI/WPF/ViewModels/PathEditingViewModel.cs` + +工作: + +- 用户点击继续吸附到 Rail 参考路径 +- 新建 Rail 路径时允许指定安装构型 + +### 4. 渲染 + +文件: + +- `src/Core/PathPointRenderPlugin.cs` + +工作: + +- 将 `PathType.Rail` 的固定“向上/向下 Z 偏移”改为局部法向偏移 +- 通行空间不再假定路径点永远位于物体顶部 + +### 5. 动画 + +文件: + +- `src/Core/Animation/PathAnimationManager.cs` + +工作: + +- 将 Rail 路径下的构件位置计算改为: + - 先得到安装头参考点 + - 再根据 `RailMountMode` 和 `RailPayloadAnchorMode` 推导物体位置 +- 后续增加 pitch/roll 对齐能力 + +### 6. 导出 + +文件: + +- `src/Core/PathDataManager.cs` + +工作: + +- 将 `suspension` 语义调整为更中性的 rail-guided aerial path +- 视下游格式能力决定是否补充导出字段 + +## 分阶段实现建议 + +### Phase 1:最小可用版本 + +目标:先支持 4 种构型的碰撞检测和动画位置正确。 + +范围: + +- 增加 Rail 构型字段 +- Rail 动画和通行空间偏移改为基于安装构型计算 +- 继续沿用现有参考线提取,但将其抽象为“参考路径” + +限制: + +- 暂不处理完整 pitch/roll 姿态 +- 暂不精确重建双轨实体几何关系 + +### Phase 2:几何语义升级 + +目标:把参考路径从“轨下中心线”升级为“安装头中心线”。 + +范围: + +- 重构 `RailGeometryHelper` +- 为双轨模型提取真正的中线 +- 新旧路径分别兼容 + +### Phase 3:姿态精化 + +目标:对斜轨和长臂构件提供更真实的空间姿态。 + +范围: + +- 动画支持 pitch / roll +- 构件包络与局部坐标系一致 +- 提升碰撞检测准确性 + +## 当前建议 + +本次开发先从 Phase 1 开始: + +1. 先把路径模型改为可表达 4 种构型 +2. 先把渲染和动画从“世界 Z 偏移”改成“构型驱动偏移” +3. 再进入轨道参考线提取的升级 + +这样风险更低,也更容易验证。 diff --git a/doc/design/2026/linear-assembly-reference-path.md b/doc/design/2026/linear-assembly-reference-path.md new file mode 100644 index 0000000..002141f --- /dev/null +++ b/doc/design/2026/linear-assembly-reference-path.md @@ -0,0 +1,299 @@ +# 终点反推的直线装配路径设计 + +## 背景 + +当前空轨路径已经扩展出轨上/轨下、顶面对接/底面对接、局部姿态等能力,但用户进一步澄清了真实业务: + +- 已知箱体最终安装后的准确位置和空间方向 +- 该场景只需要一条起点到终点的直线运动 +- Navisworks 里用户无法点击空气中的点,必须点击到某个实际对象 + +因此,这类路径不应继续建模为“轨道识别”问题,而应建模为“已知终点位姿的直线装配运动”问题。 + +## 核心思路 + +### 1. 终点位姿作为主输入 + +由用户选择终点处已经组装好的箱体,系统读取: + +- 包围盒 +- 当前朝向 +- 终点构件中心 / 顶面中心 / 底面中心 + +终点位姿是整个运动仿真的主基准。 + +### 2. 生成可点击的参考杆 + +为了满足 Navisworks 的点击限制,系统不只绘制一条渲染线,而是生成一个临时参考实体: + +- 形态:细长杆体 +- 初版建议:直接复用 `unit_cube.nwc`,通过缩放生成细长方杆 +- 作用: + - 可视化直线参考路径 + - 允许用户点击取点 + - 为起点吸附提供稳定对象 + +### 3. 用户点击参考杆确定起点 + +用户在三维视图中点击参考杆上的某个位置后: + +- 获取点击坐标 +- 将点击点投影到参考杆轴线 +- 该投影点作为真实起点 + +### 4. 生成最终直线路径 + +系统根据: + +- 起点 +- 终点位姿 +- 轨上/轨下 +- 顶面对接/底面对接 + +生成直线路径点列,并继续复用现有: + +- 动画 +- 通行空间 +- 碰撞检测 + +## 与当前 Rail 方案的关系 + +### 保留 + +- `RailMountMode` +- `RailPayloadAnchorMode` +- `RailPathDefinitionMode` +- `RailPathPoseHelper` +- 动画完整姿态支持 + +这些都仍然有价值,因为终点和路径参考点之间仍需要姿态推导。 + +### 弱化 / 退出主路径 + +- 轨道几何识别 +- 双轨自动分析 +- 基于轨道装置的中心线提取 + +这些能力可保留为辅助模式,但不再作为该场景的主生成逻辑。 + +## 最小可行实现 + +### Phase 1 + +新增“终点反推参考杆”基础能力: + +- 选择终点箱体 +- 读取终点包围盒和朝向 +- 生成参考杆临时实体 +- 支持显示 / 隐藏 / 更新参考杆 + +### Phase 2 + +新增“点击参考杆生成直线路径”: + +- 用户点击参考杆 +- 起点投影到参考线 +- 终点由终点箱体位姿推导 +- 自动生成两点直线路径 + +### Phase 3 + +补齐 UI 与持久化: + +- 在 PathEditingViewModel / PathEditingView 中新增入口 +- 保存终点箱体引用、参考线方向、参考杆长度等参数 + +## 推荐复用点 + +- `VirtualObjectManager` + - 复用 `unit_cube.nwc` 的加载、缩放、隐藏逻辑 +- `ModelItemTransformHelper` + - 复用带缩放的位姿变换 +- `NavisworksSelectionHelper` + - 复用当前选择对象读取 +- `PathPlanningManager` + - 复用点击工具和路径点落点链路 + +## 需要新增的能力 + +建议新增一个专用管理器,而不是继续塞进 `VirtualObjectManager`: + +- `AssemblyReferencePathManager` + +职责: + +- 管理参考杆临时实体 +- 根据终点箱体位姿生成参考轴线 +- 提供点击点到参考线的投影 +- 输出最终直线路径点 + +## 结论 + +本需求的主问题是“箱体的直线装配运动仿真”,不是“轨道识别”。 +推荐将实现重心切换为: + +`终点箱体位姿 -> 参考杆 -> 点击起点 -> 直线路径 -> 动画/碰撞` + +这条路线比轨道几何分析更稳定、更符合 Navisworks 交互约束,也更贴近用户真实使用方式。 + +## 当前实现状态 + +当前 worktree 中已经完成一版可试用实现,核心能力包括: + +- 在路径编辑页新增“直线装配参考”区域 +- 支持捕获终点处已安装箱体 +- 支持选择 `轨上安装 / 轨下安装` +- 支持选择 `顶面对接 / 底面对接` +- 根据终点箱体位姿生成一根可点击的参考杆 +- 在三维中渲染终点对接点标记 +- 用户点击参考杆后,自动吸附到参考线并生成两点直线路径 +- 新生成路径仍使用 `PathType.Rail`,因此可继续复用现有 Rail 动画、通行空间和碰撞检测链路 + +说明: + +- 顶部原有“空轨路径”入口仍保留,但已改名为“传统空轨路径” +- 当前推荐优先使用“直线装配参考”流程 + +## 当前操作流程 + +### 1. 选择终点箱体 + +先在 Navisworks 三维视图中选中终点处已经安装好的箱体,然后在路径编辑页点击: + +- `捕获终点箱体` + +系统会记录: + +- 箱体名称 +- 包围盒中心与尺寸 +- 当前选择的安装方式 +- 当前选择的对接基准 +- 推导出的终点对接点 + +### 2. 设置装配构型 + +在“直线装配参考”区域中设置: + +- `安装方式` + - `轨下安装` + - `轨上安装` +- `对接基准` + - `顶面对接` + - `底面对接` + +这两个参数会直接写入最终生成的 `PathRoute`: + +- `RailMountMode` +- `RailPayloadAnchorMode` + +### 3. 生成参考杆 + +设置参考杆参数后点击: + +- `生成参考杆` + +当前实现会: + +- 从终点对接点沿箱体前向反推出参考杆 +- 在三维场景中生成一根可点击的细长参考杆 +- 同时渲染终点对接点标记,便于确认当前到底是顶面中心还是底面中心 + +参考杆资源优先使用: + +- `resources/unit_cylinder.nwc` + +如果该资源暂未放入,则会临时退回: + +- `resources/unit_cube.nwc` + +### 4. 点击参考杆生成路径 + +点击: + +- `取起点并生成路径` + +然后在三维中点击参考杆附近任意可拾取位置。系统会: + +- 获取点击坐标 +- 投影到参考杆轴线 +- 将投影点作为真实起点 +- 将终点对接点作为真实终点 +- 自动创建一条两点直线路径 + +生成后的路径名称形如: + +- `直线装配_1` + +## 当前界面入口 + +相关界面位于: + +- `路径编辑页 -> 直线装配参考` + +当前主要按钮: + +- `捕获终点箱体` +- `生成参考杆` +- `取起点并生成路径` +- `隐藏参考杆` + +传统模式入口: + +- `传统空轨路径` + +## 当前已知边界 + +### 1. 终点对接点依赖包围盒和当前朝向 + +当前终点对接点通过以下信息推导: + +- 世界包围盒 +- 当前变换的局部 Z 方向 +- 估算出的局部箱体高度 + +这对规则箱型构件是可行的,但如果对象几何明显偏离箱体、或包围盒受附属零件影响较大,终点锚点可能仍需进一步校核。 + +### 2. 参考杆方向当前取箱体前向 + +当前参考杆方向来自: + +- 箱体变换后的局部 X 轴 + +这适合“装配移动方向与构件主朝向一致”的场景。 +如果后续遇到“箱体朝向正确,但装配退让方向不是局部 X 轴”的场景,需要再补一个“参考方向选择”输入。 + +### 3. 仍然复用 Rail 路径类型 + +当前新路径仍记为: + +- `PathType.Rail` + +这是有意为之,目的是复用已有 Rail 动画和通行空间逻辑。 +如果未来这类路径越来越多,可以再考虑单独抽象成新的路径类型,例如 `LinearAssembly`。 + +## 建议测试用例 + +建议至少验证以下组合: + +1. `轨下安装 + 顶面对接` +2. `轨下安装 + 底面对接` +3. `轨上安装 + 顶面对接` +4. `轨上安装 + 底面对接` + +每组建议检查: + +- 终点对接点标记是否落在预期位置 +- 参考杆是否从终点沿正确方向反推 +- 点击参考杆后起点是否吸附到参考线上 +- 生成路径后,动画位置和通行空间偏移是否符合构型 + +## 后续建议 + +下一阶段建议优先做这几项: + +1. 增加“参考方向来源”选项 + 例如允许用户直接指定反推方向,而不只使用箱体局部 X 轴。 +2. 为直线装配路径增加更明确的导出字段 + 便于区分“传统空轨路径”和“终点反推的直线装配路径”。 +3. 补一份面向最终用户的简短操作说明 + 可以从本节内容提炼成更短的 UI 使用指南。 diff --git a/doc/design/2026/直线装配路径_用户操作说明.md b/doc/design/2026/直线装配路径_用户操作说明.md new file mode 100644 index 0000000..85b9764 --- /dev/null +++ b/doc/design/2026/直线装配路径_用户操作说明.md @@ -0,0 +1,142 @@ +# 直线装配路径用户操作说明 + +## 适用场景 + +适用于以下场景: + +- 已知箱体最终安装后的准确位置和朝向 +- 只需要做起点到终点的直线运动仿真 +- 目标是检查运动过程中的碰撞 + +不推荐优先用于以下场景: + +- 需要沿复杂轨道中心线连续编辑路径 +- 需要依赖传统空轨模型吸附逐点绘制路径 + +## 推荐流程 + +### 1. 选择终点箱体 + +先在 Navisworks 三维视图中选中终点处已经安装好的箱体。 + +然后在路径编辑页的“直线装配参考”区域点击: + +- `捕获终点箱体` + +### 2. 设置装配构型 + +在“直线装配参考”区域设置: + +- `安装方式` + - `轨下安装` + - `轨上安装` +- `对接基准` + - `顶面对接` + - `底面对接` + +### 3. 生成参考杆 + +设置参考杆长度、直径后点击: + +- `生成参考杆` + +生成后,三维中会看到: + +- 一根可点击的参考杆 +- 一个终点对接点标记 + +说明: + +- 终点对接点标记用于确认当前终点是落在顶面中心还是底面中心 + +### 4. 取起点并生成路径 + +点击: + +- `取起点并生成路径` + +然后在三维里点击参考杆附近位置。系统会: + +- 自动吸附到参考杆轴线 +- 用吸附点作为起点 +- 用终点对接点作为终点 +- 自动生成一条两点直线路径 + +### 5. 检查路径与动画 + +路径生成后,建议检查: + +- 终点对接点是否正确 +- 起点是否吸附在参考杆上 +- 安装方式是否正确 +- 顶面/底面对接是否正确 +- 动画过程中构件位置是否合理 + +## 当前界面入口 + +主要入口: + +- `路径编辑页 -> 直线装配参考` + +当前主要按钮: + +- `捕获终点箱体` +- `生成参考杆` +- `取起点并生成路径` +- `隐藏参考杆` + +## 与传统空轨路径的区别 + +### 直线装配路径 + +特点: + +- 以终点箱体位姿为主输入 +- 自动生成参考杆 +- 只需要点一次起点 +- 适合直线装配仿真 + +### 传统空轨路径 + +入口: + +- `传统空轨路径` + +特点: + +- 基于空轨基准线吸附 +- 适合沿空轨逐点编辑路径 +- 不作为当前装配仿真场景的首选入口 + +## 建议试用检查项 + +建议至少试以下四组: + +1. `轨下安装 + 顶面对接` +2. `轨下安装 + 底面对接` +3. `轨上安装 + 顶面对接` +4. `轨上安装 + 底面对接` + +每组建议检查: + +- 终点标记位置是否正确 +- 参考杆方向是否正确 +- 起点吸附是否准确 +- 路径生成后动画是否符合预期 + +## 常见提示 + +如果无法生成参考杆,先检查: + +- 是否已经在三维里选中终点箱体 +- 当前选择对象是否是有效模型对象 + +如果无法取起点,先检查: + +- 是否已经生成参考杆 +- 是否进入了“取起点并生成路径”状态 + +如果终点位置不符合预期,优先检查: + +- 当前是否选对了 `轨上安装 / 轨下安装` +- 当前是否选对了 `顶面对接 / 底面对接` diff --git a/resources/unit_cylinder.obj b/resources/unit_cylinder.obj new file mode 100644 index 0000000..f920599 --- /dev/null +++ b/resources/unit_cylinder.obj @@ -0,0 +1,78 @@ +# unit cylinder for assembly reference rod +# dimensions: length 0.01m along +X, diameter 0.01m +# centered at origin +o unit_cylinder +v -0.005000 0.000000 0.000000 +v -0.005000 0.005000 0.000000 +v -0.005000 0.004330 0.002500 +v -0.005000 0.002500 0.004330 +v -0.005000 0.000000 0.005000 +v -0.005000 -0.002500 0.004330 +v -0.005000 -0.004330 0.002500 +v -0.005000 -0.005000 0.000000 +v -0.005000 -0.004330 -0.002500 +v -0.005000 -0.002500 -0.004330 +v -0.005000 0.000000 -0.005000 +v -0.005000 0.002500 -0.004330 +v -0.005000 0.004330 -0.002500 +v 0.005000 0.000000 0.000000 +v 0.005000 0.005000 0.000000 +v 0.005000 0.004330 0.002500 +v 0.005000 0.002500 0.004330 +v 0.005000 0.000000 0.005000 +v 0.005000 -0.002500 0.004330 +v 0.005000 -0.004330 0.002500 +v 0.005000 -0.005000 0.000000 +v 0.005000 -0.004330 -0.002500 +v 0.005000 -0.002500 -0.004330 +v 0.005000 0.000000 -0.005000 +v 0.005000 0.002500 -0.004330 +v 0.005000 0.004330 -0.002500 +f 2 15 16 +f 2 16 3 +f 3 16 17 +f 3 17 4 +f 4 17 18 +f 4 18 5 +f 5 18 19 +f 5 19 6 +f 6 19 20 +f 6 20 7 +f 7 20 21 +f 7 21 8 +f 8 21 22 +f 8 22 9 +f 9 22 23 +f 9 23 10 +f 10 23 24 +f 10 24 11 +f 11 24 25 +f 11 25 12 +f 12 25 26 +f 12 26 13 +f 13 26 15 +f 13 15 2 +f 1 3 2 +f 1 4 3 +f 1 5 4 +f 1 6 5 +f 1 7 6 +f 1 8 7 +f 1 9 8 +f 1 10 9 +f 1 11 10 +f 1 12 11 +f 1 13 12 +f 1 2 13 +f 14 15 16 +f 14 16 17 +f 14 17 18 +f 14 18 19 +f 14 19 20 +f 14 20 21 +f 14 21 22 +f 14 22 23 +f 14 23 24 +f 14 24 25 +f 14 25 26 +f 14 26 15 diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index f9de754..3d05eef 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -72,6 +72,8 @@ namespace NavisworksTransport.Core.Animation public double Progress { get; set; } // 进度(0-1) public Point3D Position { get; set; } // 该帧的位置 public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度) + public Rotation3D Rotation { get; set; } // 可选的完整三维姿态 + public bool HasCustomRotation { get; set; } // 是否使用完整三维姿态 public List Collisions { get; set; } // 该帧的碰撞结果 public bool HasCollision => Collisions?.Count > 0; @@ -79,6 +81,8 @@ namespace NavisworksTransport.Core.Animation { Collisions = new List(); YawRadians = 0.0; + Rotation = Rotation3D.Identity; + HasCustomRotation = false; } } @@ -156,6 +160,8 @@ namespace NavisworksTransport.Core.Animation private AnimationState _currentState = AnimationState.Idle; private double _pausedProgress = 0.0; // 暂停时的进度(0-1之间) private double _currentYaw = 0.0; // 当前偏航角(弧度) + private Rotation3D _currentRotation = Rotation3D.Identity; // 当前完整姿态 + private bool _hasCurrentRotation = false; // 当前是否使用完整姿态 // TimeLiner 集成 private TimeLinerIntegrationManager _timeLinerManager; @@ -167,6 +173,8 @@ namespace NavisworksTransport.Core.Animation // === 物体状态保存和恢复 === private Point3D _savedObjectPosition; private double _savedObjectYaw; + private Rotation3D _savedObjectRotation = Rotation3D.Identity; + private bool _savedObjectHasCustomRotation = false; private bool _hasSavedObjectState = false; /// @@ -413,6 +421,8 @@ namespace NavisworksTransport.Core.Animation // 2. 重置内部跟踪变量(同步到原始状态) // 注意:ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值 _currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform); + _currentRotation = Rotation3D.Identity; + _hasCurrentRotation = false; var originalBoundingBox = objectToRestore.BoundingBox(); _currentPosition = new Point3D( @@ -639,10 +649,23 @@ namespace NavisworksTransport.Core.Animation } else if (_route.PathType == PathType.Rail) { - // 空轨路径:路径点是悬挂点,物体悬挂在下方 - double objectHeight = _animatedObject.BoundingBox().Max.Z - _animatedObject.BoundingBox().Min.Z; - startPosition = new Point3D(startPosition.X, startPosition.Y, startPosition.Z - objectHeight); - LogManager.Debug($"[移动到起点] 空轨路径调整: 悬挂点Z={_pathPoints[0].Z:F2}, 物体底面Z={startPosition.Z:F2}, 物体高度={objectHeight:F2}"); + double objectHeight = GetAnimatedObjectHeight(); + Point3D previousPoint = _pathPoints[0]; + Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0]; + startPosition = RailPathPoseHelper.ResolveBottomPosition(_route, startPosition, previousPoint, nextPoint, objectHeight); + LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体底面=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={_route.RailPayloadAnchorMode}, 偏移={_route.RailReferenceToAnchorOffset:F2}"); + + if (!_isVirtualObject && _animatedObject != null && + RailPathPoseHelper.TryCreateRailRotation(previousPoint, _pathPoints[0], nextPoint, out var railRotation)) + { + ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, startPosition, railRotation); + _currentPosition = startPosition; + _currentRotation = railRotation; + _hasCurrentRotation = true; + _currentYaw = ModelItemTransformHelper.GetYawFromRotation(railRotation); + LogManager.Debug("[移动到起点] Rail路径已应用完整三维姿态"); + return true; + } } else { @@ -814,6 +837,8 @@ namespace NavisworksTransport.Core.Animation double targetDistance = i * distancePerFrameInModelUnits; // 按距离采样(模型单位),不是按进度 Point3D framePosition; + Point3D previousFramePoint = Point3D.Origin; + Point3D nextFramePoint = Point3D.Origin; double yawRadians; if (isAerialPath) @@ -832,6 +857,8 @@ namespace NavisworksTransport.Core.Animation // 线性插值 Point3D p1 = allSampledPoints[segmentIndex]; Point3D p2 = allSampledPoints[segmentIndex + 1]; + previousFramePoint = p1; + nextFramePoint = p2; framePosition = new Point3D( p1.X + segmentProgress * (p2.X - p1.X), p1.Y + segmentProgress * (p2.Y - p1.Y), @@ -848,7 +875,7 @@ namespace NavisworksTransport.Core.Animation } // 🔥 空中路径:根据路径类型和线段索引调整物体位置 - double objectHeight = _animatedObject.BoundingBox().Max.Z - _animatedObject.BoundingBox().Min.Z; + double objectHeight = GetAnimatedObjectHeight(); if (_route.PathType == PathType.Hoisting) { @@ -896,10 +923,8 @@ namespace NavisworksTransport.Core.Animation } else if (_route.PathType == PathType.Rail) { - // 空轨路径:路径点是悬挂点,物体悬挂在下方 - // 物体顶面应该在路径点,所以物体底面 = 路径点 - 物体高度 - framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - objectHeight); - LogManager.Debug($"[空轨路径] 调整物体位置: 悬挂点Z={framePosition.Z + objectHeight:F2}, 物体底面Z={framePosition.Z:F2}"); + framePosition = RailPathPoseHelper.ResolveBottomPosition(_route, framePosition, p1, p2, objectHeight); + LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体底面=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 对接={_route.RailPayloadAnchorMode}"); } } else @@ -926,6 +951,8 @@ namespace NavisworksTransport.Core.Animation } yawRadians = ComputeYawOnPath(i, allSampledPoints, edgeIndex, edgeProgress); + previousFramePoint = framePosition; + nextFramePoint = framePosition; } // 创建帧并检测碰撞 @@ -938,6 +965,13 @@ namespace NavisworksTransport.Core.Animation Collisions = new List() }; + if (_route.PathType == PathType.Rail && + RailPathPoseHelper.TryCreateRailRotation(previousFramePoint, framePosition, nextFramePoint, out var railRotation)) + { + frame.Rotation = railRotation; + frame.HasCustomRotation = true; + } + // 记录第一帧的角度(用于调试) if (i == 0) { @@ -2092,6 +2126,8 @@ namespace NavisworksTransport.Core.Animation incrementalTransform = components.Combine(); _currentYaw = newYaw; + _currentRotation = Rotation3D.Identity; + _hasCurrentRotation = false; } else { @@ -2112,6 +2148,31 @@ namespace NavisworksTransport.Core.Animation } } + /// + /// 使用完整三维姿态更新对象位置。 + /// 当前先用于 Rail 路径,避免影响现有地面/吊装路径的 yaw 流程。 + /// + private void UpdateObjectPosition(Point3D newPosition, Rotation3D newRotation) + { + try + { + if (_animatedObject == null) + { + return; + } + + ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, newPosition, newRotation); + _currentPosition = newPosition; + _currentRotation = newRotation; + _hasCurrentRotation = true; + _currentYaw = ModelItemTransformHelper.GetYawFromRotation(newRotation); + } + catch (Exception ex) + { + LogManager.Error($"使用完整姿态更新对象位置失败: {ex.Message}"); + } + } + /// /// 获取文档单位信息(用于日志记录) /// @@ -2228,8 +2289,10 @@ namespace NavisworksTransport.Core.Animation var (position, yaw) = GetObjectCurrentPosition(obj); _savedObjectPosition = position; _savedObjectYaw = yaw; + _savedObjectRotation = _currentRotation; + _savedObjectHasCustomRotation = _hasCurrentRotation; _hasSavedObjectState = true; - LogManager.Info($"已保存物体状态: pos=({position.X:F2},{position.Y:F2},{position.Z:F2}), yaw={yaw * 180 / Math.PI:F2}度"); + LogManager.Info($"已保存物体状态: pos=({position.X:F2},{position.Y:F2},{position.Z:F2}), yaw={yaw * 180 / Math.PI:F2}度, customRotation={_savedObjectHasCustomRotation}"); } catch (Exception ex) { @@ -2262,14 +2325,21 @@ namespace NavisworksTransport.Core.Animation var bbox = obj.BoundingBox(); _currentPosition = new Point3D(bbox.Center.X, bbox.Center.Y, bbox.Min.Z); - // 使用保存的朝向作为当前朝向,避免UpdateObjectPosition计算不必要的大角度差 - // 这样能确保物体直接从当前动画位置平滑恢复到保存位置,不会突然转动 _currentYaw = _savedObjectYaw; + _currentRotation = _savedObjectRotation; + _hasCurrentRotation = _savedObjectHasCustomRotation; - UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw); + if (_savedObjectHasCustomRotation) + { + UpdateObjectPosition(_savedObjectPosition, _savedObjectRotation); + } + else + { + UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw); + } _animatedObject = originalAnimatedObject; - LogManager.Info($"已恢复物体状态: pos=({_savedObjectPosition.X:F2},{_savedObjectPosition.Y:F2},{_savedObjectPosition.Z:F2}), yaw={_savedObjectYaw * 180 / Math.PI:F2}度"); + LogManager.Info($"已恢复物体状态: pos=({_savedObjectPosition.X:F2},{_savedObjectPosition.Y:F2},{_savedObjectPosition.Z:F2}), yaw={_savedObjectYaw * 180 / Math.PI:F2}度, customRotation={_savedObjectHasCustomRotation}"); } catch (Exception ex) { @@ -2343,7 +2413,14 @@ namespace NavisworksTransport.Core.Animation // 更新对象位置和朝向 var frameData = _animationFrames[_currentFrameIndex]; - UpdateObjectPosition(frameData.Position, frameData.YawRadians); + if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail) + { + UpdateObjectPosition(frameData.Position, frameData.Rotation); + } + else + { + UpdateObjectPosition(frameData.Position, frameData.YawRadians); + } // 更新碰撞高亮 UpdateCollisionHighlightFromFrame(); @@ -2721,6 +2798,25 @@ namespace NavisworksTransport.Core.Animation /// public double CurrentYaw => _currentYaw; + /// + /// 获取当前动画物体高度(模型单位) + /// + private double GetAnimatedObjectHeight() + { + if (_isVirtualObject) + { + return _virtualObjectHeight; + } + + if (_animatedObject == null) + { + throw new InvalidOperationException("动画对象为空,无法获取物体高度"); + } + + var boundingBox = _animatedObject.BoundingBox(); + return boundingBox.Max.Z - boundingBox.Min.Z; + } + /// /// 获取当前碰撞检测精度 /// @@ -2984,7 +3080,14 @@ namespace NavisworksTransport.Core.Animation actualYaw += correctionRad; } - UpdateObjectPosition(frameData.Position, actualYaw); + if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail) + { + UpdateObjectPosition(frameData.Position, frameData.Rotation); + } + else + { + UpdateObjectPosition(frameData.Position, actualYaw); + } // 更新碰撞高亮(基于预计算结果) UpdateCollisionHighlightFromFrame(); @@ -3800,4 +3903,4 @@ namespace NavisworksTransport.Core.Animation #endregion } -} \ No newline at end of file +} diff --git a/src/Core/AssemblyReferencePathManager.cs b/src/Core/AssemblyReferencePathManager.cs new file mode 100644 index 0000000..8505cc3 --- /dev/null +++ b/src/Core/AssemblyReferencePathManager.cs @@ -0,0 +1,408 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Autodesk.Navisworks.Api; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.Core +{ + /// + /// 直线装配参考路径管理器。 + /// 优先使用单位圆柱体资源生成一根可点击的临时参考杆,供用户在 3D 视图中选择起点。 + /// 如果圆柱资源缺失,则退回到单位立方体资源。 + /// + public class AssemblyReferencePathManager + { + private const double ReferenceRodBaseSizeInMeters = 0.01; + + private static AssemblyReferencePathManager _instance; + private static readonly object _lock = new object(); + + private Model _referenceRodModel; + private ModelItem _referenceRodModelItem; + private bool _isUpdatingReferenceRod; + private bool _isReferenceRodVisible; + private Point3D _referenceLineStart = Point3D.Origin; + private Point3D _referenceLineEnd = Point3D.Origin; + private double _referenceRodDiameterInMeters; + private string _referenceResourceName; + + public static AssemblyReferencePathManager Instance + { + get + { + if (_instance == null) + { + lock (_lock) + { + if (_instance == null) + { + _instance = new AssemblyReferencePathManager(); + } + } + } + + return _instance; + } + } + + public ModelItem CurrentReferenceRod => _referenceRodModelItem; + public bool IsReferenceRodVisible => _isReferenceRodVisible; + public bool IsUpdatingReferenceRod => _isUpdatingReferenceRod; + public bool HasReferenceLine => _referenceRodModelItem != null && _isReferenceRodVisible; + public Point3D ReferenceLineStart => _referenceLineStart; + public Point3D ReferenceLineEnd => _referenceLineEnd; + public string ReferenceResourceName => _referenceResourceName; + + private AssemblyReferencePathManager() + { + _isReferenceRodVisible = false; + } + + /// + /// 创建或更新参考杆。 + /// + public ModelItem CreateOrUpdateReferenceRod(Point3D startPoint, Point3D endPoint, double diameterInMeters) + { + var doc = Application.ActiveDocument; + if (doc == null || doc.IsClear) + { + throw new InvalidOperationException("当前没有可用文档,无法创建装配参考杆"); + } + + double referenceLength = GeometryHelper.CalculatePointDistance(startPoint, endPoint); + if (referenceLength <= 1e-6) + { + throw new InvalidOperationException("参考线长度为 0,无法创建装配参考杆"); + } + + _isUpdatingReferenceRod = true; + try + { + EnsureReferenceRodModelLoaded(); + + double lengthInMeters = UnitsConverter.ConvertToMeters(referenceLength); + ScaleReferenceRod(lengthInMeters, diameterInMeters); + PositionReferenceRod(startPoint, endPoint); + + ShowReferenceRod(); + + _referenceLineStart = startPoint; + _referenceLineEnd = endPoint; + _referenceRodDiameterInMeters = diameterInMeters; + + LogManager.Info($"[装配参考杆] 已创建/更新,长度={lengthInMeters:F3}m,直径={diameterInMeters:F3}m"); + return _referenceRodModelItem; + } + finally + { + _isUpdatingReferenceRod = false; + } + } + + public void ShowReferenceRod() + { + if (_referenceRodModelItem == null) + { + return; + } + + var items = new ModelItemCollection { _referenceRodModelItem }; + Application.ActiveDocument.Models.SetHidden(items, false); + _isReferenceRodVisible = true; + } + + public void HideReferenceRod() + { + if (_referenceRodModelItem == null) + { + return; + } + + var items = new ModelItemCollection { _referenceRodModelItem }; + Application.ActiveDocument.Models.SetHidden(items, true); + _isReferenceRodVisible = false; + } + + /// + /// 将任意点击点投影到当前参考线。 + /// + public Point3D ProjectPointToReferenceLine(Point3D point) + { + if (!HasReferenceLine) + { + throw new InvalidOperationException("当前没有可用的装配参考线"); + } + + return ProjectPointToSegment(point, _referenceLineStart, _referenceLineEnd); + } + + public void Cleanup() + { + try + { + if (_referenceRodModel != null) + { + var doc = Application.ActiveDocument; + if (doc != null && !doc.IsClear) + { + int index = doc.Models.IndexOf(_referenceRodModel); + if (index >= 0) + { + doc.TryRemoveFile(index); + } + } + } + } + catch (Exception ex) + { + LogManager.Error($"清理装配参考杆失败: {ex.Message}"); + } + finally + { + _referenceRodModel = null; + _referenceRodModelItem = null; + _isReferenceRodVisible = false; + _referenceLineStart = Point3D.Origin; + _referenceLineEnd = Point3D.Origin; + _referenceRodDiameterInMeters = 0.0; + _referenceResourceName = null; + } + } + + private void EnsureReferenceRodModelLoaded() + { + if (_referenceRodModel != null && + Application.ActiveDocument.Models.IndexOf(_referenceRodModel) >= 0 && + _referenceRodModelItem != null) + { + return; + } + + string resourcePath = GetReferenceRodResourcePath(out string resourceName); + if (string.IsNullOrEmpty(resourcePath) || !File.Exists(resourcePath)) + { + throw new FileNotFoundException($"找不到参考杆资源文件: {resourcePath}"); + } + + var doc = Application.ActiveDocument; + int modelCountBefore = doc.Models.Count; + if (!doc.TryAppendFile(resourcePath)) + { + throw new InvalidOperationException($"无法追加文件: {resourcePath}"); + } + + if (doc.Models.Count <= modelCountBefore) + { + throw new InvalidOperationException("追加参考杆资源后模型数量未增加"); + } + + _referenceRodModel = doc.Models.Last(); + _referenceRodModelItem = FindFirstGeometryItem(_referenceRodModel.RootItem); + _isReferenceRodVisible = true; + _referenceResourceName = resourceName; + LogManager.Info($"[装配参考杆] 已加载资源: {resourceName}"); + } + + private void ScaleReferenceRod(double lengthInMeters, double diameterInMeters) + { + if (_referenceRodModel == null) + { + return; + } + + var currentTransform = _referenceRodModel.Transform; + var components = currentTransform.Factor(); + components.Scale = new Vector3D( + lengthInMeters / ReferenceRodBaseSizeInMeters, + diameterInMeters / ReferenceRodBaseSizeInMeters, + diameterInMeters / ReferenceRodBaseSizeInMeters); + + Transform3D scaledTransform = components.Combine(); + Application.ActiveDocument.Models.SetModelUnitsAndTransform( + _referenceRodModel, + _referenceRodModel.Units, + scaledTransform, + false); + } + + private void PositionReferenceRod(Point3D startPoint, Point3D endPoint) + { + if (_referenceRodModelItem == null) + { + return; + } + + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { _referenceRodModelItem }; + var currentScale = _referenceRodModelItem.Transform.Factor().Scale; + + doc.Models.ResetPermanentTransform(modelItems); + + var originalBounds = _referenceRodModelItem.BoundingBox(); + var originalCenter = originalBounds.Center; + var targetCenter = new Point3D( + (startPoint.X + endPoint.X) / 2.0, + (startPoint.Y + endPoint.Y) / 2.0, + (startPoint.Z + endPoint.Z) / 2.0); + + Rotation3D rotation = CreateLineRotation(startPoint, endPoint); + + var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); + var components = identity.Factor(); + components.Scale = currentScale; + components.Rotation = rotation; + + var preTransform = components.Combine(); + var linear = preTransform.Linear; + var transformedCenter = new Point3D( + linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z, + linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z, + linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z); + + components.Translation = new Vector3D( + targetCenter.X - transformedCenter.X, + targetCenter.Y - transformedCenter.Y, + targetCenter.Z - transformedCenter.Z); + + Transform3D transform = components.Combine(); + doc.Models.OverridePermanentTransform(modelItems, transform, false); + } + + private static Rotation3D CreateLineRotation(Point3D startPoint, Point3D endPoint) + { + var tangent = Normalize(new Vector3D( + endPoint.X - startPoint.X, + endPoint.Y - startPoint.Y, + endPoint.Z - startPoint.Z)); + + var worldUp = new Vector3D(0, 0, 1); + var projectedNormal = new Vector3D( + worldUp.X - Dot(worldUp, tangent) * tangent.X, + worldUp.Y - Dot(worldUp, tangent) * tangent.Y, + worldUp.Z - Dot(worldUp, tangent) * tangent.Z); + + if (VectorLengthSquared(projectedNormal) < 1e-9) + { + var fallbackUp = new Vector3D(0, 1, 0); + projectedNormal = new Vector3D( + fallbackUp.X - Dot(fallbackUp, tangent) * tangent.X, + fallbackUp.Y - Dot(fallbackUp, tangent) * tangent.Y, + fallbackUp.Z - Dot(fallbackUp, tangent) * tangent.Z); + } + + var normal = Normalize(projectedNormal); + var lateral = Normalize(Cross(normal, tangent)); + + return new Rotation3D( + new UnitVector3D(tangent), + new UnitVector3D(lateral), + new UnitVector3D(normal)); + } + + private static Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd) + { + double segmentX = segmentEnd.X - segmentStart.X; + double segmentY = segmentEnd.Y - segmentStart.Y; + double segmentZ = segmentEnd.Z - segmentStart.Z; + double lengthSquared = segmentX * segmentX + segmentY * segmentY + segmentZ * segmentZ; + + if (lengthSquared < 1e-9) + { + return segmentStart; + } + + double pointX = point.X - segmentStart.X; + double pointY = point.Y - segmentStart.Y; + double pointZ = point.Z - segmentStart.Z; + double projection = (pointX * segmentX + pointY * segmentY + pointZ * segmentZ) / lengthSquared; + projection = Math.Max(0.0, Math.Min(1.0, projection)); + + return new Point3D( + segmentStart.X + projection * segmentX, + segmentStart.Y + projection * segmentY, + segmentStart.Z + projection * segmentZ); + } + + private static Vector3D Cross(Vector3D a, Vector3D b) + { + return new Vector3D( + a.Y * b.Z - a.Z * b.Y, + a.Z * b.X - a.X * b.Z, + a.X * b.Y - a.Y * b.X); + } + + private static double Dot(Vector3D a, Vector3D b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z; + } + + private static Vector3D Normalize(Vector3D vector) + { + double lengthSquared = VectorLengthSquared(vector); + if (lengthSquared < 1e-9) + { + return new Vector3D(1, 0, 0); + } + + double length = Math.Sqrt(lengthSquared); + return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length); + } + + private static double VectorLengthSquared(Vector3D vector) + { + return vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z; + } + + private static ModelItem FindFirstGeometryItem(ModelItem item) + { + if (item == null) + { + return null; + } + + if (item.HasGeometry) + { + return item; + } + + foreach (var child in item.Children) + { + var geometryItem = FindFirstGeometryItem(child); + if (geometryItem != null) + { + return geometryItem; + } + } + + return item; + } + + private static string GetReferenceRodResourcePath(out string resourceName) + { + string pluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string[] preferredPaths = new[] + { + Path.Combine(pluginDir, "resources", "unit_cylinder.nwc"), + Path.Combine(pluginDir, "..", "..", "resources", "unit_cylinder.nwc"), + @"c:\Users\Tellme\apps\NavisworksTransport\resources\unit_cylinder.nwc", + Path.Combine(pluginDir, "resources", "unit_cube.nwc"), + Path.Combine(pluginDir, "..", "..", "resources", "unit_cube.nwc"), + @"c:\Users\Tellme\apps\NavisworksTransport\resources\unit_cube.nwc" + }; + + foreach (string path in preferredPaths) + { + if (File.Exists(path)) + { + resourceName = Path.GetFileName(path); + return path; + } + } + + resourceName = "unit_cylinder.nwc"; + return preferredPaths[0]; + } + } +} diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs index c797993..31812b6 100644 --- a/src/Core/PathDataManager.cs +++ b/src/Core/PathDataManager.cs @@ -48,6 +48,10 @@ namespace NavisworksTransport public string name { get; set; } public string description { get; set; } public string pathType { get; set; } + public string railMountMode { get; set; } + public string railPayloadAnchorMode { get; set; } + public string railPathDefinitionMode { get; set; } + public double railReferenceToAnchorOffset { get; set; } public double totalLength { get; set; } public JsonObjectLimits objectLimits { get; set; } public double gridSize { get; set; } @@ -275,6 +279,10 @@ namespace NavisworksTransport name = route.Name, description = route.Description ?? "", pathType = route.PathType.ToString(), + railMountMode = route.RailMountMode.ToString(), + railPayloadAnchorMode = route.RailPayloadAnchorMode.ToString(), + railPathDefinitionMode = route.RailPathDefinitionMode.ToString(), + railReferenceToAnchorOffset = Math.Round(route.RailReferenceToAnchorOffset, exportSettings?.Precision ?? 3), totalLength = Math.Round(route.TotalLength, exportSettings?.Precision ?? 3), objectLimits = new { @@ -512,6 +520,26 @@ namespace NavisworksTransport continue; } + if (!string.IsNullOrEmpty(jsonRoute.railMountMode) && + Enum.TryParse(jsonRoute.railMountMode, out var railMountMode)) + { + route.RailMountMode = railMountMode; + } + + if (!string.IsNullOrEmpty(jsonRoute.railPayloadAnchorMode) && + Enum.TryParse(jsonRoute.railPayloadAnchorMode, out var railPayloadAnchorMode)) + { + route.RailPayloadAnchorMode = railPayloadAnchorMode; + } + + if (!string.IsNullOrEmpty(jsonRoute.railPathDefinitionMode) && + Enum.TryParse(jsonRoute.railPathDefinitionMode, out var railPathDefinitionMode)) + { + route.RailPathDefinitionMode = railPathDefinitionMode; + } + + route.RailReferenceToAnchorOffset = jsonRoute.railReferenceToAnchorOffset; + // 解析路径点 var points = new List(); foreach (var jsonPoint in jsonRoute.points) @@ -1369,6 +1397,10 @@ namespace NavisworksTransport routeElement.SetAttribute("name", route.Name); routeElement.SetAttribute("description", route.Description ?? ""); routeElement.SetAttribute("pathType", route.PathType.ToString()); + routeElement.SetAttribute("railMountMode", route.RailMountMode.ToString()); + routeElement.SetAttribute("railPayloadAnchorMode", route.RailPayloadAnchorMode.ToString()); + routeElement.SetAttribute("railPathDefinitionMode", route.RailPathDefinitionMode.ToString()); + routeElement.SetAttribute("railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset.ToString("F3")); routeElement.SetAttribute("totalLength", route.TotalLength.ToString("F3")); routeElement.SetAttribute("maxObjectLength", route.MaxObjectLength.ToString("F3")); routeElement.SetAttribute("maxObjectWidth", route.MaxObjectWidth.ToString("F3")); @@ -1501,6 +1533,26 @@ namespace NavisworksTransport LiftHeight = double.TryParse(routeNode.Attributes?["liftHeight"]?.Value, out double liftHeight) ? liftHeight : 0 }; + if (Enum.TryParse(routeNode.Attributes?["railMountMode"]?.Value, out var railMountMode)) + { + route.RailMountMode = railMountMode; + } + + if (Enum.TryParse(routeNode.Attributes?["railPayloadAnchorMode"]?.Value, out var railPayloadAnchorMode)) + { + route.RailPayloadAnchorMode = railPayloadAnchorMode; + } + + if (Enum.TryParse(routeNode.Attributes?["railPathDefinitionMode"]?.Value, out var railPathDefinitionMode)) + { + route.RailPathDefinitionMode = railPathDefinitionMode; + } + + if (double.TryParse(routeNode.Attributes?["railReferenceToAnchorOffset"]?.Value, out double railReferenceToAnchorOffset)) + { + route.RailReferenceToAnchorOffset = railReferenceToAnchorOffset; + } + // 解析创建时间 if (DateTime.TryParse(routeNode.Attributes?["created"]?.Value, out DateTime createdTime)) { @@ -1727,4 +1779,4 @@ namespace NavisworksTransport return Errors.Concat(Warnings); } } -} \ No newline at end of file +} diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index b1757cd..b9d27e2 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -63,6 +63,7 @@ namespace NavisworksTransport // 创建数据库表结构 CreateTables(); + EnsurePathRoutesTableExtended(); if (isNewDatabase) { @@ -98,6 +99,10 @@ namespace NavisworksTransport GridSize REAL, PathType INTEGER, LiftHeight REAL, + RailMountMode INTEGER, + RailPayloadAnchorMode INTEGER, + RailPathDefinitionMode INTEGER, + RailReferenceToAnchorOffset REAL, CreatedTime DATETIME, LastModified DATETIME ) @@ -455,8 +460,8 @@ namespace NavisworksTransport // 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算 var sql = @" INSERT OR REPLACE INTO PathRoutes - (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, CreatedTime, LastModified) - VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @created, @modified) + (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPayloadAnchorMode, RailPathDefinitionMode, RailReferenceToAnchorOffset, CreatedTime, LastModified) + VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPayloadAnchorMode, @railPathDefinitionMode, @railReferenceToAnchorOffset, @created, @modified) "; using (var cmd = new SQLiteCommand(sql, _connection)) @@ -473,6 +478,10 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@gridSize", route.GridSize); cmd.Parameters.AddWithValue("@pathType", (int)route.PathType); cmd.Parameters.AddWithValue("@liftHeightMeters", route.LiftHeight); + cmd.Parameters.AddWithValue("@railMountMode", (int)route.RailMountMode); + cmd.Parameters.AddWithValue("@railPayloadAnchorMode", (int)route.RailPayloadAnchorMode); + cmd.Parameters.AddWithValue("@railPathDefinitionMode", (int)route.RailPathDefinitionMode); + cmd.Parameters.AddWithValue("@railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset); cmd.Parameters.AddWithValue("@created", route.CreatedTime); cmd.Parameters.AddWithValue("@modified", DateTime.Now); cmd.ExecuteNonQuery(); @@ -1515,6 +1524,10 @@ namespace NavisworksTransport GridSize = Convert.ToDouble(reader["GridSize"]), PathType = (PathType)Convert.ToInt32(reader["PathType"]), LiftHeight = Convert.ToDouble(reader["LiftHeight"]), + RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]), + RailPayloadAnchorMode = (RailPayloadAnchorMode)Convert.ToInt32(reader["RailPayloadAnchorMode"]), + RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]), + RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"]), CreatedTime = Convert.ToDateTime(reader["CreatedTime"]), LastModified = Convert.ToDateTime(reader["LastModified"]) }; @@ -2696,7 +2709,7 @@ namespace NavisworksTransport cmd.CommandText = @" SELECT Id, Name, CreatedTime, LastModified, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, - SafetyMargin, GridSize, PathType, LiftHeight + SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPayloadAnchorMode, RailPathDefinitionMode, RailReferenceToAnchorOffset FROM PathRoutes WHERE Id = @Id"; @@ -2720,7 +2733,11 @@ namespace NavisworksTransport SafetyMargin = Convert.ToDouble(reader["SafetyMargin"]), GridSize = Convert.ToDouble(reader["GridSize"]), PathType = (PathType)Convert.ToInt32(reader["PathType"]), - LiftHeight = Convert.ToDouble(reader["LiftHeight"]) + LiftHeight = Convert.ToDouble(reader["LiftHeight"]), + RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]), + RailPayloadAnchorMode = (RailPayloadAnchorMode)Convert.ToInt32(reader["RailPayloadAnchorMode"]), + RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]), + RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"]) }; // 加载路径点和路径边 @@ -2735,6 +2752,48 @@ namespace NavisworksTransport #endregion + /// + /// 检查并扩展 PathRoutes 表结构 + /// + public void EnsurePathRoutesTableExtended() + { + try + { + var checkSql = "PRAGMA table_info(PathRoutes)"; + var existingColumns = new HashSet(); + + using (var cmd = new SQLiteCommand(checkSql, _connection)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + existingColumns.Add(reader["name"].ToString().ToLower()); + } + } + + var columnsToAdd = new Dictionary + { + ["railmountmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailMountMode INTEGER DEFAULT 0", + ["railpayloadanchormode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPayloadAnchorMode INTEGER DEFAULT 0", + ["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0", + ["railreferencetoanchoroffset"] = "ALTER TABLE PathRoutes ADD COLUMN RailReferenceToAnchorOffset REAL DEFAULT 0" + }; + + foreach (var column in columnsToAdd) + { + if (!existingColumns.Contains(column.Key)) + { + ExecuteNonQuery(column.Value); + LogManager.Info($"扩展 PathRoutes 表: 添加字段 {column.Key}"); + } + } + } + catch (Exception ex) + { + LogManager.Error($"扩展 PathRoutes 表失败: {ex.Message}", ex); + } + } + #region 批处理队列管理(简化版) /// diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index e61ac37..f4b23f4 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -1600,6 +1600,12 @@ namespace NavisworksTransport { PathType = pathType }; + + if (pathType == PathType.Rail) + { + newRoute.RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine; + } + _editingRoute = newRoute; // 立即添加到路径集合,确保UI能正确找到Core路径 @@ -4194,27 +4200,30 @@ namespace NavisworksTransport return clickedPoint; } - // 找到最近的基准路径点 + // 找到最近的基准路径投影点 Point3D nearestPoint = null; double minDistance = double.MaxValue; double maxSnapDistance = 2.0; // 最大吸附距离(模型单位) foreach (var pathPoints in baselinePaths.Values) { - foreach (var point in pathPoints) + var snappedPoint = FindNearestPointOnPolyline(pathPoints, clickedPoint); + if (snappedPoint == null) { - double distance = CalculateDistance3D(clickedPoint, point); - if (distance < minDistance) - { - minDistance = distance; - nearestPoint = point; - } + continue; + } + + double distance = CalculateDistance3D(clickedPoint, snappedPoint); + if (distance < minDistance) + { + minDistance = distance; + nearestPoint = snappedPoint; } } if (nearestPoint != null && minDistance <= maxSnapDistance) { - LogManager.Info($"[空轨吸附] 点击点 ({clickedPoint.X:F2}, {clickedPoint.Y:F2}, {clickedPoint.Z:F2}) 吸附到基准路径点 ({nearestPoint.X:F2}, {nearestPoint.Y:F2}, {nearestPoint.Z:F2}),距离 {minDistance:F2}"); + LogManager.Info($"[空轨吸附] 点击点 ({clickedPoint.X:F2}, {clickedPoint.Y:F2}, {clickedPoint.Z:F2}) 吸附到基准路径投影点 ({nearestPoint.X:F2}, {nearestPoint.Y:F2}, {nearestPoint.Z:F2}),距离 {minDistance:F2}"); return nearestPoint; } else @@ -4241,6 +4250,66 @@ namespace NavisworksTransport return Math.Sqrt(dx * dx + dy * dy + dz * dz); } + /// + /// 查找折线上距离目标点最近的投影点 + /// + private Point3D FindNearestPointOnPolyline(List pathPoints, Point3D targetPoint) + { + if (pathPoints == null || pathPoints.Count == 0) + { + return null; + } + + if (pathPoints.Count == 1) + { + return pathPoints[0]; + } + + Point3D nearestPoint = null; + double minDistance = double.MaxValue; + + for (int i = 0; i < pathPoints.Count - 1; i++) + { + var projectedPoint = ProjectPointToSegment(targetPoint, pathPoints[i], pathPoints[i + 1]); + double distance = CalculateDistance3D(targetPoint, projectedPoint); + if (distance < minDistance) + { + minDistance = distance; + nearestPoint = projectedPoint; + } + } + + return nearestPoint ?? pathPoints[0]; + } + + /// + /// 将点投影到线段上 + /// + private Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd) + { + double segmentX = segmentEnd.X - segmentStart.X; + double segmentY = segmentEnd.Y - segmentStart.Y; + double segmentZ = segmentEnd.Z - segmentStart.Z; + + double segmentLengthSquared = segmentX * segmentX + segmentY * segmentY + segmentZ * segmentZ; + if (segmentLengthSquared < 1e-9) + { + return segmentStart; + } + + double pointX = point.X - segmentStart.X; + double pointY = point.Y - segmentStart.Y; + double pointZ = point.Z - segmentStart.Z; + + double projection = (pointX * segmentX + pointY * segmentY + pointZ * segmentZ) / segmentLengthSquared; + projection = Math.Max(0.0, Math.Min(1.0, projection)); + + return new Point3D( + segmentStart.X + projection * segmentX, + segmentStart.Y + projection * segmentY, + segmentStart.Z + projection * segmentZ); + } + /// /// 搜索并设置可通行的通道 /// diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index ec31b81..6c595e5 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -113,6 +113,54 @@ namespace NavisworksTransport Lateral } + /// + /// Rail 路径安装方式 + /// + public enum RailMountMode + { + /// + /// 轨下安装 + /// + UnderRail = 0, + + /// + /// 轨上安装 + /// + OverRail = 1 + } + + /// + /// Rail 路径中构件与安装头的对接方式 + /// + public enum RailPayloadAnchorMode + { + /// + /// 构件顶面中心与安装头对接 + /// + TopCenter = 0, + + /// + /// 构件底面中心与安装头对接 + /// + BottomCenter = 1 + } + + /// + /// Rail 路径的参考线定义方式 + /// + public enum RailPathDefinitionMode + { + /// + /// 旧版兼容模式:路径点即轨下悬挂参考点 + /// + LegacySuspensionPoint = 0, + + /// + /// 新版模式:路径点表示双轨参考中心线 + /// + RailCenterLine = 1 + } + /// /// 通道检测结果 /// @@ -690,6 +738,28 @@ namespace NavisworksTransport /// public double LiftHeight { get; set; } + /// + /// Rail 路径安装方式(仅当 PathType == Rail 时有效) + /// + public RailMountMode RailMountMode { get; set; } + + /// + /// Rail 路径中构件与安装头的对接方式(仅当 PathType == Rail 时有效) + /// + public RailPayloadAnchorMode RailPayloadAnchorMode { get; set; } + + /// + /// Rail 路径参考线定义方式(仅当 PathType == Rail 时有效) + /// + public RailPathDefinitionMode RailPathDefinitionMode { get; set; } + + /// + /// Rail 参考线到构件对接点的偏移距离(模型单位) + /// 在 LegacySuspensionPoint 模式下通常为 0。 + /// 在 RailCenterLine 模式下,结合 RailMountMode 解释为相对轨道中心线的上下偏移。 + /// + public double RailReferenceToAnchorOffset { get; set; } + // 数据库分析相关属性 /// /// 碰撞数量(从数据库加载) @@ -728,6 +798,10 @@ namespace NavisworksTransport Description = string.Empty; TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值 IsCurved = false; + RailMountMode = RailMountMode.UnderRail; + RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter; + RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; + RailReferenceToAnchorOffset = 0.0; } /// @@ -748,6 +822,10 @@ namespace NavisworksTransport IsCurved = false; LastModified = DateTime.Now; Description = string.Empty; + RailMountMode = RailMountMode.UnderRail; + RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter; + RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; + RailReferenceToAnchorOffset = 0.0; } /// @@ -980,7 +1058,11 @@ namespace NavisworksTransport MaxObjectLength = MaxObjectLength, MaxObjectWidth = MaxObjectWidth, MaxObjectHeight = MaxObjectHeight, - SafetyMargin = SafetyMargin + SafetyMargin = SafetyMargin, + RailMountMode = RailMountMode, + RailPayloadAnchorMode = RailPayloadAnchorMode, + RailPathDefinitionMode = RailPathDefinitionMode, + RailReferenceToAnchorOffset = RailReferenceToAnchorOffset }; // 克隆所有路径点 @@ -2627,4 +2709,4 @@ namespace NavisworksTransport } #endregion -} \ No newline at end of file +} diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index 3656bcb..d3fd3ab 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -1212,7 +1212,14 @@ namespace NavisworksTransport var result = new Dictionary>(); foreach (var kvp in _railBaselineVisualizations) { - result[kvp.Key] = kvp.Value.PathLineMarkers.Select(m => m.StartPoint).ToList(); + var pathPoints = kvp.Value.PathLineMarkers.Select(m => m.StartPoint).ToList(); + var lastMarker = kvp.Value.PathLineMarkers.LastOrDefault(); + if (lastMarker != null) + { + pathPoints.Add(lastMarker.EndPoint); + } + + result[kvp.Key] = pathPoints; } return result; } @@ -1490,8 +1497,9 @@ namespace NavisworksTransport } else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) { - // 空轨路径:路径点是轨道中心线位置,通行空间顶面对齐轨道,中心需要向上偏移半个高度 - verticalOffset = height / 2.0; + verticalOffset = RailPathPoseHelper.GetObjectSpaceCenterZOffset( + visualization.PathRoute, + height); } else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Hoisting) { @@ -1577,7 +1585,22 @@ namespace NavisworksTransport Point3D adjustedStartPoint; Point3D adjustedEndPoint; - if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) + if (_showObjectSpace && visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) + { + adjustedStartPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition( + visualization.PathRoute, + startPoint, + startPoint, + endPoint, + height); + adjustedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition( + visualization.PathRoute, + endPoint, + startPoint, + endPoint, + height); + } + else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) { // 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移) adjustedStartPoint = new Point3D( @@ -1648,7 +1671,39 @@ namespace NavisworksTransport Point3D adjustedEndPoint; List adjustedSampledPoints; - if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) + if (_showObjectSpace && visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) + { + adjustedStartPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition( + visualization.PathRoute, + startPoint, + startPoint, + endPoint, + height); + adjustedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition( + visualization.PathRoute, + endPoint, + startPoint, + endPoint, + height); + + adjustedSampledPoints = new List(); + if (edge.SampledPoints != null) + { + for (int sampleIndex = 0; sampleIndex < edge.SampledPoints.Count; sampleIndex++) + { + var currentPoint = edge.SampledPoints[sampleIndex]; + var previousPoint = sampleIndex > 0 ? edge.SampledPoints[sampleIndex - 1] : currentPoint; + var nextPoint = sampleIndex < edge.SampledPoints.Count - 1 ? edge.SampledPoints[sampleIndex + 1] : currentPoint; + adjustedSampledPoints.Add(RailPathPoseHelper.ResolveObjectSpaceCenterPosition( + visualization.PathRoute, + currentPoint, + previousPoint, + nextPoint, + height)); + } + } + } + else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) { // 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移) adjustedStartPoint = new Point3D( @@ -1888,7 +1943,22 @@ namespace NavisworksTransport Point3D adjustedStartPoint; Point3D adjustedEndPoint; - if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) + if (_showObjectSpace && visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) + { + adjustedStartPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition( + visualization.PathRoute, + startPoint.Position, + startPoint.Position, + endPoint.Position, + height); + adjustedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition( + visualization.PathRoute, + endPoint.Position, + startPoint.Position, + endPoint.Position, + height); + } + else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) { // 通行空间模式且有垂直偏移时 // 所有空中路径:统一沿Z轴向下偏移,顶面中心对齐路径点 @@ -3417,4 +3487,4 @@ namespace NavisworksTransport return $"CircleMarker[序号={SequenceNumber}, 类型={PointType}, 中心=({Center.X:F2},{Center.Y:F2},{Center.Z:F2}), 半径={Radius:F2}]"; } } -} \ No newline at end of file +} diff --git a/src/PathPlanning/RailGeometryHelper.cs b/src/PathPlanning/RailGeometryHelper.cs index e74e717..3a3b932 100644 --- a/src/PathPlanning/RailGeometryHelper.cs +++ b/src/PathPlanning/RailGeometryHelper.cs @@ -106,9 +106,14 @@ namespace NavisworksTransport.PathPlanning public class RailBaselinePath { /// - /// 完整的基准路径点列表(下表面中心线) + /// 完整的基准路径点列表 /// public List PathPoints { get; set; } + + /// + /// 基准路径的定义方式 + /// + public RailPathDefinitionMode DefinitionMode { get; set; } /// /// 路径总长度(模型单位) @@ -162,13 +167,28 @@ namespace NavisworksTransport.PathPlanning public static RailBaselinePath ExtractRailBaselinePath( ModelItem railModel, double samplingInterval = 0.5) + { + return ExtractRailBaselinePath(railModel, RailPathDefinitionMode.RailCenterLine, samplingInterval); + } + + /// + /// 步骤1: 提取空轨完整基准路径(用于可视化和吸附) + /// + /// 空轨模型项 + /// 基准线定义模式 + /// 采样间隔(模型单位) + /// 空轨基准路径信息 + public static RailBaselinePath ExtractRailBaselinePath( + ModelItem railModel, + RailPathDefinitionMode definitionMode, + double samplingInterval) { try { - LogManager.Debug($"[空轨] 步骤1: 开始提取完整基准路径: {railModel.DisplayName}"); + LogManager.Debug($"[空轨] 步骤1: 开始提取完整基准路径: {railModel.DisplayName}, 模式={definitionMode}"); // 检查缓存 - var cacheKey = $"baseline_{railModel.DisplayName}_{railModel.GetHashCode()}"; + var cacheKey = $"baseline_{definitionMode}_{railModel.DisplayName}_{railModel.GetHashCode()}"; if (_baselinePathCache.TryGetValue(cacheKey, out var cachedPath)) { // 检查缓存是否过期 @@ -185,7 +205,9 @@ namespace NavisworksTransport.PathPlanning } // 使用 OBB 方法提取基准路径 - var pathPoints = ExtractRailBottomCenterLineByOBB(railModel, samplingInterval); + var pathPoints = definitionMode == RailPathDefinitionMode.RailCenterLine + ? ExtractRailCenterLineByOBB(railModel, samplingInterval) + : ExtractRailBottomCenterLineByOBB(railModel, samplingInterval); LogManager.Debug($"[空轨] 生成 {pathPoints.Count} 个采样点"); // 计算路径长度 @@ -195,6 +217,7 @@ namespace NavisworksTransport.PathPlanning var baselinePath = new RailBaselinePath { PathPoints = pathPoints, + DefinitionMode = definitionMode, TotalLength = totalLength, StartPoint = pathPoints.First(), EndPoint = pathPoints.Last(), @@ -205,7 +228,7 @@ namespace NavisworksTransport.PathPlanning // 加入缓存 _baselinePathCache[cacheKey] = baselinePath; - LogManager.Debug($"[空轨] 步骤1完成: 基准路径总长度 {totalLength:F2},{pathPoints.Count}个采样点"); + LogManager.Debug($"[空轨] 步骤1完成: 基准路径总长度 {totalLength:F2},{pathPoints.Count}个采样点,模式={definitionMode}"); return baselinePath; } catch (Exception ex) @@ -241,19 +264,8 @@ namespace NavisworksTransport.PathPlanning LogManager.Debug($"[空轨] 步骤3: 吸附点到基准路径,输入点=({inputPoint.X:F2}, {inputPoint.Y:F2}, {inputPoint.Z:F2})"); - Point3D nearestPoint = baselinePath.PathPoints[0]; - double minDistance = double.MaxValue; - - // 找到最近的路径点 - foreach (var pathPoint in baselinePath.PathPoints) - { - double distance = CalculateDistance2D(inputPoint, pathPoint); - if (distance < minDistance) - { - minDistance = distance; - nearestPoint = pathPoint; - } - } + Point3D nearestPoint = FindNearestPointOnPath(baselinePath.PathPoints, inputPoint); + double minDistance = CalculateDistance2D(inputPoint, nearestPoint); // 检查是否在最大吸附距离内 if (minDistance <= maxSnapDistance) @@ -361,6 +373,66 @@ namespace NavisworksTransport.PathPlanning return nearestIndex; } + /// + /// 找到路径上距离目标点最近的投影点 + /// + private static Point3D FindNearestPointOnPath(List pathPoints, Point3D targetPoint) + { + if (pathPoints == null || pathPoints.Count == 0) + { + return new Point3D(); + } + + if (pathPoints.Count == 1) + { + return pathPoints[0]; + } + + Point3D nearestPoint = pathPoints[0]; + double minDistance = double.MaxValue; + + for (int i = 0; i < pathPoints.Count - 1; i++) + { + var projectedPoint = ProjectPointToSegment(targetPoint, pathPoints[i], pathPoints[i + 1]); + double distance = CalculateDistance2D(targetPoint, projectedPoint); + if (distance < minDistance) + { + minDistance = distance; + nearestPoint = projectedPoint; + } + } + + return nearestPoint; + } + + /// + /// 将点投影到线段上 + /// + private static Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd) + { + double segmentX = segmentEnd.X - segmentStart.X; + double segmentY = segmentEnd.Y - segmentStart.Y; + double segmentZ = segmentEnd.Z - segmentStart.Z; + + double segmentLengthSquared = segmentX * segmentX + segmentY * segmentY + segmentZ * segmentZ; + if (segmentLengthSquared < 1e-9) + { + return segmentStart; + } + + double pointX = point.X - segmentStart.X; + double pointY = point.Y - segmentStart.Y; + double pointZ = point.Z - segmentStart.Z; + + double projection = (pointX * segmentX + pointY * segmentY + pointZ * segmentZ) / segmentLengthSquared; + projection = Math.Max(0.0, Math.Min(1.0, projection)); + + return new Point3D( + segmentStart.X + projection * segmentX, + segmentStart.Y + projection * segmentY, + segmentStart.Z + projection * segmentZ); + } + /// /// 重采样路径 /// @@ -395,8 +467,10 @@ namespace NavisworksTransport.PathPlanning /// public static void ClearBaselinePathCache(ModelItem railModel) { - var cacheKey = $"baseline_{railModel.DisplayName}_{railModel.GetHashCode()}"; - _baselinePathCache.Remove(cacheKey); + var legacyCacheKey = $"baseline_{RailPathDefinitionMode.LegacySuspensionPoint}_{railModel.DisplayName}_{railModel.GetHashCode()}"; + var centerCacheKey = $"baseline_{RailPathDefinitionMode.RailCenterLine}_{railModel.DisplayName}_{railModel.GetHashCode()}"; + _baselinePathCache.Remove(legacyCacheKey); + _baselinePathCache.Remove(centerCacheKey); LogManager.Debug($"[空轨] 已清除模型 {railModel.DisplayName} 的基准路径缓存"); } @@ -465,7 +539,114 @@ namespace NavisworksTransport.PathPlanning } /// - /// 使用 OBB 包围体提取空轨下表面中心线(新方法,支持倾斜) + /// 使用 OBB 包围体提取空轨参考中心线 + /// + /// 空轨模型项 + /// 采样间隔(模型单位),默认0.5 + /// 路径点列表(轨道参考中心线) + public static List ExtractRailCenterLineByOBB( + ModelItem railModel, + double samplingInterval = 0.5) + { + try + { + LogManager.Debug($"[空轨] 使用 OBB 方法提取轨道参考中心线: {railModel.DisplayName}"); + + var obb = ComputeOrientedBoundingBox(railModel); + var geometryCenterLine = TryExtractDualRailCenterLine(railModel, obb, samplingInterval); + if (geometryCenterLine != null && geometryCenterLine.Count >= 2) + { + LogManager.Debug($"[空轨] 双轨几何中心线提取完成: 采样点数={geometryCenterLine.Count}"); + return geometryCenterLine; + } + + LogManager.Warning("[空轨] 双轨几何中心线提取失败,退回 OBB 主轴中线"); + return BuildCenterLineFromOffsets(obb, 0.0, 0.0, samplingInterval); + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 提取轨道参考中心线失败: {ex.Message}"); + throw; + } + } + + private static List TryExtractDualRailCenterLine( + ModelItem railModel, + OrientedBoundingBox obb, + double samplingInterval) + { + var triangles = GeometryHelper.ExtractTriangles(new[] { railModel }); + var pointSet = new HashSet(); + foreach (var triangle in triangles) + { + pointSet.Add(triangle.Point1); + pointSet.Add(triangle.Point2); + pointSet.Add(triangle.Point3); + } + + var points = pointSet.ToList(); + if (points.Count < 20) + { + LogManager.Warning($"[空轨] 双轨几何中心线样本不足: {points.Count}"); + return null; + } + + int verticalAxisIndex = GetMostVerticalAxisIndex(obb); + int acrossAxisIndex = verticalAxisIndex == 1 ? 2 : 1; + + var localPoints = points + .Select(point => ProjectPointToObbLocal(point, obb)) + .ToList(); + + var railBandPoints = ExtractDominantRailBand(localPoints, verticalAxisIndex); + if (railBandPoints.Count < 10) + { + LogManager.Warning($"[空轨] 双轨几何中心线有效轨面点不足: {railBandPoints.Count}"); + return null; + } + + double centerAcross = EstimateDualRailAcrossCenter(railBandPoints, acrossAxisIndex, obb.GetAxisLength(acrossAxisIndex)); + double centerVertical = railBandPoints.Average(point => GetAxisValue(point, verticalAxisIndex)); + + LogManager.Debug($"[空轨] 双轨几何中心估计: across={centerAcross:F3}, vertical={centerVertical:F3}, verticalAxis={verticalAxisIndex}, acrossAxis={acrossAxisIndex}"); + return BuildCenterLineFromOffsets(obb, centerAcross, centerVertical, samplingInterval, acrossAxisIndex, verticalAxisIndex); + } + + private static List BuildCenterLineFromOffsets( + OrientedBoundingBox obb, + double acrossOffset, + double verticalOffset, + double samplingInterval, + int acrossAxisIndex = 1, + int verticalAxisIndex = 2) + { + var principalAxis = obb.GetAxis(0); + var principalLength = obb.GetAxisLength(0); + var halfLength = principalLength / 2.0; + var acrossAxis = obb.GetAxis(acrossAxisIndex); + var verticalAxis = obb.GetAxis(verticalAxisIndex); + + var offsetCenter = new Point3D( + obb.Center.X + acrossAxis.X * acrossOffset + verticalAxis.X * verticalOffset, + obb.Center.Y + acrossAxis.Y * acrossOffset + verticalAxis.Y * verticalOffset, + obb.Center.Z + acrossAxis.Z * acrossOffset + verticalAxis.Z * verticalOffset); + + var start = new Point3D( + offsetCenter.X - principalAxis.X * halfLength, + offsetCenter.Y - principalAxis.Y * halfLength, + offsetCenter.Z - principalAxis.Z * halfLength); + var end = new Point3D( + offsetCenter.X + principalAxis.X * halfLength, + offsetCenter.Y + principalAxis.Y * halfLength, + offsetCenter.Z + principalAxis.Z * halfLength); + + var sampledPath = SampleLine(start, end, samplingInterval); + LogManager.Debug($"[空轨] 轨道参考中心线提取完成: 主轴长度={principalLength:F2}, 采样点数={sampledPath.Count}"); + return sampledPath; + } + + /// + /// 使用 OBB 包围体提取空轨下表面中心线(旧方法,支持倾斜) /// /// 空轨模型项 /// 采样间隔(模型单位),默认0.5 @@ -794,6 +975,193 @@ namespace NavisworksTransport.PathPlanning return result; } + private static int GetMostVerticalAxisIndex(OrientedBoundingBox obb) + { + double bestAbsDot = double.MinValue; + int bestIndex = 2; + + for (int i = 1; i < 3; i++) + { + var axis = obb.GetAxis(i); + double absDot = Math.Abs(axis.Z); + if (absDot > bestAbsDot) + { + bestAbsDot = absDot; + bestIndex = i; + } + } + + return bestIndex; + } + + private static Point3D ProjectPointToObbLocal(Point3D point, OrientedBoundingBox obb) + { + double dx = point.X - obb.Center.X; + double dy = point.Y - obb.Center.Y; + double dz = point.Z - obb.Center.Z; + + return new Point3D( + dx * obb.Axes[0].X + dy * obb.Axes[0].Y + dz * obb.Axes[0].Z, + dx * obb.Axes[1].X + dy * obb.Axes[1].Y + dz * obb.Axes[1].Z, + dx * obb.Axes[2].X + dy * obb.Axes[2].Y + dz * obb.Axes[2].Z); + } + + private static List ExtractDominantRailBand(List localPoints, int verticalAxisIndex) + { + const int BinCount = 24; + + double minValue = localPoints.Min(point => GetAxisValue(point, verticalAxisIndex)); + double maxValue = localPoints.Max(point => GetAxisValue(point, verticalAxisIndex)); + double range = maxValue - minValue; + if (range < 1e-6) + { + return new List(localPoints); + } + + double binSize = range / BinCount; + var bins = new List[BinCount]; + for (int i = 0; i < BinCount; i++) + { + bins[i] = new List(); + } + + foreach (var point in localPoints) + { + double value = GetAxisValue(point, verticalAxisIndex); + int binIndex = (int)((value - minValue) / binSize); + binIndex = Math.Max(0, Math.Min(BinCount - 1, binIndex)); + bins[binIndex].Add(point); + } + + int dominantBin = 0; + int dominantCount = bins[0].Count; + for (int i = 1; i < bins.Length; i++) + { + if (bins[i].Count > dominantCount) + { + dominantBin = i; + dominantCount = bins[i].Count; + } + } + + var dominantPoints = new List(); + int startBin = Math.Max(0, dominantBin - 1); + int endBin = Math.Min(BinCount - 1, dominantBin + 1); + for (int i = startBin; i <= endBin; i++) + { + dominantPoints.AddRange(bins[i]); + } + + return dominantPoints.Count > 0 ? dominantPoints : localPoints; + } + + private static double EstimateDualRailAcrossCenter(List railBandPoints, int acrossAxisIndex, double acrossAxisLength) + { + var acrossValues = railBandPoints + .Select(point => GetAxisValue(point, acrossAxisIndex)) + .OrderBy(value => value) + .ToList(); + + if (acrossValues.Count < 2) + { + return acrossValues.Count == 1 ? acrossValues[0] : 0.0; + } + + double minAcross = acrossValues.First(); + double maxAcross = acrossValues.Last(); + double totalWidth = maxAcross - minAcross; + if (totalWidth < Math.Max(1e-6, acrossAxisLength * 0.05)) + { + return (minAcross + maxAcross) / 2.0; + } + + double centerA = Percentile(acrossValues, 0.25); + double centerB = Percentile(acrossValues, 0.75); + + for (int iteration = 0; iteration < 8; iteration++) + { + double sumA = 0.0; + double sumB = 0.0; + int countA = 0; + int countB = 0; + + foreach (double value in acrossValues) + { + if (Math.Abs(value - centerA) <= Math.Abs(value - centerB)) + { + sumA += value; + countA++; + } + else + { + sumB += value; + countB++; + } + } + + if (countA == 0 || countB == 0) + { + break; + } + + centerA = sumA / countA; + centerB = sumB / countB; + } + + if (Math.Abs(centerA - centerB) < acrossAxisLength * 0.1) + { + return Median(acrossValues); + } + + return (centerA + centerB) / 2.0; + } + + private static double GetAxisValue(Point3D point, int axisIndex) + { + switch (axisIndex) + { + case 0: + return point.X; + case 1: + return point.Y; + case 2: + return point.Z; + default: + throw new ArgumentOutOfRangeException(nameof(axisIndex)); + } + } + + private static double Percentile(List sortedValues, double percentile) + { + if (sortedValues == null || sortedValues.Count == 0) + { + return 0.0; + } + + if (sortedValues.Count == 1) + { + return sortedValues[0]; + } + + double clamped = Math.Max(0.0, Math.Min(1.0, percentile)); + double position = clamped * (sortedValues.Count - 1); + int lowerIndex = (int)Math.Floor(position); + int upperIndex = (int)Math.Ceiling(position); + + if (lowerIndex == upperIndex) + { + return sortedValues[lowerIndex]; + } + + double t = position - lowerIndex; + return sortedValues[lowerIndex] + (sortedValues[upperIndex] - sortedValues[lowerIndex]) * t; + } + + private static double Median(List sortedValues) + { + return Percentile(sortedValues, 0.5); + } + /// /// 筛选下表面三角形(法线向下,Z < 0) /// @@ -1070,4 +1438,4 @@ namespace NavisworksTransport.PathPlanning } } } -} \ No newline at end of file +} diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 84139bf..db79b0b 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -43,6 +43,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels public string Description { get; set; } } + /// + /// Rail 构型选项辅助类 + /// + /// 枚举类型 + public class RailConfigOption + { + /// + /// 配置值 + /// + public T Value { get; set; } + + /// + /// 显示名称 + /// + public string DisplayName { get; set; } + + /// + /// 详细描述 + /// + public string Description { get; set; } + } + /// /// 路径编辑页面专用ViewModel /// @@ -96,6 +118,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 路径策略参数 private PathStrategyOption _selectedPathStrategy; private ObservableCollection _pathStrategyOptions; + private ObservableCollection> _railMountModeOptions; + private ObservableCollection> _railPayloadAnchorModeOptions; + private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; + private const double DefaultAssemblyReferenceRodDiameterInMeters = 0.2; + private string _assemblyTerminalObjectName = "未选择"; + private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体"; + private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters; + private double _assemblyReferenceRodDiameterInMeters = DefaultAssemblyReferenceRodDiameterInMeters; + private string _assemblyStartPointText = "未选择"; + private RailMountMode _assemblyMountMode = RailMountMode.UnderRail; + private RailPayloadAnchorMode _assemblyPayloadAnchorMode = RailPayloadAnchorMode.TopCenter; + private bool _hasAssemblyTerminalObject; + private bool _isSelectingAssemblyStartPoint; + private ModelItem _assemblyTerminalObject; + private Point3D _assemblyStartPoint; + private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; // 自动路径起点和终点的路径对象引用(用于正确的ID管理) private PathRoute _autoPathStartPointRoute = null; @@ -192,6 +230,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 🔥 路径类型改变时,检查是否可以使用路径线 OnPropertyChanged(nameof(CanUsePathLines)); + OnPropertyChanged(nameof(IsRailRouteSelected)); + OnPropertyChanged(nameof(SelectedRailMountMode)); + OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode)); if (!CanUsePathLines && ShowPathLines) { // 吊装和空轨路径不能使用路径线,自动关闭 @@ -248,6 +289,131 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } } + + public bool IsRailRouteSelected => SelectedPathRoute?.PathType == PathType.Rail; + + public ObservableCollection> RailMountModeOptions + { + get => _railMountModeOptions; + } + + public ObservableCollection> RailPayloadAnchorModeOptions + { + get => _railPayloadAnchorModeOptions; + } + + public RailMountMode SelectedRailMountMode + { + get => GetSelectedCoreRoute()?.RailMountMode ?? RailMountMode.UnderRail; + set + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail || coreRoute.RailMountMode == value) + { + return; + } + + coreRoute.RailMountMode = value; + ApplyRailRouteConfigurationChange(coreRoute, $"安装方式已更新为 {value}"); + OnPropertyChanged(nameof(SelectedRailMountMode)); + } + } + + public RailPayloadAnchorMode SelectedRailPayloadAnchorMode + { + get => GetSelectedCoreRoute()?.RailPayloadAnchorMode ?? RailPayloadAnchorMode.TopCenter; + set + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail || coreRoute.RailPayloadAnchorMode == value) + { + return; + } + + coreRoute.RailPayloadAnchorMode = value; + ApplyRailRouteConfigurationChange(coreRoute, $"对接基准已更新为 {value}"); + OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode)); + } + } + + public string AssemblyTerminalObjectName + { + get => _assemblyTerminalObjectName; + set => SetProperty(ref _assemblyTerminalObjectName, value); + } + + public string AssemblyTerminalObjectInfo + { + get => _assemblyTerminalObjectInfo; + set => SetProperty(ref _assemblyTerminalObjectInfo, value); + } + + public double AssemblyReferenceRodLengthInMeters + { + get => _assemblyReferenceRodLengthInMeters; + set + { + if (SetProperty(ref _assemblyReferenceRodLengthInMeters, value)) + { + OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); + } + } + } + + public double AssemblyReferenceRodDiameterInMeters + { + get => _assemblyReferenceRodDiameterInMeters; + set + { + if (SetProperty(ref _assemblyReferenceRodDiameterInMeters, value)) + { + OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); + } + } + } + + public bool HasAssemblyTerminalObject + { + get => _hasAssemblyTerminalObject; + set => SetProperty(ref _hasAssemblyTerminalObject, value); + } + + public string AssemblyStartPointText + { + get => _assemblyStartPointText; + set => SetProperty(ref _assemblyStartPointText, value); + } + + public RailPayloadAnchorMode AssemblyPayloadAnchorMode + { + get => _assemblyPayloadAnchorMode; + set + { + if (SetProperty(ref _assemblyPayloadAnchorMode, value) && HasAssemblyTerminalObject) + { + RefreshAssemblyTerminalObjectInfo(); + RefreshAssemblyReferenceRodIfNeeded(); + } + } + } + + public RailMountMode AssemblyMountMode + { + get => _assemblyMountMode; + set + { + if (SetProperty(ref _assemblyMountMode, value) && HasAssemblyTerminalObject) + { + RefreshAssemblyTerminalObjectInfo(); + } + } + } + + public bool IsSelectingAssemblyStartPoint + { + get => _isSelectingAssemblyStartPoint; + set => SetProperty(ref _isSelectingAssemblyStartPoint, value); + } /// /// 更新路径可视化显示:清理其他路径,只显示当前选中的路径 /// @@ -763,6 +929,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand ImportPathCommand { get; private set; } public ICommand ExportPathCommand { get; private set; } public ICommand SaveAsPathCommand { get; private set; } + public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; } + public ICommand GenerateAssemblyReferenceRodCommand { get; private set; } + public ICommand SelectAssemblyStartPointCommand { get; private set; } + public ICommand ClearAssemblyReferenceRodCommand { get; private set; } // 多层吊装命令 public ICommand SelectMultiLevelStartPointCommand { get; private set; } @@ -798,6 +968,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels public bool CanExecuteExportPath => _pathPlanningManager?.Routes?.Count > 0 || SelectedPathRoute != null; public bool CanExecuteSaveAsPath => SelectedPathRoute != null && SelectedPathRoute.Points.Count > 0; + public bool CanGenerateAssemblyReferenceRod => HasAssemblyTerminalObject && + AssemblyReferenceRodLengthInMeters > 0 && + AssemblyReferenceRodDiameterInMeters > 0; + public bool CanSelectAssemblyStartPoint => HasAssemblyTerminalObject && + _pathPlanningManager != null && + AssemblyReferencePathManager.Instance.HasReferenceLine && + !IsSelectingAssemblyStartPoint; public bool CanExecuteModifyPoint => SelectedPathPoint != null && _pathPlanningManager?.PathEditState != PathEditState.EditingPoint; @@ -835,6 +1012,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 初始化路径策略选项 InitializePathStrategyOptions(); + InitializeRailConfigOptions(); // 从配置加载参数 LoadParametersFromConfig(); @@ -897,6 +1075,66 @@ namespace NavisworksTransport.UI.WPF.ViewModels _selectedPathStrategy = _pathStrategyOptions.FirstOrDefault(x => x.Value == PathStrategy.Shortest); } + /// + /// 初始化 Rail 构型选项 + /// + private void InitializeRailConfigOptions() + { + _railMountModeOptions = new ObservableCollection> + { + new RailConfigOption + { + Value = RailMountMode.UnderRail, + DisplayName = "轨下安装", + Description = "安装头位于双轨下方,构件从轨道平面下侧连接" + }, + new RailConfigOption + { + Value = RailMountMode.OverRail, + DisplayName = "轨上安装", + Description = "安装头位于双轨上方,构件从轨道平面上侧连接" + } + }; + + _railPayloadAnchorModeOptions = new ObservableCollection> + { + new RailConfigOption + { + Value = RailPayloadAnchorMode.TopCenter, + DisplayName = "顶面对接", + Description = "以箱体顶面中心与安装头刚性对接" + }, + new RailConfigOption + { + Value = RailPayloadAnchorMode.BottomCenter, + DisplayName = "底面对接", + Description = "以箱体底面中心与安装头刚性对接" + } + }; + } + + private PathRoute GetSelectedCoreRoute() + { + if (_pathPlanningManager == null || SelectedPathRoute == null) + { + return null; + } + + return _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == SelectedPathRoute.Name); + } + + private void ApplyRailRouteConfigurationChange(PathRoute coreRoute, string logMessage) + { + coreRoute.LastModified = DateTime.Now; + + SyncObjectParametersToRenderPlugin(); + _pathPlanningManager.DrawRouteVisualization(coreRoute, isAutoPath: false); + _pathPlanningManager.SavePathToDatabase(coreRoute); + _pathPlanningManager.RaiseVisualizationStateChanged(); + + LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}"); + } + /// /// 从配置文件加载参数 /// @@ -1031,6 +1269,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels ImportPathCommand = new RelayCommand(async () => await ExecuteImportPathAsync()); ExportPathCommand = new RelayCommand(async () => await ExecuteExportPathAsync(), () => CanExecuteExportPath); SaveAsPathCommand = new RelayCommand(async () => await ExecuteSaveAsPathAsync(), () => CanExecuteSaveAsPath); + CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync()); + GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod); + SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint); + ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod()); } #endregion @@ -1039,6 +1281,349 @@ namespace NavisworksTransport.UI.WPF.ViewModels #region 路径管理命令 + private async Task ExecuteCaptureAssemblyTerminalObjectAsync() + { + await SafeExecuteAsync(() => + { + var document = NavisApplication.ActiveDocument; + var selectedItem = document?.CurrentSelection?.SelectedItems?.FirstOrDefault(); + if (selectedItem == null) + { + throw new InvalidOperationException("请先在 Navisworks 中选择终点处已安装的箱体"); + } + + if (!ModelItemAnalysisHelper.IsModelItemValid(selectedItem)) + { + throw new InvalidOperationException("当前选择对象无效,请重新选择终点箱体"); + } + + _assemblyTerminalObject = selectedItem; + _assemblyStartPoint = Point3D.Origin; + HasAssemblyTerminalObject = true; + AssemblyStartPointText = "未选择"; + AssemblyTerminalObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(selectedItem); + RefreshAssemblyTerminalObjectInfo(); + RenderAssemblyAnchorMarker(); + OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}"); + LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}"); + }, "捕获终点箱体"); + } + + private async Task ExecuteGenerateAssemblyReferenceRodAsync() + { + await SafeExecuteAsync(() => + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体"); + } + + var forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform); + var endPoint = GetAssemblyTerminalAnchorPoint(); + double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters); + var startPoint = new Point3D( + endPoint.X - forward.X * rodLength, + endPoint.Y - forward.Y * rodLength, + endPoint.Z - forward.Z * rodLength); + + AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod( + startPoint, + endPoint, + AssemblyReferenceRodDiameterInMeters); + + _assemblyStartPoint = Point3D.Origin; + AssemblyStartPointText = "未选择"; + RefreshAssemblyTerminalObjectInfo(startPoint, endPoint); + RenderAssemblyAnchorMarker(); + + UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认位置"); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + LogManager.Info($"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}"); + }, "生成装配参考杆"); + } + + private async Task ExecuteSelectAssemblyStartPointAsync() + { + await SafeExecuteAsync(() => + { + if (_pathPlanningManager == null) + { + throw new InvalidOperationException("路径规划管理器未初始化,无法选择装配起点"); + } + + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体"); + } + + if (!AssemblyReferencePathManager.Instance.HasReferenceLine) + { + throw new InvalidOperationException("请先生成直线装配参考杆"); + } + + CleanupAssemblyReferenceSelection(); + + _pathPlanningManager.DisableMouseHandling(); + IsSelectingAssemblyStartPoint = true; + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + + PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked; + PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked; + + if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) + { + CleanupAssemblyReferenceSelection(); + throw new InvalidOperationException("ToolPlugin 初始化失败,请重试"); + } + + _pathPlanningManager.StartClickTool(PathPointType.StartPoint); + UpdateMainStatus("请在参考杆附近点击一个起点,系统会自动吸附到参考线上并生成直线路径"); + LogManager.Info("[直线装配] 已进入起点拾取模式"); + }, "选择直线装配起点"); + } + + private void ExecuteClearAssemblyReferenceRod() + { + try + { + CleanupAssemblyReferenceSelection(); + AssemblyReferencePathManager.Instance.HideReferenceRod(); + ClearAssemblyAnchorMarker(); + AssemblyStartPointText = "未选择"; + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + UpdateMainStatus("已隐藏直线装配参考杆"); + LogManager.Info("[直线装配] 已隐藏参考杆"); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 隐藏参考杆失败: {ex.Message}"); + } + } + + private async void OnAssemblyReferenceMouseClicked(object sender, PickItemResult pickResult) + { + try + { + if (!IsSelectingAssemblyStartPoint || pickResult == null) + { + return; + } + + Point3D projectedStartPoint = AssemblyReferencePathManager.Instance.ProjectPointToReferenceLine(pickResult.Point); + _assemblyStartPoint = projectedStartPoint; + AssemblyStartPointText = $"({projectedStartPoint.X:F2}, {projectedStartPoint.Y:F2}, {projectedStartPoint.Z:F2})"; + + await SafeExecuteAsync(() => + { + CreateAssemblyLinearRoute(projectedStartPoint); + CleanupAssemblyReferenceSelection(); + UpdateMainStatus("已根据参考杆起点生成直线装配路径"); + }, "生成直线装配路径"); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 处理起点拾取失败: {ex.Message}", ex); + UpdateMainStatus($"直线装配起点拾取失败: {ex.Message}"); + CleanupAssemblyReferenceSelection(); + } + } + + private void CreateAssemblyLinearRoute(Point3D startPoint) + { + Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd; + string routeName = $"直线装配_{_pathPlanningManager.Routes.Count + 1}"; + + var route = new PathRoute(routeName) + { + Description = $"直线装配路径 - {AssemblyTerminalObjectName}", + PathType = PathType.Rail, + RailMountMode = AssemblyMountMode, + RailPayloadAnchorMode = AssemblyPayloadAnchorMode, + RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine + }; + + route.AddPoint(new PathPoint(_assemblyStartPoint, "装配起点", PathPointType.StartPoint)); + route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint)); + + if (!_pathPlanningManager.AddRoute(route)) + { + throw new InvalidOperationException("装配路径添加失败"); + } + + _pathPlanningManager.SetCurrentRoute(route); + _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false); + _pathPlanningManager.SavePathToDatabase(route); + + if (!_pathPlanningManager.GeneratePath(route)) + { + throw new InvalidOperationException("装配路径生成失败"); + } + + AssemblyTerminalObjectInfo = string.Format( + "对接终点=({0:F2}, {1:F2}, {2:F2}),装配起点={3},安装方式={4},对接基准={5}", + endPoint.X, + endPoint.Y, + endPoint.Z, + AssemblyStartPointText, + AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装", + AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接"); + + OnPropertyChanged(nameof(PathRoutes)); + LogManager.Info($"[直线装配] 已生成路径: {route.Name}"); + } + + private Point3D GetAssemblyTerminalAnchorPoint() + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + throw new InvalidOperationException("终点箱体未设置或已失效,无法计算对接点"); + } + + var bounds = _assemblyTerminalObject.BoundingBox(); + var center = bounds.Center; + Vector3D up = ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform); + Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, _assemblyTerminalObject.Transform); + double halfHeight = Math.Max(0.0, localSize.Z / 2.0); + double direction = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? 1.0 : -1.0; + + return new Point3D( + center.X + up.X * halfHeight * direction, + center.Y + up.Y * halfHeight * direction, + center.Z + up.Z * halfHeight * direction); + } + + private void RefreshAssemblyTerminalObjectInfo(Point3D referenceStartPoint = null, Point3D referenceEndPoint = null) + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + return; + } + + var bounds = _assemblyTerminalObject.BoundingBox(); + Point3D anchorPoint = GetAssemblyTerminalAnchorPoint(); + string anchorText = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接"; + string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装"; + + if (referenceStartPoint != null && referenceEndPoint != null) + { + AssemblyTerminalObjectInfo = string.Format( + "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),参考杆起点=({11:F2}, {12:F2}, {13:F2})", + bounds.Center.X, + bounds.Center.Y, + bounds.Center.Z, + bounds.Max.X - bounds.Min.X, + bounds.Max.Y - bounds.Min.Y, + bounds.Max.Z - bounds.Min.Z, + mountText, + anchorText, + referenceEndPoint.X, + referenceEndPoint.Y, + referenceEndPoint.Z, + referenceStartPoint.X, + referenceStartPoint.Y, + referenceStartPoint.Z); + return; + } + + AssemblyTerminalObjectInfo = string.Format( + "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2})", + bounds.Center.X, + bounds.Center.Y, + bounds.Center.Z, + bounds.Max.X - bounds.Min.X, + bounds.Max.Y - bounds.Min.Y, + bounds.Max.Z - bounds.Min.Z, + mountText, + anchorText, + anchorPoint.X, + anchorPoint.Y, + anchorPoint.Z); + } + + private void RefreshAssemblyReferenceRodIfNeeded() + { + if (!AssemblyReferencePathManager.Instance.HasReferenceLine || _assemblyTerminalObject == null) + { + return; + } + + try + { + Point3D endPoint = GetAssemblyTerminalAnchorPoint(); + Vector3D forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform); + double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters); + Point3D startPoint = new Point3D( + endPoint.X - forward.X * rodLength, + endPoint.Y - forward.Y * rodLength, + endPoint.Z - forward.Z * rodLength); + + AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod( + startPoint, + endPoint, + AssemblyReferenceRodDiameterInMeters); + + RefreshAssemblyTerminalObjectInfo(startPoint, endPoint); + RenderAssemblyAnchorMarker(); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 刷新参考杆失败: {ex.Message}", ex); + } + } + + private void RenderAssemblyAnchorMarker() + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + return; + } + + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + Point3D anchorPoint = GetAssemblyTerminalAnchorPoint(); + var markerRoute = new PathRoute("装配对接点") + { + Id = AssemblyAnchorMarkerPathId, + Description = "直线装配终点对接标记" + }; + markerRoute.AddPoint(new PathPoint(anchorPoint, "对接点", PathPointType.EndPoint)); + renderPlugin.RenderPointOnly(markerRoute); + } + + private void ClearAssemblyAnchorMarker() + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.RemovePath(AssemblyAnchorMarkerPathId); + } + + private void CleanupAssemblyReferenceSelection() + { + try + { + PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked; + IsSelectingAssemblyStartPoint = false; + _pathPlanningManager?.EnableMouseHandling(); + _pathPlanningManager?.StopClickTool(); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 清理起点拾取状态失败: {ex.Message}", ex); + } + } + private async Task ExecuteNewPathAsync() { await SafeExecuteAsync(() => @@ -1117,7 +1702,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels { await SafeExecuteAsync(() => { - UpdateMainStatus("正在创建新空轨路径..."); + UpdateMainStatus("正在创建传统空轨路径..."); // 检查是否有空轨基准路径 if (PathPointRenderPlugin.Instance != null) @@ -1126,11 +1711,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels { // 清除所有现有路径的可视化显示 PathPointRenderPlugin.Instance.ClearPathsExcept("grid_visualization_all", "grid_visualization_channel", "grid_visualization_unknown", "grid_visualization_obstacle", "grid_visualization_door"); - LogManager.Info("新建空轨路径:已清除现有路径可视化显示(保留网格可视化)"); + LogManager.Info("新建传统空轨路径:已清除现有路径可视化显示(保留网格可视化)"); } catch (Exception ex) { - LogManager.Error($"新建空轨路径:清除现有路径可视化失败: {ex.Message}", ex); + LogManager.Error($"新建传统空轨路径:清除现有路径可视化失败: {ex.Message}", ex); throw; } } @@ -1140,7 +1725,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (_pathPlanningManager != null) { - // 创建空轨路径 + // 创建传统空轨路径 var newRoute = _pathPlanningManager.StartCreatingNewRoute( isRailPath: true, pathType: PathType.Rail); @@ -1170,22 +1755,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (!ForceReinitializeToolPlugin(subscribeToEvents: true)) { UpdateMainStatus("ToolPlugin初始化失败,请重试"); - LogManager.Error("新建空轨路径:ToolPlugin初始化失败"); + LogManager.Error("新建传统空轨路径:ToolPlugin初始化失败"); return; } // 启动点击工具,设置空轨吸附模式 _pathPlanningManager.StartClickTool(PathPointType.WayPoint, enableRailSnapping: true); - LogManager.Info($"已启动空轨路径点击工具(吸附模式): {newRoute.Name}"); + LogManager.Info($"已启动传统空轨路径点击工具(吸附模式): {newRoute.Name}"); - UpdateMainStatus($"已进入新建空轨路径模式: {newRoute.Name} - 请在3D视图中点击空轨模型设置路径点(将自动吸附到基准路径)"); - LogManager.Info($"开始新建空轨路径: {newRoute.Name},已强制重新初始化ToolPlugin获取鼠标焦点"); + UpdateMainStatus($"已进入传统空轨路径模式: {newRoute.Name} - 请在3D视图中点击空轨模型设置路径点(将自动吸附到基准路径)"); + LogManager.Info($"开始新建传统空轨路径: {newRoute.Name},已强制重新初始化ToolPlugin获取鼠标焦点"); } else { - UpdateMainStatus("创建新空轨路径失败:没有可通行的物流模型"); - LogManager.Error("创建新空轨路径失败:没有可通行的物流模型"); - MessageBox.Show("创建新空轨路径失败:没有找到任何可通行的物流模型。\n请先为模型设置可通行的物流属性,然后再尝试创建路径。", "错误", + UpdateMainStatus("创建传统空轨路径失败:没有可通行的物流模型"); + LogManager.Error("创建传统空轨路径失败:没有可通行的物流模型"); + MessageBox.Show("创建传统空轨路径失败:没有找到任何可通行的物流模型。\n请先为模型设置可通行的物流属性,然后再尝试创建路径。", "错误", MessageBoxButton.OK, MessageBoxImage.Warning); } } @@ -1194,7 +1779,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels UpdateMainStatus("路径规划管理器未初始化"); LogManager.Error("路径规划管理器未初始化"); } - }, "新建空轨路径"); + }, "新建传统空轨路径"); } /// @@ -4468,6 +5053,24 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Warning($"清理自动路径事件订阅时发生异常: {ex.Message}"); } + try + { + CleanupAssemblyReferenceSelection(); + } + catch (Exception ex) + { + LogManager.Warning($"清理直线装配事件订阅时发生异常: {ex.Message}"); + } + + try + { + ClearAssemblyAnchorMarker(); + } + catch (Exception ex) + { + LogManager.Warning($"清理直线装配对接点标记时发生异常: {ex.Message}"); + } + // 确保停止任何活动的点击工具 try { @@ -4536,4 +5139,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels public double Height { get; set; } public double SafetyMargin { get; set; } } -} \ No newline at end of file +} diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 9724bcd..e8f87cb 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -206,10 +206,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 public RailMountMode RailMountMode { get; set; } - /// - /// Rail 路径中构件与安装头的对接方式(仅当 PathType == Rail 时有效) - /// - public RailPayloadAnchorMode RailPayloadAnchorMode { get; set; } - /// /// Rail 路径参考线定义方式(仅当 PathType == Rail 时有效) /// @@ -799,7 +783,6 @@ namespace NavisworksTransport TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值 IsCurved = false; RailMountMode = RailMountMode.UnderRail; - RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter; RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; RailReferenceToAnchorOffset = 0.0; } @@ -823,7 +806,6 @@ namespace NavisworksTransport LastModified = DateTime.Now; Description = string.Empty; RailMountMode = RailMountMode.UnderRail; - RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter; RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; RailReferenceToAnchorOffset = 0.0; } @@ -1060,7 +1042,6 @@ namespace NavisworksTransport MaxObjectHeight = MaxObjectHeight, SafetyMargin = SafetyMargin, RailMountMode = RailMountMode, - RailPayloadAnchorMode = RailPayloadAnchorMode, RailPathDefinitionMode = RailPathDefinitionMode, RailReferenceToAnchorOffset = RailReferenceToAnchorOffset }; diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs index 4152169..f3d505d 100644 --- a/src/Core/VirtualObjectManager.cs +++ b/src/Core/VirtualObjectManager.cs @@ -249,6 +249,144 @@ namespace NavisworksTransport.Core } } + public void MoveVirtualObject(Point3D position, Rotation3D rotation) + { + if (_virtualObjectModelItem == null) return; + + try + { + ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation); + LogManager.Debug("虚拟物体已应用完整三维姿态"); + } + catch (Exception ex) + { + LogManager.Error($"移动虚拟物体并应用完整三维姿态失败: {ex.Message}"); + } + } + + public void MoveVirtualObject(Point3D position, Matrix3 linearTransform) + { + if (_virtualObjectModel == null || _virtualObjectModelItem == null) return; + + try + { + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { _virtualObjectModelItem }; + + // 虚拟物体的尺寸缩放在 Model 层完成。 + // 对盒体而言,如果再把三维姿态放在 Item 层,会让非等比缩放和旋转分层,导致姿态看起来发歪。 + // 因此这里直接在 Model 层一次性应用“缩放 + 旋转 + 平移”。 + doc.Models.ResetPermanentTransform(modelItems); + + var modelTransform = _virtualObjectModel.Transform; + var modelComponents = modelTransform.Factor(); + var currentScale = modelComponents.Scale; + var originalBounds = _virtualObjectModelItem.BoundingBox(); + var originalCenter = originalBounds.Center; + var originalGroundPos = new Point3D(originalBounds.Center.X, originalBounds.Center.Y, originalBounds.Min.Z); + + var effectiveLinear = new Matrix3( + linearTransform.Get(0, 0) * currentScale.X, linearTransform.Get(0, 1) * currentScale.Y, linearTransform.Get(0, 2) * currentScale.Z, + linearTransform.Get(1, 0) * currentScale.X, linearTransform.Get(1, 1) * currentScale.Y, linearTransform.Get(1, 2) * currentScale.Z, + linearTransform.Get(2, 0) * currentScale.X, linearTransform.Get(2, 1) * currentScale.Y, linearTransform.Get(2, 2) * currentScale.Z); + + LogMatrixDiagnostics(linearTransform, effectiveLinear); + + var localGroundPos = originalGroundPos; + var transformedGroundPos = new Point3D( + effectiveLinear.Get(0, 0) * localGroundPos.X + effectiveLinear.Get(0, 1) * localGroundPos.Y + effectiveLinear.Get(0, 2) * localGroundPos.Z, + effectiveLinear.Get(1, 0) * localGroundPos.X + effectiveLinear.Get(1, 1) * localGroundPos.Y + effectiveLinear.Get(1, 2) * localGroundPos.Z, + effectiveLinear.Get(2, 0) * localGroundPos.X + effectiveLinear.Get(2, 1) * localGroundPos.Y + effectiveLinear.Get(2, 2) * localGroundPos.Z); + + var translation = new Vector3D( + position.X - transformedGroundPos.X, + position.Y - transformedGroundPos.Y, + position.Z - transformedGroundPos.Z); + + LogManager.Info( + $"[虚拟物体姿态] 应用前: 中心=({originalCenter.X:F3},{originalCenter.Y:F3},{originalCenter.Z:F3}), " + + $"底面中心=({originalGroundPos.X:F3},{originalGroundPos.Y:F3},{originalGroundPos.Z:F3}), " + + $"尺寸=({originalBounds.Size.X:F3},{originalBounds.Size.Y:F3},{originalBounds.Size.Z:F3}), " + + $"缩放=({currentScale.X:F3},{currentScale.Y:F3},{currentScale.Z:F3})"); + LogManager.Info( + $"[虚拟物体姿态] 目标: 底面中心=({position.X:F3},{position.Y:F3},{position.Z:F3}), " + + $"线性矩阵=[[{linearTransform.Get(0, 0):F4},{linearTransform.Get(0, 1):F4},{linearTransform.Get(0, 2):F4}]," + + $"[{linearTransform.Get(1, 0):F4},{linearTransform.Get(1, 1):F4},{linearTransform.Get(1, 2):F4}]," + + $"[{linearTransform.Get(2, 0):F4},{linearTransform.Get(2, 1):F4},{linearTransform.Get(2, 2):F4}]]"); + LogManager.Info( + $"[虚拟物体姿态] 组合后: 变换底面中心=({transformedGroundPos.X:F3},{transformedGroundPos.Y:F3},{transformedGroundPos.Z:F3}), " + + $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})"); + + var newTransform = new Transform3D(effectiveLinear, translation); + doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false); + + var appliedBounds = _virtualObjectModelItem.BoundingBox(); + LogManager.Info( + $"[虚拟物体姿态] 应用后: 中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Center.Z:F3}), " + + $"底面中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Min.Z:F3}), " + + $"尺寸=({appliedBounds.Size.X:F3},{appliedBounds.Size.Y:F3},{appliedBounds.Size.Z:F3})"); + } + catch (Exception ex) + { + LogManager.Error($"移动虚拟物体并应用完整三维线性姿态失败: {ex.Message}"); + } + } + + private void LogMatrixDiagnostics(Matrix3 sourceLinear, Matrix3 effectiveLinear) + { + if (sourceLinear == null || effectiveLinear == null) + { + return; + } + + var sourceRow0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(0, 1), sourceLinear.Get(0, 2)); + var sourceRow1 = new Vector3D(sourceLinear.Get(1, 0), sourceLinear.Get(1, 1), sourceLinear.Get(1, 2)); + var sourceRow2 = new Vector3D(sourceLinear.Get(2, 0), sourceLinear.Get(2, 1), sourceLinear.Get(2, 2)); + var sourceCol0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(1, 0), sourceLinear.Get(2, 0)); + var sourceCol1 = new Vector3D(sourceLinear.Get(0, 1), sourceLinear.Get(1, 1), sourceLinear.Get(2, 1)); + var sourceCol2 = new Vector3D(sourceLinear.Get(0, 2), sourceLinear.Get(1, 2), sourceLinear.Get(2, 2)); + + LogManager.Info( + $"[矩阵诊断] 源矩阵行: r0=({sourceRow0.X:F4},{sourceRow0.Y:F4},{sourceRow0.Z:F4}), " + + $"r1=({sourceRow1.X:F4},{sourceRow1.Y:F4},{sourceRow1.Z:F4}), " + + $"r2=({sourceRow2.X:F4},{sourceRow2.Y:F4},{sourceRow2.Z:F4})"); + LogManager.Info( + $"[矩阵诊断] 源矩阵列: c0=({sourceCol0.X:F4},{sourceCol0.Y:F4},{sourceCol0.Z:F4}), " + + $"c1=({sourceCol1.X:F4},{sourceCol1.Y:F4},{sourceCol1.Z:F4}), " + + $"c2=({sourceCol2.X:F4},{sourceCol2.Y:F4},{sourceCol2.Z:F4})"); + + LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按当前乘法", effectiveLinear, transpose: false); + LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按转置乘法", effectiveLinear, transpose: true); + } + + private void LogMatrixBasisMapping(string prefix, Matrix3 matrix, bool transpose) + { + var mappedX = TransformDirection(matrix, new Vector3D(1, 0, 0), transpose); + var mappedY = TransformDirection(matrix, new Vector3D(0, 1, 0), transpose); + var mappedZ = TransformDirection(matrix, new Vector3D(0, 0, 1), transpose); + + LogManager.Info( + $"{prefix}: X->({mappedX.X:F4},{mappedX.Y:F4},{mappedX.Z:F4}), " + + $"Y->({mappedY.X:F4},{mappedY.Y:F4},{mappedY.Z:F4}), " + + $"Z->({mappedZ.X:F4},{mappedZ.Y:F4},{mappedZ.Z:F4})"); + } + + private Vector3D TransformDirection(Matrix3 matrix, Vector3D direction, bool transpose) + { + if (!transpose) + { + return new Vector3D( + matrix.Get(0, 0) * direction.X + matrix.Get(0, 1) * direction.Y + matrix.Get(0, 2) * direction.Z, + matrix.Get(1, 0) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(1, 2) * direction.Z, + matrix.Get(2, 0) * direction.X + matrix.Get(2, 1) * direction.Y + matrix.Get(2, 2) * direction.Z); + } + + return new Vector3D( + matrix.Get(0, 0) * direction.X + matrix.Get(1, 0) * direction.Y + matrix.Get(2, 0) * direction.Z, + matrix.Get(0, 1) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(2, 1) * direction.Z, + matrix.Get(0, 2) * direction.X + matrix.Get(1, 2) * direction.Y + matrix.Get(2, 2) * direction.Z); + } + public void ResetToCADPosition() { if (_virtualObjectModelItem == null) return; diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 240c311..877389a 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -119,7 +119,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels private PathStrategyOption _selectedPathStrategy; private ObservableCollection _pathStrategyOptions; private ObservableCollection> _railMountModeOptions; - private ObservableCollection> _railPayloadAnchorModeOptions; private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters(); private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0; @@ -130,7 +129,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels private double _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; private string _assemblyStartPointText = "未选择"; private RailMountMode _assemblyMountMode = RailMountMode.UnderRail; - private RailPayloadAnchorMode _assemblyPayloadAnchorMode = RailPayloadAnchorMode.TopCenter; private bool _hasAssemblyTerminalObject; private bool _isSelectingAssemblyStartPoint; private ModelItem _assemblyTerminalObject; @@ -236,7 +234,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(CanUsePathLines)); OnPropertyChanged(nameof(IsRailRouteSelected)); OnPropertyChanged(nameof(SelectedRailMountMode)); - OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode)); if (!CanUsePathLines && ShowPathLines) { // 吊装和空轨路径不能使用路径线,自动关闭 @@ -301,11 +298,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _railMountModeOptions; } - public ObservableCollection> RailPayloadAnchorModeOptions - { - get => _railPayloadAnchorModeOptions; - } - public RailMountMode SelectedRailMountMode { get => GetSelectedCoreRoute()?.RailMountMode ?? RailMountMode.UnderRail; @@ -323,23 +315,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } - public RailPayloadAnchorMode SelectedRailPayloadAnchorMode - { - get => GetSelectedCoreRoute()?.RailPayloadAnchorMode ?? RailPayloadAnchorMode.TopCenter; - set - { - var coreRoute = GetSelectedCoreRoute(); - if (coreRoute == null || coreRoute.PathType != PathType.Rail || coreRoute.RailPayloadAnchorMode == value) - { - return; - } - - coreRoute.RailPayloadAnchorMode = value; - ApplyRailRouteConfigurationChange(coreRoute, $"对接基准已更新为 {value}"); - OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode)); - } - } - public string AssemblyTerminalObjectName { get => _assemblyTerminalObjectName; @@ -402,19 +377,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels set => SetProperty(ref _assemblyStartPointText, value); } - public RailPayloadAnchorMode AssemblyPayloadAnchorMode - { - get => _assemblyPayloadAnchorMode; - set - { - if (SetProperty(ref _assemblyPayloadAnchorMode, value) && HasAssemblyTerminalObject) - { - RefreshAssemblyTerminalObjectInfo(); - RefreshAssemblyReferenceRodIfNeeded(); - } - } - } - public RailMountMode AssemblyMountMode { get => _assemblyMountMode; @@ -423,6 +385,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (SetProperty(ref _assemblyMountMode, value) && HasAssemblyTerminalObject) { RefreshAssemblyTerminalObjectInfo(); + RefreshAssemblyReferenceRodIfNeeded(); } } } @@ -1114,21 +1077,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels } }; - _railPayloadAnchorModeOptions = new ObservableCollection> - { - new RailConfigOption - { - Value = RailPayloadAnchorMode.TopCenter, - DisplayName = "顶面对接", - Description = "以箱体顶面中心与安装头刚性对接" - }, - new RailConfigOption - { - Value = RailPayloadAnchorMode.BottomCenter, - DisplayName = "底面对接", - Description = "以箱体底面中心与安装头刚性对接" - } - }; } private PathRoute GetSelectedCoreRoute() @@ -1466,7 +1414,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels Description = $"直线装配路径 - {AssemblyTerminalObjectName}", PathType = PathType.Rail, RailMountMode = AssemblyMountMode, - RailPayloadAnchorMode = AssemblyPayloadAnchorMode, RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine }; @@ -1501,7 +1448,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels endPoint.Z, AssemblyStartPointText, AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装", - AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接"); + GetAssemblyAnchorText()); OnPropertyChanged(nameof(PathRoutes)); LogManager.Info($"[直线装配] 已生成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})"); @@ -1517,7 +1464,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels var bounds = _assemblyTerminalObject.BoundingBox(); var center = bounds.Center; Vector3D up = ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform); - double direction = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? 1.0 : -1.0; + double direction = GetAssemblyAnchorDirection(); double verticalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters); return new Point3D( @@ -1568,7 +1515,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels var bounds = _assemblyTerminalObject.BoundingBox(); Point3D anchorPoint = GetAssemblyTerminalAnchorPoint(); - string anchorText = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接"; + string anchorText = GetAssemblyAnchorText(); string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装"; if (referenceStartPoint != null && referenceEndPoint != null) @@ -1651,6 +1598,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); } + private double GetAssemblyAnchorDirection() + { + return PathRoute.IsTopPayloadAnchorForMountMode(AssemblyMountMode) + ? 1.0 + : -1.0; + } + + private string GetAssemblyAnchorText() + { + return PathRoute.IsTopPayloadAnchorForMountMode(AssemblyMountMode) + ? "顶面对接" + : "底面对接"; + } + private sealed class AssemblyReferenceLine { public AssemblyReferenceLine(Point3D startPoint, Point3D endPoint, Vector3D direction) diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index e493651..5fdc00d 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -328,29 +328,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 DisplayMemberPath="DisplayName" ToolTip="选择直线装配路径使用轨上安装还是轨下安装"/> - - - - - - - @@ -485,23 +462,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 DisplayMemberPath="DisplayName" ToolTip="选择安装头位于双轨上方还是下方"/> - - - - - - diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs index f878236..c66ad9d 100644 --- a/src/Utils/ModelItemTransformHelper.cs +++ b/src/Utils/ModelItemTransformHelper.cs @@ -361,6 +361,23 @@ namespace NavisworksTransport.Utils ApplyAbsoluteTransform(item, targetPosition, targetRotation, preserveCurrentScale: true); } + /// + /// 将物体移动到指定位置和完整三维线性姿态(先回到CAD原始位置,再移动)。 + /// targetLinear 的列向量分别表示物体局部 X/Y/Z 轴在世界坐标中的方向。 + /// + public static void MoveItemToPositionAndLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear) + { + ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: false); + } + + /// + /// 将物体移动到指定位置和完整三维线性姿态,同时保留当前缩放。 + /// + public static void MoveItemToPositionAndLinearTransformWithCurrentScale(ModelItem item, Point3D targetPosition, Matrix3 targetLinear) + { + ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: true); + } + /// /// 将物体移动到指定中心点和完整三维朝向,同时保留当前缩放。 /// 适用于参考杆、辅助几何等以几何中心作为定位基准的场景。 @@ -577,6 +594,67 @@ namespace NavisworksTransport.Utils doc.Models.OverridePermanentTransform(modelItems, transform, false); } + private static void ApplyAbsoluteLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear, bool preserveCurrentScale) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (targetLinear == null) + { + throw new ArgumentNullException(nameof(targetLinear)); + } + + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { item }; + Vector3D currentScale = new Vector3D(1, 1, 1); + + if (preserveCurrentScale) + { + var currentComponents = item.Transform.Factor(); + currentScale = currentComponents.Scale; + } + + doc.Models.ResetPermanentTransform(modelItems); + + var originalBounds = item.BoundingBox(); + var originalGroundPos = new Point3D( + originalBounds.Center.X, + originalBounds.Center.Y, + originalBounds.Min.Z + ); + + Matrix3 effectiveLinear = preserveCurrentScale + ? BuildScaledLinearMatrix(targetLinear, currentScale) + : targetLinear; + + Point3D transformedGroundPos = TransformPoint(effectiveLinear, originalGroundPos); + var translation = new Vector3D( + targetPosition.X - transformedGroundPos.X, + targetPosition.Y - transformedGroundPos.Y, + targetPosition.Z - transformedGroundPos.Z + ); + + doc.Models.OverridePermanentTransform(modelItems, new Transform3D(effectiveLinear, translation), false); + } + + private static Matrix3 BuildScaledLinearMatrix(Matrix3 linear, Vector3D scale) + { + return new Matrix3( + linear.Get(0, 0) * scale.X, linear.Get(0, 1) * scale.Y, linear.Get(0, 2) * scale.Z, + linear.Get(1, 0) * scale.X, linear.Get(1, 1) * scale.Y, linear.Get(1, 2) * scale.Z, + linear.Get(2, 0) * scale.X, linear.Get(2, 1) * scale.Y, linear.Get(2, 2) * scale.Z); + } + + private static Point3D TransformPoint(Matrix3 linear, Point3D point) + { + return new Point3D( + linear.Get(0, 0) * point.X + linear.Get(0, 1) * point.Y + linear.Get(0, 2) * point.Z, + linear.Get(1, 0) * point.X + linear.Get(1, 1) * point.Y + linear.Get(1, 2) * point.Z, + linear.Get(2, 0) * point.X + linear.Get(2, 1) * point.Y + linear.Get(2, 2) * point.Z); + } + /// /// 将物体移动到指定位置和朝向,同时保持缩放比例 /// 专为虚拟物体设计,避免缩放被覆盖 diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs index 24e5c3d..9bd81be 100644 --- a/src/Utils/RailPathPoseHelper.cs +++ b/src/Utils/RailPathPoseHelper.cs @@ -11,6 +11,7 @@ namespace NavisworksTransport.Utils public static class RailPathPoseHelper { private const double TangentEpsilon = 1e-9; + private static bool _rotationConstructorConventionLogged; /// /// 获取 Rail 参考点到构件对接点的有符号偏移(模型单位)。 @@ -45,7 +46,7 @@ namespace NavisworksTransport.Utils return 0.0; } - if (route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter) + if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)) { return anchorOffset - objectHeight; } @@ -55,7 +56,8 @@ namespace NavisworksTransport.Utils /// /// 计算通行空间中心相对于 Rail 参考点的 Z 偏移(模型单位)。 - /// 对于当前渲染实现,top-center 对接对应正半高,bottom-center 对接对应负半高。 + /// 顶面对接时,通行空间中心位于对接点下方半高; + /// 底面对接时,通行空间中心位于对接点上方半高。 /// public static double GetObjectSpaceCenterZOffset(PathRoute route, double objectSpaceHeight) { @@ -66,12 +68,12 @@ namespace NavisworksTransport.Utils return 0.0; } - if (route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter) + if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)) { - return anchorOffset + objectSpaceHeight / 2.0; + return anchorOffset - objectSpaceHeight / 2.0; } - return anchorOffset - objectSpaceHeight / 2.0; + return anchorOffset + objectSpaceHeight / 2.0; } /// @@ -149,16 +151,17 @@ namespace NavisworksTransport.Utils } /// - /// 根据轨道路径局部坐标系创建 Rail 构件姿态。 - /// 约定:局部 X 轴沿轨道切向,局部 Z 轴沿轨道法向,局部 Y 轴为横向。 + /// 根据轨道路径切向和法向创建 Rail 构件完整三维姿态。 + /// 约定:构件本地 X 轴为前进方向,本地 Z 轴为上方向。 + /// 返回值是可直接用于 Transform3D 的线性矩阵。 /// - public static bool TryCreateRailRotation( + public static bool TryCreateRailLinearTransform( Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, - out Rotation3D rotation) + out Matrix3 linearTransform) { - rotation = Rotation3D.Identity; + linearTransform = null; var tangent = ResolveTangent(previousPoint, currentPoint, nextPoint); double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z; @@ -167,29 +170,179 @@ namespace NavisworksTransport.Utils return false; } - var normal = ResolveRailNormal(previousPoint, currentPoint, nextPoint); - var lateral = Cross(normal, tangent); + tangent = Normalize(tangent); + var normal = Normalize(ResolveRailNormal(previousPoint, currentPoint, nextPoint)); + var lateral = Normalize(Cross(normal, tangent)); + double lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z; if (lateralLengthSquared < TangentEpsilon) + { + Vector3D fallbackUp = Math.Abs(tangent.Z) < 0.9 + ? new Vector3D(0, 0, 1) + : new Vector3D(0, 1, 0); + lateral = Normalize(Cross(fallbackUp, tangent)); + lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z; + if (lateralLengthSquared < TangentEpsilon) + { + return false; + } + } + + normal = Normalize(Cross(tangent, lateral)); + + // Navisworks 在 Transform3D(Matrix3, ...) 中按列读取局部基向量: + // 第 1 列 = 本地 X 轴在世界中的方向 + // 第 2 列 = 本地 Y 轴在世界中的方向 + // 第 3 列 = 本地 Z 轴在世界中的方向 + linearTransform = new Matrix3( + tangent.X, lateral.X, normal.X, + tangent.Y, lateral.Y, normal.Y, + tangent.Z, lateral.Z, normal.Z); + + LogManager.Info( + $"[Rail姿态] 切向=({tangent.X:F4},{tangent.Y:F4},{tangent.Z:F4}), " + + $"侧向=({lateral.X:F4},{lateral.Y:F4},{lateral.Z:F4}), " + + $"法向=({normal.X:F4},{normal.Y:F4},{normal.Z:F4})"); + return true; + } + + /// + /// 根据轨道路径切向和法向创建 Rail 构件完整三维旋转。 + /// 约定:构件本地 X 轴为前进方向,本地 Z 轴为上方向。 + /// + public static bool TryCreateRailRotation( + Point3D previousPoint, + Point3D currentPoint, + Point3D nextPoint, + out Rotation3D rotation) + { + rotation = Rotation3D.Identity; + LogRotationConstructorConventionOnce(); + + if (!TryCreateRailLinearTransform(previousPoint, currentPoint, nextPoint, out var linearTransform)) { return false; } - tangent = Normalize(tangent); - normal = Normalize(normal); - lateral = Normalize(lateral); + rotation = CreateRotationFromLinearTransform(linearTransform); + var verifiedLinear = new Transform3D(rotation).Linear; - rotation = new Rotation3D( - new UnitVector3D(tangent.X, tangent.Y, tangent.Z), - new UnitVector3D(lateral.X, lateral.Y, lateral.Z), - new UnitVector3D(normal.X, normal.Y, normal.Z)); + LogManager.Info( + $"[Rail旋转验证] X=({verifiedLinear.Get(0, 0):F4},{verifiedLinear.Get(1, 0):F4},{verifiedLinear.Get(2, 0):F4}), " + + $"Y=({verifiedLinear.Get(0, 1):F4},{verifiedLinear.Get(1, 1):F4},{verifiedLinear.Get(2, 1):F4}), " + + $"Z=({verifiedLinear.Get(0, 2):F4},{verifiedLinear.Get(1, 2):F4},{verifiedLinear.Get(2, 2):F4})"); return true; } + private static Rotation3D CreateRotationFromLinearTransform(Matrix3 linearTransform) + { + double m00 = linearTransform.Get(0, 0); + double m01 = linearTransform.Get(0, 1); + double m02 = linearTransform.Get(0, 2); + double m10 = linearTransform.Get(1, 0); + double m11 = linearTransform.Get(1, 1); + double m12 = linearTransform.Get(1, 2); + double m20 = linearTransform.Get(2, 0); + double m21 = linearTransform.Get(2, 1); + double m22 = linearTransform.Get(2, 2); + + double qx; + double qy; + double qz; + double qw; + + double trace = m00 + m11 + m22; + if (trace > 0.0) + { + double s = Math.Sqrt(trace + 1.0) * 2.0; + qw = 0.25 * s; + qx = (m21 - m12) / s; + qy = (m02 - m20) / s; + qz = (m10 - m01) / s; + } + else if (m00 > m11 && m00 > m22) + { + double s = Math.Sqrt(1.0 + m00 - m11 - m22) * 2.0; + qw = (m21 - m12) / s; + qx = 0.25 * s; + qy = (m01 + m10) / s; + qz = (m02 + m20) / s; + } + else if (m11 > m22) + { + double s = Math.Sqrt(1.0 + m11 - m00 - m22) * 2.0; + qw = (m02 - m20) / s; + qx = (m01 + m10) / s; + qy = 0.25 * s; + qz = (m12 + m21) / s; + } + else + { + double s = Math.Sqrt(1.0 + m22 - m00 - m11) * 2.0; + qw = (m10 - m01) / s; + qx = (m02 + m20) / s; + qy = (m12 + m21) / s; + qz = 0.25 * s; + } + + var rotation = new Rotation3D(qx, qy, qz, qw); + double error = CalculateLinearError(new Transform3D(rotation).Linear, linearTransform); + LogManager.Info( + $"[Rail旋转验证] quaternion候选=({qx:F4},{qy:F4},{qz:F4},{qw:F4}), 误差={error:F6}"); + return rotation; + } + + private static void LogRotationConstructorConventionOnce() + { + if (_rotationConstructorConventionLogged) + { + return; + } + + _rotationConstructorConventionLogged = true; + + LogKnownAxisRotation("X90", new UnitVector3D(1, 0, 0), Math.PI / 2.0); + LogKnownAxisRotation("Y90", new UnitVector3D(0, 1, 0), Math.PI / 2.0); + LogKnownAxisRotation("Z90", new UnitVector3D(0, 0, 1), Math.PI / 2.0); + } + + private static void LogKnownAxisRotation(string name, UnitVector3D axis, double angle) + { + Rotation3D axisAngleRotation = new Rotation3D(axis, angle); + Rotation3D reconstructed = new Rotation3D( + axisAngleRotation.A, + axisAngleRotation.B, + axisAngleRotation.C, + axisAngleRotation.D); + Matrix3 linear = new Transform3D(reconstructed).Linear; + + LogManager.Info( + $"[Rotation构造测试] {name}: " + + $"A={axisAngleRotation.A:F6}, B={axisAngleRotation.B:F6}, C={axisAngleRotation.C:F6}, D={axisAngleRotation.D:F6}, " + + $"LinearX=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " + + $"LinearY=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " + + $"LinearZ=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})"); + } + + private static double CalculateLinearError(Matrix3 actual, Matrix3 expected) + { + double error = 0.0; + for (int row = 0; row < 3; row++) + { + for (int column = 0; column < 3; column++) + { + double delta = actual.Get(row, column) - expected.Get(row, column); + error += delta * delta; + } + } + + return Math.Sqrt(error); + } + private static double GetBottomOffsetMagnitude(PathRoute route, double objectHeight) { double anchorOffset = GetAnchorOffset(route); - return route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter + return PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode) ? anchorOffset - objectHeight : anchorOffset; } @@ -197,9 +350,9 @@ namespace NavisworksTransport.Utils private static double GetObjectSpaceCenterOffsetMagnitude(PathRoute route, double objectSpaceHeight) { double anchorOffset = GetAnchorOffset(route); - return route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter - ? anchorOffset + objectSpaceHeight / 2.0 - : anchorOffset - objectSpaceHeight / 2.0; + return PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode) + ? anchorOffset - objectSpaceHeight / 2.0 + : anchorOffset + objectSpaceHeight / 2.0; } private static Vector3D ResolveRailNormal(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint) From 5e8c50e04347766b106252301ffd409ee30c7cb6 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Fri, 20 Mar 2026 12:50:08 +0800 Subject: [PATCH 05/87] Stabilize rail 3D animation transforms --- deploy-plugin.bat | 60 ++++- doc/design/2026/NavisworksAPI使用方法.md | 45 ++++ src/Core/Animation/PathAnimationManager.cs | 132 ++++------ src/Core/VirtualObjectManager.cs | 123 --------- src/Utils/CollisionSceneHelper.cs | 24 +- src/Utils/ModelItemTransformHelper.cs | 292 ++++++++++++++++----- 6 files changed, 393 insertions(+), 283 deletions(-) diff --git a/deploy-plugin.bat b/deploy-plugin.bat index 96fa442..0d5d775 100644 --- a/deploy-plugin.bat +++ b/deploy-plugin.bat @@ -1,26 +1,70 @@ @echo off setlocal +set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin" +set "BUILD_DIR=bin\x64\Release" +set "MAX_WAIT_SECONDS=15" +set "COPY_RETRY_COUNT=5" + :: Stop Navisworks to release locked plugin DLLs. taskkill /F /IM Roamer.exe 2>nul if %errorlevel% == 0 ( echo Navisworks process terminated. - timeout /t 3 /nobreak >nul ) -set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin" -set "BUILD_DIR=bin\x64\Release" +:: Wait until Roamer.exe fully exits. Fixed delay alone is not reliable. +set /a WAIT_COUNT=0 +:WAIT_FOR_ROAMER_EXIT +tasklist /FI "IMAGENAME eq Roamer.exe" | find /I "Roamer.exe" >nul +if errorlevel 1 goto ROAMER_EXITED +if %WAIT_COUNT% GEQ %MAX_WAIT_SECONDS% ( + echo Warning: Roamer.exe still appears to be running after %MAX_WAIT_SECONDS% seconds. + goto ROAMER_EXITED +) +timeout /t 1 /nobreak >nul +set /a WAIT_COUNT+=1 +goto WAIT_FOR_ROAMER_EXIT + +:ROAMER_EXITED if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%" +set /a COPY_ATTEMPT=0 +:COPY_DLLS copy "%BUILD_DIR%\*.dll" "%TARGET_DIR%\" >nul +if not errorlevel 1 goto DLLS_DEPLOYED +set /a COPY_ATTEMPT+=1 +if %COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% ( + echo Failed to deploy plugin DLLs after %COPY_RETRY_COUNT% attempts. + exit /b 1 +) +timeout /t 1 /nobreak >nul +goto COPY_DLLS + +:DLLS_DEPLOYED echo Plugin DLLs deployed. :: Copy bundled resources. -if exist "%BUILD_DIR%\resources" ( - if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources" - xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul - echo Resources folder deployed. -) +if exist "%BUILD_DIR%\resources" goto PREPARE_RESOURCES +goto DEPLOY_DONE +:PREPARE_RESOURCES +if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources" +set /a RESOURCE_COPY_ATTEMPT=0 + +:COPY_RESOURCES +xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul +if not errorlevel 1 goto RESOURCES_DEPLOYED +set /a RESOURCE_COPY_ATTEMPT+=1 +if %RESOURCE_COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% ( + echo Failed to deploy resources after %COPY_RETRY_COUNT% attempts. + exit /b 1 +) +timeout /t 1 /nobreak >nul +goto COPY_RESOURCES + +:RESOURCES_DEPLOYED +echo Resources folder deployed. + +:DEPLOY_DONE echo Plugin deployed successfully! diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md index 930dc03..39f03e4 100644 --- a/doc/design/2026/NavisworksAPI使用方法.md +++ b/doc/design/2026/NavisworksAPI使用方法.md @@ -880,6 +880,51 @@ doc.Models.OverridePermanentTransform(modelItems, transform, false); - 资源本地长度轴约定(例如是否沿本地 `+X`) - 当前实际显示位置,优先看 `BoundingBox()`,不要只看 `ModelItem.Transform` +#### 11.7.2.5 真实模型做三维旋转时,推荐使用“先移到原点,再旋转,再移到目标点”(2026-03-20 新增,重要) + +在 Rail 路径真实模型调试中,一个非常关键的经验是: + +- **不要**只在公式层面“猜一个补偿平移” +- 对真实模型做完整三维姿态时,更稳的做法是显式拆成三步: + +```csharp +// 1. 把当前参考点移到原点 +doc.Models.OverridePermanentTransform( + modelItems, + Transform3D.CreateTranslation(new Vector3D(-currentPoint.X, -currentPoint.Y, -currentPoint.Z)), + false); + +// 2. 在原点执行三维旋转 +var rotationComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor(); +rotationComponents.Rotation = deltaRotation; +doc.Models.OverridePermanentTransform(modelItems, rotationComponents.Combine(), false); + +// 3. 再把参考点移到目标位置 +doc.Models.OverridePermanentTransform( + modelItems, + Transform3D.CreateTranslation(new Vector3D(targetPoint.X, targetPoint.Y, targetPoint.Z)), + false); +``` + +**适用场景:** + +- 真实模型不是“原点资源”,本身已经位于世界坐标某个远离原点的位置 +- 需要做完整三维旋转(不只是 yaw) +- 目标是让物体绕某个固定参考点“自转”,而不是带着原始世界位置绕原点“公转” + +**核心理解:** + +- Navisworks 的旋转仍然是绕世界原点 +- 所以如果想绕“某个固定参考点”旋转,就先把这个参考点移到原点 +- 这个固定参考点不一定必须是中心,也可以是底面中心、安装点或其他局部固定点 +- 但在复杂三维姿态下,**几何中心通常是最稳定、最容易验证的参考点** + +**这条原则解决的问题:** + +- 虚拟物体因为本来就接近“原点资源”,旋转通常更容易成功 +- 真实模型自带原始世界位置时,如果直接旋转,很容易出现“方向对了,但位置跑很远” +- 将参考点显式移到原点后再旋转,可以显著降低三维平移补偿出错的概率 + #### 11.7.3 正确实现"绕物体中心旋转" **解决方案:手动计算旋转导致的位置偏移并补偿** diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 8b0e72c..e7b4c7b 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -74,8 +74,6 @@ namespace NavisworksTransport.Core.Animation public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度) public Rotation3D Rotation { get; set; } // 可选的完整三维姿态 public bool HasCustomRotation { get; set; } // 是否使用完整三维姿态 - public Matrix3 LinearTransform { get; set; } // 可选的完整三维线性姿态 - public bool HasCustomLinearTransform { get; set; }// 是否使用完整三维线性姿态 public List Collisions { get; set; } // 该帧的碰撞结果 public bool HasCollision => Collisions?.Count > 0; @@ -85,8 +83,6 @@ namespace NavisworksTransport.Core.Animation YawRadians = 0.0; Rotation = Rotation3D.Identity; HasCustomRotation = false; - LinearTransform = null; - HasCustomLinearTransform = false; } } @@ -425,15 +421,11 @@ namespace NavisworksTransport.Core.Animation // 2. 重置内部跟踪变量(同步到原始状态) // 注意:ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值 _currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform); - _currentRotation = Rotation3D.Identity; - _hasCurrentRotation = false; + _currentRotation = objectToRestore.Transform.Factor().Rotation; + _hasCurrentRotation = true; var originalBoundingBox = objectToRestore.BoundingBox(); - _currentPosition = new Point3D( - originalBoundingBox.Center.X, - originalBoundingBox.Center.Y, - originalBoundingBox.Min.Z - ); + _currentPosition = GetTrackedObjectPosition(objectToRestore, _route?.PathType == PathType.Rail && !isVirtual); string objectName = isVirtual ? "虚拟物体" : objectToRestore.DisplayName; LogManager.Info($"[归位] {objectName} 已彻底恢复到原始位置, yaw={_currentYaw:F3}"); @@ -596,10 +588,12 @@ namespace NavisworksTransport.Core.Animation _animatedObject = animatedObject; _originalTransform = animatedObject.Transform; _originalCenter = animatedObject.BoundingBox().Center; - _currentPosition = new Point3D(_originalCenter.X, _originalCenter.Y, animatedObject.BoundingBox().Min.Z); + _currentPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject); // 统一逻辑:从当前 Transform 中提取实际朝向(无论虚拟物体还是普通物体) _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform); + _currentRotation = _originalTransform.Factor().Rotation; + _hasCurrentRotation = true; } if (pathPoints != null) @@ -668,23 +662,7 @@ namespace NavisworksTransport.Core.Animation $"Y=({railLinearTransform.Get(0, 1):F4},{railLinearTransform.Get(1, 1):F4},{railLinearTransform.Get(2, 1):F4}), " + $"Z=({railLinearTransform.Get(0, 2):F4},{railLinearTransform.Get(1, 2):F4},{railLinearTransform.Get(2, 2):F4})"); - if (_isVirtualObject) - { - VirtualObjectManager.Instance.MoveVirtualObject(startPosition, railRotation); - } - else if (_animatedObject != null) - { - ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, startPosition, railRotation); - } - else - { - return false; - } - - _currentPosition = startPosition; - _currentRotation = railRotation; - _hasCurrentRotation = true; - _currentYaw = Math.Atan2(railLinearTransform.Get(1, 0), railLinearTransform.Get(0, 0)); + UpdateObjectPosition(startPosition, railRotation); LogManager.Info("[移动到起点] Rail路径已应用完整三维旋转姿态"); return true; } @@ -2184,7 +2162,30 @@ namespace NavisworksTransport.Core.Animation } else if (_animatedObject != null) { - ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, newPosition, newRotation); + var currentRotation = _hasCurrentRotation + ? _currentRotation + : _animatedObject.Transform.Factor().Rotation; + var currentLinear = new Transform3D(currentRotation).Linear; + var targetLinear = new Transform3D(newRotation).Linear; + LogManager.Info( + $"[动画姿态入口] {_animatedObject.DisplayName} 当前点=({_currentPosition.X:F3},{_currentPosition.Y:F3},{_currentPosition.Z:F3}), " + + $"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})"); + LogManager.Info( + $"[动画姿态入口] {_animatedObject.DisplayName} 当前姿态: " + + $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " + + $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " + + $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})"); + LogManager.Info( + $"[动画姿态入口] {_animatedObject.DisplayName} 目标姿态: " + + $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + + $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + + $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation( + _animatedObject, + _currentPosition, + currentRotation, + newPosition, + newRotation); } else { @@ -2202,34 +2203,6 @@ namespace NavisworksTransport.Core.Animation } } - private void UpdateObjectPosition(Point3D newPosition, Matrix3 newLinearTransform) - { - try - { - if (_isVirtualObject) - { - VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newLinearTransform); - } - else if (_animatedObject != null) - { - ModelItemTransformHelper.MoveItemToPositionAndLinearTransformWithCurrentScale(_animatedObject, newPosition, newLinearTransform); - } - else - { - return; - } - - _currentPosition = newPosition; - _currentRotation = Rotation3D.Identity; - _hasCurrentRotation = false; - _currentYaw = Math.Atan2(newLinearTransform.Get(1, 0), newLinearTransform.Get(0, 0)); - } - catch (Exception ex) - { - LogManager.Error($"使用完整三维线性姿态更新对象位置失败: {ex.Message}"); - } - } - /// /// 获取文档单位信息(用于日志记录) /// @@ -2319,12 +2292,7 @@ namespace NavisworksTransport.Core.Animation return (null, 0); } - var bbox = obj.BoundingBox(); - var position = new Point3D( - bbox.Center.X, - bbox.Center.Y, - bbox.Min.Z - ); + var position = _currentPosition; // 🔥 关键:使用 _currentYaw(实际当前朝向),而不是 Transform 的 CAD 原始朝向 // Transform 返回的是 CAD 设计时的原始朝向,不是动画后的实际朝向 return (position, _currentYaw); @@ -2380,8 +2348,7 @@ namespace NavisworksTransport.Core.Animation var originalAnimatedObject = _animatedObject; _animatedObject = obj; - var bbox = obj.BoundingBox(); - _currentPosition = new Point3D(bbox.Center.X, bbox.Center.Y, bbox.Min.Z); + _currentPosition = _savedObjectPosition; _currentYaw = _savedObjectYaw; _currentRotation = _savedObjectRotation; _hasCurrentRotation = _savedObjectHasCustomRotation; @@ -2470,11 +2437,7 @@ namespace NavisworksTransport.Core.Animation // 更新对象位置和朝向 var frameData = _animationFrames[_currentFrameIndex]; - if (frameData.HasCustomLinearTransform && _route?.PathType == PathType.Rail) - { - UpdateObjectPosition(frameData.Position, frameData.LinearTransform); - } - else if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail) + if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail) { UpdateObjectPosition(frameData.Position, frameData.Rotation); } @@ -2858,6 +2821,8 @@ namespace NavisworksTransport.Core.Animation /// 用于保存物体当前状态(因为Transform返回的是CAD原始值) /// public double CurrentYaw => _currentYaw; + public Rotation3D CurrentRotation => _currentRotation; + public bool HasCurrentRotation => _hasCurrentRotation; /// /// 获取当前动画物体高度(模型单位) @@ -2962,7 +2927,7 @@ namespace NavisworksTransport.Core.Animation { _originalTransform = animatedObject.Transform; _originalCenter = animatedObject.BoundingBox().Center; - _currentPosition = new Point3D(_originalCenter.X, _originalCenter.Y, animatedObject.BoundingBox().Min.Z); + _currentPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject); // 保持当前的 _currentYaw(因为物体可能已经被 MoveObjectToPathStart 旋转) // 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值 @@ -2979,6 +2944,22 @@ namespace NavisworksTransport.Core.Animation LogManager.Info($"[CreateAnimation] 动画已创建,状态设置为Ready"); } + private Point3D GetTrackedObjectPosition(ModelItem item, bool useStableRailAnchor) + { + if (item == null) + { + return new Point3D(0, 0, 0); + } + + var bounds = item.BoundingBox(); + if (useStableRailAnchor) + { + return ModelItemTransformHelper.GetStableBottomAnchorPoint(bounds, item.Transform); + } + + return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z); + } + /// /// 设置物体角度修正值(度,顺时针) /// @@ -3015,7 +2996,6 @@ namespace NavisworksTransport.Core.Animation #region 动画实现方法 - /// /// DispatcherTimer动画处理核心 /// @@ -3141,11 +3121,7 @@ namespace NavisworksTransport.Core.Animation actualYaw += correctionRad; } - if (frameData.HasCustomLinearTransform && _route?.PathType == PathType.Rail) - { - UpdateObjectPosition(frameData.Position, frameData.LinearTransform); - } - else if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail) + if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail) { UpdateObjectPosition(frameData.Position, frameData.Rotation); } diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs index f3d505d..b1ea2dd 100644 --- a/src/Core/VirtualObjectManager.cs +++ b/src/Core/VirtualObjectManager.cs @@ -264,129 +264,6 @@ namespace NavisworksTransport.Core } } - public void MoveVirtualObject(Point3D position, Matrix3 linearTransform) - { - if (_virtualObjectModel == null || _virtualObjectModelItem == null) return; - - try - { - var doc = Application.ActiveDocument; - var modelItems = new ModelItemCollection { _virtualObjectModelItem }; - - // 虚拟物体的尺寸缩放在 Model 层完成。 - // 对盒体而言,如果再把三维姿态放在 Item 层,会让非等比缩放和旋转分层,导致姿态看起来发歪。 - // 因此这里直接在 Model 层一次性应用“缩放 + 旋转 + 平移”。 - doc.Models.ResetPermanentTransform(modelItems); - - var modelTransform = _virtualObjectModel.Transform; - var modelComponents = modelTransform.Factor(); - var currentScale = modelComponents.Scale; - var originalBounds = _virtualObjectModelItem.BoundingBox(); - var originalCenter = originalBounds.Center; - var originalGroundPos = new Point3D(originalBounds.Center.X, originalBounds.Center.Y, originalBounds.Min.Z); - - var effectiveLinear = new Matrix3( - linearTransform.Get(0, 0) * currentScale.X, linearTransform.Get(0, 1) * currentScale.Y, linearTransform.Get(0, 2) * currentScale.Z, - linearTransform.Get(1, 0) * currentScale.X, linearTransform.Get(1, 1) * currentScale.Y, linearTransform.Get(1, 2) * currentScale.Z, - linearTransform.Get(2, 0) * currentScale.X, linearTransform.Get(2, 1) * currentScale.Y, linearTransform.Get(2, 2) * currentScale.Z); - - LogMatrixDiagnostics(linearTransform, effectiveLinear); - - var localGroundPos = originalGroundPos; - var transformedGroundPos = new Point3D( - effectiveLinear.Get(0, 0) * localGroundPos.X + effectiveLinear.Get(0, 1) * localGroundPos.Y + effectiveLinear.Get(0, 2) * localGroundPos.Z, - effectiveLinear.Get(1, 0) * localGroundPos.X + effectiveLinear.Get(1, 1) * localGroundPos.Y + effectiveLinear.Get(1, 2) * localGroundPos.Z, - effectiveLinear.Get(2, 0) * localGroundPos.X + effectiveLinear.Get(2, 1) * localGroundPos.Y + effectiveLinear.Get(2, 2) * localGroundPos.Z); - - var translation = new Vector3D( - position.X - transformedGroundPos.X, - position.Y - transformedGroundPos.Y, - position.Z - transformedGroundPos.Z); - - LogManager.Info( - $"[虚拟物体姿态] 应用前: 中心=({originalCenter.X:F3},{originalCenter.Y:F3},{originalCenter.Z:F3}), " + - $"底面中心=({originalGroundPos.X:F3},{originalGroundPos.Y:F3},{originalGroundPos.Z:F3}), " + - $"尺寸=({originalBounds.Size.X:F3},{originalBounds.Size.Y:F3},{originalBounds.Size.Z:F3}), " + - $"缩放=({currentScale.X:F3},{currentScale.Y:F3},{currentScale.Z:F3})"); - LogManager.Info( - $"[虚拟物体姿态] 目标: 底面中心=({position.X:F3},{position.Y:F3},{position.Z:F3}), " + - $"线性矩阵=[[{linearTransform.Get(0, 0):F4},{linearTransform.Get(0, 1):F4},{linearTransform.Get(0, 2):F4}]," + - $"[{linearTransform.Get(1, 0):F4},{linearTransform.Get(1, 1):F4},{linearTransform.Get(1, 2):F4}]," + - $"[{linearTransform.Get(2, 0):F4},{linearTransform.Get(2, 1):F4},{linearTransform.Get(2, 2):F4}]]"); - LogManager.Info( - $"[虚拟物体姿态] 组合后: 变换底面中心=({transformedGroundPos.X:F3},{transformedGroundPos.Y:F3},{transformedGroundPos.Z:F3}), " + - $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})"); - - var newTransform = new Transform3D(effectiveLinear, translation); - doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false); - - var appliedBounds = _virtualObjectModelItem.BoundingBox(); - LogManager.Info( - $"[虚拟物体姿态] 应用后: 中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Center.Z:F3}), " + - $"底面中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Min.Z:F3}), " + - $"尺寸=({appliedBounds.Size.X:F3},{appliedBounds.Size.Y:F3},{appliedBounds.Size.Z:F3})"); - } - catch (Exception ex) - { - LogManager.Error($"移动虚拟物体并应用完整三维线性姿态失败: {ex.Message}"); - } - } - - private void LogMatrixDiagnostics(Matrix3 sourceLinear, Matrix3 effectiveLinear) - { - if (sourceLinear == null || effectiveLinear == null) - { - return; - } - - var sourceRow0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(0, 1), sourceLinear.Get(0, 2)); - var sourceRow1 = new Vector3D(sourceLinear.Get(1, 0), sourceLinear.Get(1, 1), sourceLinear.Get(1, 2)); - var sourceRow2 = new Vector3D(sourceLinear.Get(2, 0), sourceLinear.Get(2, 1), sourceLinear.Get(2, 2)); - var sourceCol0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(1, 0), sourceLinear.Get(2, 0)); - var sourceCol1 = new Vector3D(sourceLinear.Get(0, 1), sourceLinear.Get(1, 1), sourceLinear.Get(2, 1)); - var sourceCol2 = new Vector3D(sourceLinear.Get(0, 2), sourceLinear.Get(1, 2), sourceLinear.Get(2, 2)); - - LogManager.Info( - $"[矩阵诊断] 源矩阵行: r0=({sourceRow0.X:F4},{sourceRow0.Y:F4},{sourceRow0.Z:F4}), " + - $"r1=({sourceRow1.X:F4},{sourceRow1.Y:F4},{sourceRow1.Z:F4}), " + - $"r2=({sourceRow2.X:F4},{sourceRow2.Y:F4},{sourceRow2.Z:F4})"); - LogManager.Info( - $"[矩阵诊断] 源矩阵列: c0=({sourceCol0.X:F4},{sourceCol0.Y:F4},{sourceCol0.Z:F4}), " + - $"c1=({sourceCol1.X:F4},{sourceCol1.Y:F4},{sourceCol1.Z:F4}), " + - $"c2=({sourceCol2.X:F4},{sourceCol2.Y:F4},{sourceCol2.Z:F4})"); - - LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按当前乘法", effectiveLinear, transpose: false); - LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按转置乘法", effectiveLinear, transpose: true); - } - - private void LogMatrixBasisMapping(string prefix, Matrix3 matrix, bool transpose) - { - var mappedX = TransformDirection(matrix, new Vector3D(1, 0, 0), transpose); - var mappedY = TransformDirection(matrix, new Vector3D(0, 1, 0), transpose); - var mappedZ = TransformDirection(matrix, new Vector3D(0, 0, 1), transpose); - - LogManager.Info( - $"{prefix}: X->({mappedX.X:F4},{mappedX.Y:F4},{mappedX.Z:F4}), " + - $"Y->({mappedY.X:F4},{mappedY.Y:F4},{mappedY.Z:F4}), " + - $"Z->({mappedZ.X:F4},{mappedZ.Y:F4},{mappedZ.Z:F4})"); - } - - private Vector3D TransformDirection(Matrix3 matrix, Vector3D direction, bool transpose) - { - if (!transpose) - { - return new Vector3D( - matrix.Get(0, 0) * direction.X + matrix.Get(0, 1) * direction.Y + matrix.Get(0, 2) * direction.Z, - matrix.Get(1, 0) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(1, 2) * direction.Z, - matrix.Get(2, 0) * direction.X + matrix.Get(2, 1) * direction.Y + matrix.Get(2, 2) * direction.Z); - } - - return new Vector3D( - matrix.Get(0, 0) * direction.X + matrix.Get(1, 0) * direction.Y + matrix.Get(2, 0) * direction.Z, - matrix.Get(0, 1) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(2, 1) * direction.Z, - matrix.Get(0, 2) * direction.X + matrix.Get(1, 2) * direction.Y + matrix.Get(2, 2) * direction.Z); - } - public void ResetToCADPosition() { if (_virtualObjectModelItem == null) return; diff --git a/src/Utils/CollisionSceneHelper.cs b/src/Utils/CollisionSceneHelper.cs index 5465247..b2fc291 100644 --- a/src/Utils/CollisionSceneHelper.cs +++ b/src/Utils/CollisionSceneHelper.cs @@ -104,11 +104,21 @@ namespace NavisworksTransport.Utils // 从PathAnimationManager获取当前实际朝向 var pam = PathAnimationManager.GetInstance(); var currentYaw = pam.CurrentYaw; - var state = ModelItemTransformHelper.SaveObjectState(animatedObject, currentYaw); + var currentRotation = pam.CurrentRotation; + var hasCustomRotation = pam.HasCurrentRotation; + var currentState = pam.GetObjectCurrentPosition(animatedObject); + pam.SaveObjectState(animatedObject); + + var state = ModelItemTransformHelper.SaveObjectState( + animatedObject, + currentYaw, + currentState.Position, + currentRotation, + hasCustomRotation); - LogManager.Info(string.Format("已保存动画物体状态: {0}, 位置=({1:F2},{2:F2},{3:F2}), 朝向={4:F2}°", + LogManager.Info(string.Format("已保存动画物体状态: {0}, 位置=({1:F2},{2:F2},{3:F2}), 朝向={4:F2}°, customRotation={5}", animatedObject.DisplayName, state.Position.X, state.Position.Y, state.Position.Z, - currentYaw * 180 / Math.PI)); + currentYaw * 180 / Math.PI, state.HasCustomRotation)); return state; } @@ -130,6 +140,14 @@ namespace NavisworksTransport.Utils try { + var pam = PathAnimationManager.GetInstance(); + if (pam != null && ReferenceEquals(pam.AnimatedObject, animatedObject) && state.HasCustomRotation) + { + pam.RestoreAnimatedObjectState(animatedObject); + LogManager.Info(string.Format("动画物体已通过PathAnimationManager恢复三维姿态: {0}", animatedObject.DisplayName)); + return; + } + // 检查是否是虚拟物体 bool isVirtual = VirtualObjectManager.Instance.IsVirtualObjectActive && VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject; diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs index c66ad9d..99c6a99 100644 --- a/src/Utils/ModelItemTransformHelper.cs +++ b/src/Utils/ModelItemTransformHelper.cs @@ -110,6 +110,32 @@ namespace NavisworksTransport.Utils return GetAxisDirectionFromTransform(transform, 2, new Vector3D(0, 0, 1)); } + /// + /// 获取绑定在物体局部坐标中的稳定底面锚点。 + /// 适用于带俯仰/侧倾的三维姿态对象,避免使用世界 AABB 底面中心导致锚点漂移。 + /// + public static Point3D GetStableBottomAnchorPoint(BoundingBox3D bounds, Transform3D transform) + { + if (bounds == null) + { + return new Point3D(0, 0, 0); + } + + if (transform == null) + { + return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z); + } + + var up = GetUpDirectionFromTransform(transform); + var localSize = EstimateLocalBoxSize(bounds, transform); + double halfHeight = localSize.Z / 2.0; + + return new Point3D( + bounds.Center.X - up.X * halfHeight, + bounds.Center.Y - up.Y * halfHeight, + bounds.Center.Z - up.Z * halfHeight); + } + /// /// 根据世界轴对齐包围盒和物体变换,估算物体在局部坐标系中的尺寸。 /// 适用于刚性箱体类对象,用于顶/底面对接点推导。 @@ -362,20 +388,121 @@ namespace NavisworksTransport.Utils } /// - /// 将物体移动到指定位置和完整三维线性姿态(先回到CAD原始位置,再移动)。 - /// targetLinear 的列向量分别表示物体局部 X/Y/Z 轴在世界坐标中的方向。 + /// 基于物体当前实际姿态,增量移动到目标位置和完整三维朝向。 + /// 适用于动画过程中的真实模型物体,避免每次回到 CAD 原始状态导致位置跑偏。 /// - public static void MoveItemToPositionAndLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear) + public static void MoveItemIncrementallyToPositionAndRotation( + ModelItem item, + Point3D currentPosition, + Rotation3D currentRotation, + Point3D targetPosition, + Rotation3D targetRotation) { - ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: false); + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { item }; + + Rotation3D deltaRotation = BuildDeltaRotation(currentRotation, targetRotation); + var deltaLinear = new Transform3D(deltaRotation).Linear; + var currentLinear = new Transform3D(currentRotation).Linear; + var targetLinear = new Transform3D(targetRotation).Linear; + + var rotatedCurrentPosition = new Point3D( + deltaLinear.Get(0, 0) * currentPosition.X + deltaLinear.Get(0, 1) * currentPosition.Y + deltaLinear.Get(0, 2) * currentPosition.Z, + deltaLinear.Get(1, 0) * currentPosition.X + deltaLinear.Get(1, 1) * currentPosition.Y + deltaLinear.Get(1, 2) * currentPosition.Z, + deltaLinear.Get(2, 0) * currentPosition.X + deltaLinear.Get(2, 1) * currentPosition.Y + deltaLinear.Get(2, 2) * currentPosition.Z); + + var translation = new Vector3D( + targetPosition.X - rotatedCurrentPosition.X, + targetPosition.Y - rotatedCurrentPosition.Y, + targetPosition.Z - rotatedCurrentPosition.Z); + + LogManager.Info( + $"[模型增量姿态] {item.DisplayName} 当前=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " + + $"目标=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " + + $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})"); + + LogManager.Info( + $"[模型增量姿态] {item.DisplayName} 当前旋转: " + + $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " + + $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " + + $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})"); + + LogManager.Info( + $"[模型增量姿态] {item.DisplayName} 目标旋转: " + + $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + + $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + + $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + + LogManager.Info( + $"[模型增量姿态] {item.DisplayName} 增量旋转: " + + $"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " + + $"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " + + $"Z=({deltaLinear.Get(0, 2):F4},{deltaLinear.Get(1, 2):F4},{deltaLinear.Get(2, 2):F4}), " + + $"旋后当前点=({rotatedCurrentPosition.X:F3},{rotatedCurrentPosition.Y:F3},{rotatedCurrentPosition.Z:F3})"); + + // 用显式三步法应用三维增量位姿: + // 1. 把当前锚点移到原点 + // 2. 绕原点旋转到目标姿态 + // 3. 再把锚点移到目标位置 + // 这与旧 yaw 链路的补偿语义一致,只是从二维扩展到三维。 + var toOrigin = Transform3D.CreateTranslation(new Vector3D( + -currentPosition.X, + -currentPosition.Y, + -currentPosition.Z)); + doc.Models.OverridePermanentTransform(modelItems, toOrigin, false); + + var rotationOnlyComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor(); + rotationOnlyComponents.Rotation = deltaRotation; + var rotationOnly = rotationOnlyComponents.Combine(); + doc.Models.OverridePermanentTransform(modelItems, rotationOnly, false); + + var toTarget = Transform3D.CreateTranslation(new Vector3D( + targetPosition.X, + targetPosition.Y, + targetPosition.Z)); + doc.Models.OverridePermanentTransform(modelItems, toTarget, false); + + LogIncrementalTransformActual(item, targetPosition, targetRotation); } - /// - /// 将物体移动到指定位置和完整三维线性姿态,同时保留当前缩放。 - /// - public static void MoveItemToPositionAndLinearTransformWithCurrentScale(ModelItem item, Point3D targetPosition, Matrix3 targetLinear) + private static void LogIncrementalTransformActual(ModelItem item, Point3D targetPosition, Rotation3D targetRotation) { - ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: true); + try + { + var actualBounds = item.BoundingBox(); + var actualTransform = item.Transform; + var actualRotation = actualTransform.Factor().Rotation; + var actualLinear = new Transform3D(actualRotation).Linear; + var targetLinear = new Transform3D(targetRotation).Linear; + var actualPosition = new Point3D( + actualBounds.Center.X, + actualBounds.Center.Y, + actualBounds.Min.Z); + + LogManager.Info( + $"[模型增量姿态] {item.DisplayName} 应用后位置: " + + $"实际=({actualPosition.X:F3},{actualPosition.Y:F3},{actualPosition.Z:F3}), " + + $"期望=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " + + $"偏差=({actualPosition.X - targetPosition.X:F3},{actualPosition.Y - targetPosition.Y:F3},{actualPosition.Z - targetPosition.Z:F3})"); + + LogManager.Info( + $"[模型增量姿态] {item.DisplayName} 应用后旋转: " + + $"实际X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " + + $"实际Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " + + $"实际Z=({actualLinear.Get(0, 2):F4},{actualLinear.Get(1, 2):F4},{actualLinear.Get(2, 2):F4}), " + + $"期望X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + + $"期望Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + + $"期望Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + } + catch (Exception ex) + { + LogManager.Warning($"[模型增量姿态] 输出应用后日志失败: {ex.Message}"); + } } /// @@ -554,14 +681,21 @@ namespace NavisworksTransport.Utils doc.Models.ResetPermanentTransform(modelItems); + var originalTransform = item.Transform; + var originalComponents = originalTransform.Factor(); + var originalRotation = originalComponents.Rotation; var originalBounds = item.BoundingBox(); + var originalUp = GetUpDirectionFromTransform(originalTransform); + var localSize = EstimateLocalBoxSize(originalBounds, originalTransform); + double halfHeight = localSize.Z / 2.0; var originalGroundPos = new Point3D( - originalBounds.Center.X, - originalBounds.Center.Y, - originalBounds.Min.Z + originalBounds.Center.X - originalUp.X * halfHeight, + originalBounds.Center.Y - originalUp.Y * halfHeight, + originalBounds.Center.Z - originalUp.Z * halfHeight ); - var rotationTransform = new Transform3D(targetRotation); + Rotation3D deltaRotation = BuildDeltaRotation(originalRotation, targetRotation); + var rotationTransform = new Transform3D(deltaRotation); var linear = rotationTransform.Linear; var rotatedGroundPos = new Point3D( @@ -582,77 +716,71 @@ namespace NavisworksTransport.Utils var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); var components = identity.Factor(); components.Scale = currentScale; - components.Rotation = targetRotation; + components.Rotation = deltaRotation; components.Translation = translation; transform = components.Combine(); } else { - transform = new Transform3D(targetRotation, translation); + transform = new Transform3D(deltaRotation, translation); } + LogAbsoluteTransformDiagnostics(item, originalRotation, targetRotation, deltaRotation, originalGroundPos, targetPosition, translation, halfHeight, originalUp); doc.Models.OverridePermanentTransform(modelItems, transform, false); } - private static void ApplyAbsoluteLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear, bool preserveCurrentScale) + private static Rotation3D BuildDeltaRotation(Rotation3D originalRotation, Rotation3D targetRotation) { - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - if (targetLinear == null) - { - throw new ArgumentNullException(nameof(targetLinear)); - } - - var doc = Application.ActiveDocument; - var modelItems = new ModelItemCollection { item }; - Vector3D currentScale = new Vector3D(1, 1, 1); - - if (preserveCurrentScale) - { - var currentComponents = item.Transform.Factor(); - currentScale = currentComponents.Scale; - } - - doc.Models.ResetPermanentTransform(modelItems); - - var originalBounds = item.BoundingBox(); - var originalGroundPos = new Point3D( - originalBounds.Center.X, - originalBounds.Center.Y, - originalBounds.Min.Z - ); - - Matrix3 effectiveLinear = preserveCurrentScale - ? BuildScaledLinearMatrix(targetLinear, currentScale) - : targetLinear; - - Point3D transformedGroundPos = TransformPoint(effectiveLinear, originalGroundPos); - var translation = new Vector3D( - targetPosition.X - transformedGroundPos.X, - targetPosition.Y - transformedGroundPos.Y, - targetPosition.Z - transformedGroundPos.Z - ); - - doc.Models.OverridePermanentTransform(modelItems, new Transform3D(effectiveLinear, translation), false); + var originalInverse = originalRotation.Invert(); + var deltaTransform = Transform3D.Multiply( + new Transform3D(targetRotation), + new Transform3D(originalInverse)); + return deltaTransform.Factor().Rotation; } - private static Matrix3 BuildScaledLinearMatrix(Matrix3 linear, Vector3D scale) + private static void LogAbsoluteTransformDiagnostics( + ModelItem item, + Rotation3D originalRotation, + Rotation3D targetRotation, + Rotation3D deltaRotation, + Point3D originalGroundPos, + Point3D targetPosition, + Vector3D translation, + double halfHeight, + Vector3D originalUp) { - return new Matrix3( - linear.Get(0, 0) * scale.X, linear.Get(0, 1) * scale.Y, linear.Get(0, 2) * scale.Z, - linear.Get(1, 0) * scale.X, linear.Get(1, 1) * scale.Y, linear.Get(1, 2) * scale.Z, - linear.Get(2, 0) * scale.X, linear.Get(2, 1) * scale.Y, linear.Get(2, 2) * scale.Z); - } + try + { + var originalLinear = new Transform3D(originalRotation).Linear; + var targetLinear = new Transform3D(targetRotation).Linear; + var deltaLinear = new Transform3D(deltaRotation).Linear; - private static Point3D TransformPoint(Matrix3 linear, Point3D point) - { - return new Point3D( - linear.Get(0, 0) * point.X + linear.Get(0, 1) * point.Y + linear.Get(0, 2) * point.Z, - linear.Get(1, 0) * point.X + linear.Get(1, 1) * point.Y + linear.Get(1, 2) * point.Z, - linear.Get(2, 0) * point.X + linear.Get(2, 1) * point.Y + linear.Get(2, 2) * point.Z); + LogManager.Info( + $"[模型姿态] {item.DisplayName} 原始: " + + $"X=({originalLinear.Get(0, 0):F4},{originalLinear.Get(1, 0):F4},{originalLinear.Get(2, 0):F4}), " + + $"Y=({originalLinear.Get(0, 1):F4},{originalLinear.Get(1, 1):F4},{originalLinear.Get(2, 1):F4}), " + + $"Z=({originalLinear.Get(0, 2):F4},{originalLinear.Get(1, 2):F4},{originalLinear.Get(2, 2):F4})"); + + LogManager.Info( + $"[模型姿态] {item.DisplayName} 目标: " + + $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + + $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + + $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + + LogManager.Info( + $"[模型姿态] {item.DisplayName} 增量: " + + $"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " + + $"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " + + $"Z=({deltaLinear.Get(0, 2):F4},{deltaLinear.Get(1, 2):F4},{deltaLinear.Get(2, 2):F4}), " + + $"原始Up=({originalUp.X:F4},{originalUp.Y:F4},{originalUp.Z:F4}), 半高={halfHeight:F3}, " + + $"原始底面中心=({originalGroundPos.X:F3},{originalGroundPos.Y:F3},{originalGroundPos.Z:F3}), " + + $"目标底面中心=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " + + $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})"); + } + catch (Exception ex) + { + LogManager.Warning($"[模型姿态] 输出诊断日志失败: {ex.Message}"); + } } /// @@ -735,6 +863,8 @@ namespace NavisworksTransport.Utils { public Point3D Position { get; set; } public double YawRadians { get; set; } + public Rotation3D Rotation { get; set; } + public bool HasCustomRotation { get; set; } public Transform3D Transform { get; set; } } @@ -742,7 +872,12 @@ namespace NavisworksTransport.Utils /// 保存物体当前状态 /// 注意:YawRadians 通过当前位置计算,因为 Transform 返回的是CAD原始值 /// - public static ObjectStateSnapshot SaveObjectState(ModelItem item, double? currentYaw = null) + public static ObjectStateSnapshot SaveObjectState( + ModelItem item, + double? currentYaw = null, + Point3D currentPosition = null, + Rotation3D currentRotation = null, + bool hasCustomRotation = false) { if (item == null) return null; @@ -764,8 +899,10 @@ namespace NavisworksTransport.Utils return new ObjectStateSnapshot { - Position = new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z), + Position = currentPosition ?? new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z), YawRadians = yaw, + Rotation = currentRotation ?? Rotation3D.Identity, + HasCustomRotation = hasCustomRotation, Transform = item.Transform }; } @@ -792,6 +929,19 @@ namespace NavisworksTransport.Utils private static void RestoreObjectStateInternal(ModelItem item, ObjectStateSnapshot state, bool preserveScale) { if (item == null || state == null) return; + + if (state.HasCustomRotation) + { + if (preserveScale) + { + MoveItemToPositionAndRotationWithCurrentScale(item, state.Position, state.Rotation); + } + else + { + MoveItemToPositionAndRotation(item, state.Position, state.Rotation); + } + return; + } var doc = Application.ActiveDocument; var modelItems = new ModelItemCollection { item }; From 015a2863ea5dd05c28f5f8ba801d659109585d14 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Fri, 20 Mar 2026 14:59:41 +0800 Subject: [PATCH 06/87] Fix collision pose restore and deployment verification --- deploy-plugin.bat | 104 ++++------- .../GenerateCollisionReportCommand.cs | 17 +- src/Core/Animation/PathAnimationManager.cs | 171 +++++++++++++++--- .../Collision/ClashDetectiveIntegration.cs | 124 ++++++++----- src/Core/PathDatabase.cs | 29 ++- src/Core/PathPointRenderPlugin.cs | 16 +- .../ViewModels/AnimationControlViewModel.cs | 20 +- src/Utils/CollisionSceneHelper.cs | 97 +++++++--- src/Utils/ModelItemTransformHelper.cs | 8 +- 9 files changed, 405 insertions(+), 181 deletions(-) diff --git a/deploy-plugin.bat b/deploy-plugin.bat index 0d5d775..eaf9e8a 100644 --- a/deploy-plugin.bat +++ b/deploy-plugin.bat @@ -2,69 +2,43 @@ setlocal set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin" -set "BUILD_DIR=bin\x64\Release" -set "MAX_WAIT_SECONDS=15" -set "COPY_RETRY_COUNT=5" +set "BUILD_DIR=%~dp0bin\x64\Release" +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$ErrorActionPreference = 'Stop';" ^ + "$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^ + "$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^ + "" ^ + "Get-Process Roamer -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue;" ^ + "$deadline = (Get-Date).AddSeconds(15);" ^ + "while (Get-Process Roamer -ErrorAction SilentlyContinue) {" ^ + " if ((Get-Date) -ge $deadline) { throw 'Roamer.exe still running after 15 seconds.' }" ^ + " Start-Sleep -Milliseconds 500;" ^ + "}" ^ + "" ^ + "New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^ + "Copy-Item (Join-Path $buildDir '*.dll') $targetDir -Force;" ^ + "if (Test-Path (Join-Path $buildDir 'resources')) {" ^ + " New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^ + " Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^ + "}" ^ + "" ^ + "$sourceFiles = @();" ^ + "$sourceFiles += Get-ChildItem $buildDir -File -Filter '*.dll';" ^ + "if (Test-Path (Join-Path $buildDir 'resources')) {" ^ + " $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^ + "}" ^ + "" ^ + "foreach ($sourceFile in $sourceFiles) {" ^ + " $targetFile = if ($sourceFile.DirectoryName -eq $buildDir) { Join-Path $targetDir $sourceFile.Name } else { Join-Path (Join-Path $targetDir 'resources') $sourceFile.Name };" ^ + " if (-not (Test-Path $targetFile)) { throw ('Deployment verification failed: missing target file ' + $targetFile) }" ^ + " $targetInfo = Get-Item $targetFile;" ^ + " if ($sourceFile.Length -ne $targetInfo.Length -or $sourceFile.LastWriteTime -ne $targetInfo.LastWriteTime) {" ^ + " throw ('Deployment verification failed for ' + $sourceFile.Name + ': source=' + $sourceFile.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $sourceFile.Length + '), target=' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $targetInfo.Length + ')');" ^ + " }" ^ + " Write-Host ('Verified ' + $sourceFile.Name + ': ' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff'));" ^ + "}" ^ + "" ^ + "Write-Host 'Plugin deployed successfully!';" -:: Stop Navisworks to release locked plugin DLLs. -taskkill /F /IM Roamer.exe 2>nul -if %errorlevel% == 0 ( - echo Navisworks process terminated. -) - -:: Wait until Roamer.exe fully exits. Fixed delay alone is not reliable. -set /a WAIT_COUNT=0 -:WAIT_FOR_ROAMER_EXIT -tasklist /FI "IMAGENAME eq Roamer.exe" | find /I "Roamer.exe" >nul -if errorlevel 1 goto ROAMER_EXITED -if %WAIT_COUNT% GEQ %MAX_WAIT_SECONDS% ( - echo Warning: Roamer.exe still appears to be running after %MAX_WAIT_SECONDS% seconds. - goto ROAMER_EXITED -) -timeout /t 1 /nobreak >nul -set /a WAIT_COUNT+=1 -goto WAIT_FOR_ROAMER_EXIT - -:ROAMER_EXITED - -if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%" - -set /a COPY_ATTEMPT=0 -:COPY_DLLS -copy "%BUILD_DIR%\*.dll" "%TARGET_DIR%\" >nul -if not errorlevel 1 goto DLLS_DEPLOYED -set /a COPY_ATTEMPT+=1 -if %COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% ( - echo Failed to deploy plugin DLLs after %COPY_RETRY_COUNT% attempts. - exit /b 1 -) -timeout /t 1 /nobreak >nul -goto COPY_DLLS - -:DLLS_DEPLOYED -echo Plugin DLLs deployed. - -:: Copy bundled resources. -if exist "%BUILD_DIR%\resources" goto PREPARE_RESOURCES -goto DEPLOY_DONE - -:PREPARE_RESOURCES -if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources" -set /a RESOURCE_COPY_ATTEMPT=0 - -:COPY_RESOURCES -xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul -if not errorlevel 1 goto RESOURCES_DEPLOYED -set /a RESOURCE_COPY_ATTEMPT+=1 -if %RESOURCE_COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% ( - echo Failed to deploy resources after %COPY_RETRY_COUNT% attempts. - exit /b 1 -) -timeout /t 1 /nobreak >nul -goto COPY_RESOURCES - -:RESOURCES_DEPLOYED -echo Resources folder deployed. - -:DEPLOY_DONE -echo Plugin deployed successfully! +if errorlevel 1 exit /b 1 +exit /b 0 diff --git a/src/Commands/GenerateCollisionReportCommand.cs b/src/Commands/GenerateCollisionReportCommand.cs index 68cd8d8..8614b98 100644 --- a/src/Commands/GenerateCollisionReportCommand.cs +++ b/src/Commands/GenerateCollisionReportCommand.cs @@ -362,16 +362,16 @@ namespace NavisworksTransport.Commands { UpdateProgress(92, "生成默认路径截图..."); - // 记录截图前虚拟物体实际位置和朝向 + // 记录截图前虚拟物体位置和动画管理器状态。 + // 不读取 ModelItem.Transform 的“实际朝向”,因为 override 后该值不可靠,容易误导排查。 var virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject; if (virtualObject != null) { var bounds = virtualObject.BoundingBox(); - var actualYaw = ModelItemTransformHelper.GetYawFromTransform(virtualObject.Transform); var pam = PathAnimationManager.GetInstance(); - LogManager.Info(string.Format("[默认截图前] 虚拟物体实际位置: ({0:F2},{1:F2},{2:F2}), 实际朝向: {3:F2}°, PAM记录朝向: {4:F2}°", + LogManager.Info(string.Format("[默认截图前] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}", bounds.Center.X, bounds.Center.Y, bounds.Min.Z, - actualYaw * 180 / Math.PI, pam.CurrentYaw * 180 / Math.PI)); + pam.CurrentYaw * 180 / Math.PI, pam.HasCurrentRotation)); } else { @@ -393,16 +393,15 @@ namespace NavisworksTransport.Commands "overview" ); - // 记录截图后虚拟物体实际位置和朝向 + // 记录截图后虚拟物体位置和动画管理器状态。 virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject; if (virtualObject != null) { var bounds = virtualObject.BoundingBox(); - var actualYaw = ModelItemTransformHelper.GetYawFromTransform(virtualObject.Transform); var pam = PathAnimationManager.GetInstance(); - LogManager.Info(string.Format("[默认截图后] 虚拟物体实际位置: ({0:F2},{1:F2},{2:F2}), 实际朝向: {3:F2}°, PAM记录朝向: {4:F2}°", + LogManager.Info(string.Format("[默认截图后] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}", bounds.Center.X, bounds.Center.Y, bounds.Min.Z, - actualYaw * 180 / Math.PI, pam.CurrentYaw * 180 / Math.PI)); + pam.CurrentYaw * 180 / Math.PI, pam.HasCurrentRotation)); } if (screenshotPath != null) @@ -1097,4 +1096,4 @@ namespace NavisworksTransport.Commands } } } -} \ No newline at end of file +} diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index e7b4c7b..1f53517 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -1060,6 +1060,12 @@ namespace NavisworksTransport.Core.Animation LogManager.Debug($"移动物体位置: {framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}"); LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}"); + double actualYawRadians = yawRadians; + if (!frame.HasCustomRotation && _objectRotationCorrection != 0.0) + { + actualYawRadians += _objectRotationCorrection * Math.PI / 180.0; + } + var collisionResult = new CollisionResult { ClashGuid = Guid.NewGuid(), @@ -1074,22 +1080,33 @@ namespace NavisworksTransport.Core.Animation (virtualBoundingBox.Min.Y + virtualBoundingBox.Max.Y + colliderBox.Min.Y + colliderBox.Max.Y) / 4, (virtualBoundingBox.Min.Z + virtualBoundingBox.Max.Z + colliderBox.Min.Z + colliderBox.Max.Z) / 4 ), - // 🔥 重要:记录虚拟物体的包围盒中心,而不是底面中心 - // 原因: - // 1. framePosition是路径点,代表物体在地面上的位置(底面中心) - // 2. ClashDetective移动逻辑使用包围盒中心进行定位 - // 3. 如果记录底面中心,ClashDetective会把包围盒中心移动到底面位置,导致物体下沉半个高度 - // 4. 记录包围盒中心可以确保ClashDetective移动后的位置与预计算时的虚拟物体位置一致 - Item1Position = new Point3D( - framePosition.X, - framePosition.Y, - framePosition.Z + (virtualBoundingBox.Max.Z - virtualBoundingBox.Min.Z) / 2 - ), + // 碰撞结果中的 Item1Position 必须与后续恢复链路的基准点语义一致: + // 1. 二维 yaw 路径沿用成熟旧逻辑,记录包围盒中心; + // 2. 三维 customRotation 路径记录动画跟踪点(framePosition)。 + Item1Position = frame.HasCustomRotation + ? new Point3D( + framePosition.X, + framePosition.Y, + framePosition.Z) + : new Point3D( + framePosition.X, + framePosition.Y, + framePosition.Z + (virtualBoundingBox.Max.Z - virtualBoundingBox.Min.Z) / 2), Item2Position = GetObjectPosition(collider), - Item1YawRadians = yawRadians, // 记录运动物体朝向 + Item1YawRadians = actualYawRadians, // 记录运动物体实际播放朝向 + Item1Rotation = frame.HasCustomRotation ? frame.Rotation : Rotation3D.Identity, + Item1HasCustomRotation = frame.HasCustomRotation, HasPositionInfo = true }; + LogManager.Debug( + $"[保存碰撞结果] 帧={i}, " + + $"位置语义={(frame.HasCustomRotation ? "轨迹跟踪点" : "包围盒中心")}, " + + $"原始Yaw={yawRadians * 180.0 / Math.PI:F2}°, " + + $"实际Yaw={actualYawRadians * 180.0 / Math.PI:F2}°, " + + $"customRotation={frame.HasCustomRotation}, " + + $"保存位置=({collisionResult.Item1Position.X:F3}, {collisionResult.Item1Position.Y:F3}, {collisionResult.Item1Position.Z:F3})"); + frame.Collisions.Add(collisionResult); _allCollisionResults.Add(collisionResult); } @@ -1328,19 +1345,52 @@ namespace NavisworksTransport.Core.Animation if (_animationFrames != null && _animationFrames.Count > 0) { var firstFrame = _animationFrames[0]; - var currentPos = _animatedObject.BoundingBox().Center; - var distanceToStart = Math.Sqrt( - Math.Pow(currentPos.X - firstFrame.Position.X, 2) + - Math.Pow(currentPos.Y - firstFrame.Position.Y, 2) - ); - - // 如果距离起点超过0.1米,或者朝向不一致,需要重置 - var yawDiff = Math.Abs(_currentYaw - firstFrame.YawRadians); - if (yawDiff > Math.PI) yawDiff = 2 * Math.PI - yawDiff; // 处理角度环绕 - - if (distanceToStart > 0.1 || yawDiff > 0.01) + + double distanceToStart; + bool needsReset; + string mismatchSummary; + + if (firstFrame.HasCustomRotation && _route?.PathType == PathType.Rail) { - LogManager.Info($"[动画开始] 物体不在起点,重置到起点: 距离={distanceToStart:F2}m, 角度差={yawDiff * 180 / Math.PI:F2}°"); + var dx = _currentPosition.X - firstFrame.Position.X; + var dy = _currentPosition.Y - firstFrame.Position.Y; + var dz = _currentPosition.Z - firstFrame.Position.Z; + distanceToStart = Math.Sqrt(dx * dx + dy * dy + dz * dz); + + var currentLinear = new Transform3D(_currentRotation).Linear; + var targetLinear = new Transform3D(firstFrame.Rotation).Linear; + double rotationDiff = + Math.Abs(currentLinear.Get(0, 0) - targetLinear.Get(0, 0)) + + Math.Abs(currentLinear.Get(0, 1) - targetLinear.Get(0, 1)) + + Math.Abs(currentLinear.Get(0, 2) - targetLinear.Get(0, 2)) + + Math.Abs(currentLinear.Get(1, 0) - targetLinear.Get(1, 0)) + + Math.Abs(currentLinear.Get(1, 1) - targetLinear.Get(1, 1)) + + Math.Abs(currentLinear.Get(1, 2) - targetLinear.Get(1, 2)) + + Math.Abs(currentLinear.Get(2, 0) - targetLinear.Get(2, 0)) + + Math.Abs(currentLinear.Get(2, 1) - targetLinear.Get(2, 1)) + + Math.Abs(currentLinear.Get(2, 2) - targetLinear.Get(2, 2)); + + needsReset = distanceToStart > 0.1 || rotationDiff > 0.01; + mismatchSummary = $"距离={distanceToStart:F2}m, 三维姿态差={rotationDiff:F4}"; + } + else + { + var currentPos = _animatedObject.BoundingBox().Center; + distanceToStart = Math.Sqrt( + Math.Pow(currentPos.X - firstFrame.Position.X, 2) + + Math.Pow(currentPos.Y - firstFrame.Position.Y, 2) + ); + + var yawDiff = Math.Abs(_currentYaw - firstFrame.YawRadians); + if (yawDiff > Math.PI) yawDiff = 2 * Math.PI - yawDiff; + + needsReset = distanceToStart > 0.1 || yawDiff > 0.01; + mismatchSummary = $"距离={distanceToStart:F2}m, 角度差={yawDiff * 180 / Math.PI:F2}°"; + } + + if (needsReset) + { + LogManager.Info($"[动画开始] 物体不在起点,重置到起点: {mismatchSummary}"); MoveObjectToPathStart(); } } @@ -2371,6 +2421,79 @@ namespace NavisworksTransport.Core.Animation } } + /// + /// 判断给定对象是否由当前动画管理器控制。 + /// 对虚拟物体,判断当前激活的虚拟物体;对真实模型,判断当前动画对象引用。 + /// + public bool ControlsAnimatedObject(ModelItem obj) + { + if (obj == null) + { + return false; + } + + if (_isVirtualObject) + { + return VirtualObjectManager.Instance.IsVirtualObjectActive && + ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, obj); + } + + return ReferenceEquals(_animatedObject, obj); + } + + /// + /// 使用当前动画链路将动画对象移动到指定姿态。 + /// 适用于碰撞点定位、报告截图等场景,确保与动画播放使用同一套位置/姿态应用语义。 + /// + public void MoveAnimatedObjectToPose( + ModelItem obj, + Point3D targetPosition, + double targetYawRadians, + Rotation3D targetRotation, + bool hasCustomRotation) + { + if (obj == null) + { + LogManager.Warning("MoveAnimatedObjectToPose: 对象为空"); + return; + } + + if (!ControlsAnimatedObject(obj)) + { + LogManager.Warning($"MoveAnimatedObjectToPose: 对象不受当前动画管理器控制: {obj.DisplayName}"); + return; + } + + try + { + var originalAnimatedObject = _animatedObject; + if (!_isVirtualObject) + { + _animatedObject = obj; + } + + if (hasCustomRotation) + { + UpdateObjectPosition(targetPosition, targetRotation); + } + else + { + UpdateObjectPosition(targetPosition, targetYawRadians); + } + + _animatedObject = originalAnimatedObject; + + LogManager.Info( + $"[动画姿态复用] {obj.DisplayName} 已移动到目标姿态: " + + $"位置=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), customRotation={hasCustomRotation}"); + } + catch (Exception ex) + { + LogManager.Error($"MoveAnimatedObjectToPose: 应用目标姿态失败: {ex.Message}", ex); + throw; + } + } + /// /// 获取播放方向(1=正向,-1=反向) /// diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs index d168eda..68ea04d 100644 --- a/src/Core/Collision/ClashDetectiveIntegration.cs +++ b/src/Core/Collision/ClashDetectiveIntegration.cs @@ -177,6 +177,8 @@ namespace NavisworksTransport public Point3D Item1Position { get; set; } public Point3D Item2Position { get; set; } public double Item1YawRadians { get; set; } + public Rotation3D Item1Rotation { get; set; } + public bool Item1HasCustomRotation { get; set; } public bool HasPositionInfo { get; set; } } @@ -275,9 +277,14 @@ namespace NavisworksTransport collisionRecord.Item1PosY = collision.Item1Position.Y; collisionRecord.Item1PosZ = collision.Item1Position.Z; collisionRecord.Item1YawRadians = collision.Item1YawRadians; + collisionRecord.Item1RotA = collision.Item1Rotation?.A; + collisionRecord.Item1RotB = collision.Item1Rotation?.B; + collisionRecord.Item1RotC = collision.Item1Rotation?.C; + collisionRecord.Item1RotD = collision.Item1Rotation?.D; + collisionRecord.Item1HasCustomRotation = collision.Item1HasCustomRotation; collisionRecord.HasPositionInfo = true; - LogManager.Debug($"[保存碰撞对象] 记录运动物体位置: ({collisionRecord.Item1PosX:F2}, {collisionRecord.Item1PosY:F2}, {collisionRecord.Item1PosZ:F2}), 朝向: {collisionRecord.Item1YawRadians:F2} rad"); + LogManager.Debug($"[保存碰撞对象] 记录运动物体位置: ({collisionRecord.Item1PosX:F2}, {collisionRecord.Item1PosY:F2}, {collisionRecord.Item1PosZ:F2}), yaw={collisionRecord.Item1YawRadians:F2} rad, customRotation={collisionRecord.Item1HasCustomRotation}"); } collisionObjects.Add(collisionRecord); @@ -492,9 +499,22 @@ namespace NavisworksTransport { collisionResult.Item1Position = new Point3D(obj.Item1PosX.Value, obj.Item1PosY.Value, obj.Item1PosZ.Value); collisionResult.Item1YawRadians = obj.Item1YawRadians ?? 0.0; + collisionResult.Item1HasCustomRotation = obj.Item1HasCustomRotation; + if (obj.Item1HasCustomRotation && + obj.Item1RotA.HasValue && + obj.Item1RotB.HasValue && + obj.Item1RotC.HasValue && + obj.Item1RotD.HasValue) + { + collisionResult.Item1Rotation = new Rotation3D( + obj.Item1RotA.Value, + obj.Item1RotB.Value, + obj.Item1RotC.Value, + obj.Item1RotD.Value); + } collisionResult.HasPositionInfo = true; - LogManager.Debug($"[加载碰撞结果] 恢复运动物体位置: ({obj.Item1PosX:F2}, {obj.Item1PosY:F2}, {obj.Item1PosZ:F2}), 朝向: {obj.Item1YawRadians:F2} rad"); + LogManager.Debug($"[加载碰撞结果] 恢复运动物体位置: ({obj.Item1PosX:F2}, {obj.Item1PosY:F2}, {obj.Item1PosZ:F2}), yaw={obj.Item1YawRadians:F2} rad, customRotation={obj.Item1HasCustomRotation}"); } results.Add(collisionResult); @@ -778,56 +798,68 @@ namespace NavisworksTransport var modelItems = new ModelItemCollection { testAnimatedObject }; var targetPosition = candidate.Item1Position; var targetYaw = candidate.Item1YawRadians; + var targetRotation = candidate.Item1Rotation; // 🔥 关键:为了最高精度,先回到CAD原始位置,再移动到目标位置 // 避免累积误差影响碰撞检测精度 doc.Models.ResetPermanentTransform(modelItems); - // 获取CAD原始状态 - var originalBounds = testAnimatedObject.BoundingBox(); - var originalPos = new Point3D( - (originalBounds.Min.X + originalBounds.Max.X) / 2, - (originalBounds.Min.Y + originalBounds.Max.Y) / 2, - (originalBounds.Min.Z + originalBounds.Max.Z) / 2 - ); - var originalYaw = ModelItemTransformHelper.GetYawFromTransform(testAnimatedObject.Transform); - - // 计算从CAD位置到目标位置的偏移和旋转 - var deltaPos = new Vector3D( - targetPosition.X - originalPos.X, - targetPosition.Y - originalPos.Y, - targetPosition.Z - originalPos.Z - ); - double deltaYaw = targetYaw - originalYaw; - - // 应用变换(包含旋转补偿) - Transform3D transform; - if (Math.Abs(deltaYaw) > 0.001) + if (candidate.Item1HasCustomRotation && targetRotation != null) { - // 计算绕原点旋转的补偿 - double cos = Math.Cos(deltaYaw); - double sin = Math.Sin(deltaYaw); - double rotatedX = originalPos.X * cos - originalPos.Y * sin; - double rotatedY = originalPos.X * sin + originalPos.Y * cos; - - var compensatedTranslation = new Vector3D( - targetPosition.X - rotatedX, - targetPosition.Y - rotatedY, - deltaPos.Z - ); - - var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); - var components = identity.Factor(); - components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw); - components.Translation = compensatedTranslation; - transform = components.Combine(); + var originalBounds = testAnimatedObject.BoundingBox(); + var originalCenter = originalBounds.Center; + var originalRotation = testAnimatedObject.Transform.Factor().Rotation; + ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation( + testAnimatedObject, + originalCenter, + originalRotation, + targetPosition, + targetRotation); } else { - transform = Transform3D.CreateTranslation(deltaPos); + var originalBounds = testAnimatedObject.BoundingBox(); + var originalPos = new Point3D( + (originalBounds.Min.X + originalBounds.Max.X) / 2, + (originalBounds.Min.Y + originalBounds.Max.Y) / 2, + (originalBounds.Min.Z + originalBounds.Max.Z) / 2 + ); + var originalYaw = ModelItemTransformHelper.GetYawFromTransform(testAnimatedObject.Transform); + + var deltaPos = new Vector3D( + targetPosition.X - originalPos.X, + targetPosition.Y - originalPos.Y, + targetPosition.Z - originalPos.Z + ); + double deltaYaw = targetYaw - originalYaw; + + Transform3D transform; + if (Math.Abs(deltaYaw) > 0.001) + { + double cos = Math.Cos(deltaYaw); + double sin = Math.Sin(deltaYaw); + double rotatedX = originalPos.X * cos - originalPos.Y * sin; + double rotatedY = originalPos.X * sin + originalPos.Y * cos; + + var compensatedTranslation = new Vector3D( + targetPosition.X - rotatedX, + targetPosition.Y - rotatedY, + deltaPos.Z + ); + + var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); + var components = identity.Factor(); + components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw); + components.Translation = compensatedTranslation; + transform = components.Combine(); + } + else + { + transform = Transform3D.CreateTranslation(deltaPos); + } + + doc.Models.OverridePermanentTransform(modelItems, transform, false); } - - doc.Models.OverridePermanentTransform(modelItems, transform, false); var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}"; @@ -892,6 +924,8 @@ namespace NavisworksTransport Item1Position = candidate.Item1Position, Item2Position = candidate.Item2Position, Item1YawRadians = candidate.Item1YawRadians, + Item1Rotation = candidate.Item1Rotation, + Item1HasCustomRotation = candidate.Item1HasCustomRotation, HasPositionInfo = candidate.HasPositionInfo }; LogManager.Debug($"[ClashDetective确认] 记录碰撞位置: {confirmedPositionKey}, 位置: ({candidate.Item1Position.X:F2}, {candidate.Item1Position.Y:F2}, {candidate.Item1Position.Z:F2})"); @@ -1006,6 +1040,8 @@ namespace NavisworksTransport Item1Position = confirmedPosition?.Item1Position, Item2Position = confirmedPosition?.Item2Position, Item1YawRadians = confirmedPosition?.Item1YawRadians ?? 0, + Item1Rotation = confirmedPosition?.Item1Rotation, + Item1HasCustomRotation = confirmedPosition?.Item1HasCustomRotation ?? false, HasPositionInfo = confirmedPosition != null }; clashResults.Add(collisionResult); @@ -2135,6 +2171,8 @@ namespace NavisworksTransport public Point3D Item1Position { get; set; } public Point3D Item2Position { get; set; } public double Item1YawRadians { get; set; } // 运动物体朝向(弧度) + public Rotation3D Item1Rotation { get; set; } // 运动物体完整三维姿态 + public bool Item1HasCustomRotation { get; set; } public bool HasPositionInfo { get; set; } // IEquatable 实现:基于碰撞对象进行去重 @@ -2199,4 +2237,4 @@ namespace NavisworksTransport CollisionCount = collisionCount; } } -} \ No newline at end of file +} diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index 2688d96..65d02e0 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -239,6 +239,11 @@ namespace NavisworksTransport Item1PosY REAL, Item1PosZ REAL, Item1YawRadians REAL, + Item1RotA REAL, + Item1RotB REAL, + Item1RotC REAL, + Item1RotD REAL, + Item1HasCustomRotation INTEGER DEFAULT 0, HasPositionInfo INTEGER DEFAULT 0, FOREIGN KEY(DetectionRecordId) REFERENCES CollisionDetectionRecords(Id) ON DELETE CASCADE ) @@ -1109,9 +1114,11 @@ namespace NavisworksTransport var sql = @" INSERT INTO ClashDetectiveCollisionObjects (DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName, - Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians, HasPositionInfo) + Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians, + Item1RotA, Item1RotB, Item1RotC, Item1RotD, Item1HasCustomRotation, HasPositionInfo) VALUES (@detectionRecordId, @modelIndex, @pathId, @displayName, @objectName, - @item1PosX, @item1PosY, @item1PosZ, @item1YawRadians, @hasPositionInfo) + @item1PosX, @item1PosY, @item1PosZ, @item1YawRadians, + @item1RotA, @item1RotB, @item1RotC, @item1RotD, @item1HasCustomRotation, @hasPositionInfo) "; foreach (var obj in objects) @@ -1127,6 +1134,11 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@item1PosY", obj.Item1PosY.HasValue ? (object)obj.Item1PosY.Value : DBNull.Value); cmd.Parameters.AddWithValue("@item1PosZ", obj.Item1PosZ.HasValue ? (object)obj.Item1PosZ.Value : DBNull.Value); cmd.Parameters.AddWithValue("@item1YawRadians", obj.Item1YawRadians.HasValue ? (object)obj.Item1YawRadians.Value : DBNull.Value); + cmd.Parameters.AddWithValue("@item1RotA", obj.Item1RotA.HasValue ? (object)obj.Item1RotA.Value : DBNull.Value); + cmd.Parameters.AddWithValue("@item1RotB", obj.Item1RotB.HasValue ? (object)obj.Item1RotB.Value : DBNull.Value); + cmd.Parameters.AddWithValue("@item1RotC", obj.Item1RotC.HasValue ? (object)obj.Item1RotC.Value : DBNull.Value); + cmd.Parameters.AddWithValue("@item1RotD", obj.Item1RotD.HasValue ? (object)obj.Item1RotD.Value : DBNull.Value); + cmd.Parameters.AddWithValue("@item1HasCustomRotation", obj.Item1HasCustomRotation ? 1 : 0); cmd.Parameters.AddWithValue("@hasPositionInfo", obj.HasPositionInfo ? 1 : 0); cmd.ExecuteNonQuery(); } @@ -1154,7 +1166,8 @@ namespace NavisworksTransport { var sql = @" SELECT Id, DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName, - Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians, HasPositionInfo + Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians, + Item1RotA, Item1RotB, Item1RotC, Item1RotD, Item1HasCustomRotation, HasPositionInfo FROM ClashDetectiveCollisionObjects WHERE DetectionRecordId = @detectionRecordId "; @@ -1178,6 +1191,11 @@ namespace NavisworksTransport Item1PosY = reader["Item1PosY"] != DBNull.Value ? Convert.ToDouble(reader["Item1PosY"]) : (double?)null, Item1PosZ = reader["Item1PosZ"] != DBNull.Value ? Convert.ToDouble(reader["Item1PosZ"]) : (double?)null, Item1YawRadians = reader["Item1YawRadians"] != DBNull.Value ? Convert.ToDouble(reader["Item1YawRadians"]) : (double?)null, + Item1RotA = reader["Item1RotA"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotA"]) : (double?)null, + Item1RotB = reader["Item1RotB"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotB"]) : (double?)null, + Item1RotC = reader["Item1RotC"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotC"]) : (double?)null, + Item1RotD = reader["Item1RotD"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotD"]) : (double?)null, + Item1HasCustomRotation = reader["Item1HasCustomRotation"] != DBNull.Value && Convert.ToInt32(reader["Item1HasCustomRotation"]) == 1, HasPositionInfo = reader["HasPositionInfo"] != DBNull.Value && Convert.ToInt32(reader["HasPositionInfo"]) == 1 }); } @@ -3548,6 +3566,11 @@ namespace NavisworksTransport public double? Item1PosY { get; set; } public double? Item1PosZ { get; set; } public double? Item1YawRadians { get; set; } + public double? Item1RotA { get; set; } + public double? Item1RotB { get; set; } + public double? Item1RotC { get; set; } + public double? Item1RotD { get; set; } + public bool Item1HasCustomRotation { get; set; } public bool HasPositionInfo { get; set; } } diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index 9355a2a..3bdc5f6 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -1501,15 +1501,15 @@ namespace NavisworksTransport // 计算通行空间的垂直偏移(根据路径类型) double verticalOffset = 0; - if (_showObjectSpace) - { - if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Ground) - { - // 地面路径:路径点是地面位置,通行空间底面在地面,中心需要向下偏移半个高度 - verticalOffset = -height / 2.0; - } - else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) + if (_showObjectSpace) { + if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Ground) + { + // 地面路径:路径点是地面位置,通行空间底面在地面,中心需要向上偏移半个高度 + verticalOffset = height / 2.0; + } + else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) + { verticalOffset = RailPathPoseHelper.GetObjectSpaceCenterZOffset( visualization.PathRoute, height); diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index f62e11c..871d8f2 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -181,6 +181,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 碰撞位置信息(用于还原碰撞场景) public Point3D Item1Position { get; set; } public double Item1YawRadians { get; set; } + public Rotation3D Item1Rotation { get; set; } + public bool Item1HasCustomRotation { get; set; } public bool HasPositionInfo { get; set; } } @@ -2508,6 +2510,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels null, collisionObject.Item1Position, collisionObject.Item1YawRadians, + collisionObject.Item1Rotation, + collisionObject.Item1HasCustomRotation, false); return; } @@ -2546,6 +2550,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels animatedObject, collisionObject.Item1Position, collisionObject.Item1YawRadians, + collisionObject.Item1Rotation, + collisionObject.Item1HasCustomRotation, collisionObject.HasPositionInfo); // 🔥 虚拟物体缩放已通过MoveItemToPositionAndYawWithCurrentScale保留 @@ -3666,6 +3672,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels Item1Position = objRecord.HasPositionInfo && objRecord.Item1PosX.HasValue ? new Point3D(objRecord.Item1PosX.Value, objRecord.Item1PosY.Value, objRecord.Item1PosZ.Value) : null, Item1YawRadians = objRecord.Item1YawRadians ?? 0, + Item1Rotation = objRecord.Item1HasCustomRotation && + objRecord.Item1RotA.HasValue && + objRecord.Item1RotB.HasValue && + objRecord.Item1RotC.HasValue && + objRecord.Item1RotD.HasValue + ? new Rotation3D( + objRecord.Item1RotA.Value, + objRecord.Item1RotB.Value, + objRecord.Item1RotC.Value, + objRecord.Item1RotD.Value) + : Rotation3D.Identity, + Item1HasCustomRotation = objRecord.Item1HasCustomRotation, HasPositionInfo = objRecord.HasPositionInfo }; resultViewModel.CollisionObjects.Add(objViewModel); @@ -5349,4 +5367,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels #endregion } -} \ No newline at end of file +} diff --git a/src/Utils/CollisionSceneHelper.cs b/src/Utils/CollisionSceneHelper.cs index b2fc291..a1e5327 100644 --- a/src/Utils/CollisionSceneHelper.cs +++ b/src/Utils/CollisionSceneHelper.cs @@ -20,7 +20,8 @@ namespace NavisworksTransport.Utils { if (collision == null) return; MoveToCollisionAndFocus(collision.Item2 ?? collision.Item1, animatedObject, - collision.Item1Position, collision.Item1YawRadians, collision.HasPositionInfo); + collision.Item1Position, collision.Item1YawRadians, collision.Item1Rotation, + collision.Item1HasCustomRotation, collision.HasPositionInfo); } /// @@ -30,9 +31,12 @@ namespace NavisworksTransport.Utils /// 动画物体(运动物体) /// 碰撞位置 /// 碰撞朝向 + /// 碰撞完整姿态 + /// 是否有完整姿态 /// 是否有位置信息 public static void MoveToCollisionAndFocus(ModelItem hitObject, ModelItem animatedObject, - Point3D item1Position, double item1YawRadians, bool hasPositionInfo) + Point3D item1Position, double item1YawRadians, Rotation3D item1Rotation, + bool item1HasCustomRotation, bool hasPositionInfo) { try { @@ -53,35 +57,59 @@ namespace NavisworksTransport.Utils // 移动动画物体到碰撞位置 if (animatedObject != null && hasPositionInfo && item1Position != null) { - // 计算目标底面位置(Item1Position存储的是包围盒中心,需要转换为底面) - var bounds = animatedObject.BoundingBox(); - double halfHeight = (bounds.Max.Z - bounds.Min.Z) / 2.0; - var targetGroundPosition = new Point3D( - item1Position.X, - item1Position.Y, - item1Position.Z - halfHeight - ); - + var pam = PathAnimationManager.GetInstance(); + bool useAnimationPipeline = pam != null && + pam.ControlsAnimatedObject(animatedObject) && + item1HasCustomRotation && + item1Rotation != null; + // 检查是否是虚拟物体 bool isVirtual = VirtualObjectManager.Instance.IsVirtualObjectActive && VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject; - - // 使用工具方法从CAD原始状态移动到目标位置 - if (isVirtual) + + if (useAnimationPipeline) { - // 虚拟物体:保留当前缩放 - ModelItemTransformHelper.MoveItemToPositionAndYawWithCurrentScale( - animatedObject, targetGroundPosition, item1YawRadians); + pam.MoveAnimatedObjectToPose( + animatedObject, + item1Position, + item1YawRadians, + item1Rotation, + true); + + LogManager.Info(string.Format( + "运动物体已通过动画链路移动到碰撞位置: 位置=({0:F2}, {1:F2}, {2:F2}), customRotation={3}", + item1Position.X, item1Position.Y, item1Position.Z, + true)); + } + else if (item1HasCustomRotation && item1Rotation != null) + { + throw new InvalidOperationException( + $"三维碰撞点恢复必须复用动画主链路,当前对象未受 PathAnimationManager 控制: {animatedObject.DisplayName}"); } else { - // 普通物体:标准移动 - ModelItemTransformHelper.MoveItemToPositionAndYaw(animatedObject, targetGroundPosition, item1YawRadians); + var bounds = animatedObject.BoundingBox(); + double halfHeight = (bounds.Max.Z - bounds.Min.Z) / 2.0; + var targetGroundPosition = new Point3D( + item1Position.X, + item1Position.Y, + item1Position.Z - halfHeight + ); + + if (isVirtual) + { + ModelItemTransformHelper.MoveItemToPositionAndYawWithCurrentScale( + animatedObject, targetGroundPosition, item1YawRadians); + } + else + { + ModelItemTransformHelper.MoveItemToPositionAndYaw(animatedObject, targetGroundPosition, item1YawRadians); + } + + LogManager.Info(string.Format("运动物体已移动到碰撞位置: ({0:F2}, {1:F2}, {2:F2}), 朝向: {3:F2}°", + targetGroundPosition.X, targetGroundPosition.Y, targetGroundPosition.Z, + item1YawRadians * 180 / Math.PI)); } - - LogManager.Info(string.Format("运动物体已移动到碰撞位置: ({0:F2}, {1:F2}, {2:F2}), 朝向: {3:F2}°", - targetGroundPosition.X, targetGroundPosition.Y, targetGroundPosition.Z, - item1YawRadians * 180 / Math.PI)); } } catch (Exception ex) @@ -141,10 +169,27 @@ namespace NavisworksTransport.Utils try { var pam = PathAnimationManager.GetInstance(); - if (pam != null && ReferenceEquals(pam.AnimatedObject, animatedObject) && state.HasCustomRotation) + if (state.HasCustomRotation) { - pam.RestoreAnimatedObjectState(animatedObject); - LogManager.Info(string.Format("动画物体已通过PathAnimationManager恢复三维姿态: {0}", animatedObject.DisplayName)); + if (pam != null && ReferenceEquals(pam.AnimatedObject, animatedObject)) + { + pam.RestoreAnimatedObjectState(animatedObject); + LogManager.Info(string.Format("动画物体已通过PathAnimationManager恢复三维姿态: {0}", animatedObject.DisplayName)); + return; + } + + bool isVirtualWithCustomRotation = VirtualObjectManager.Instance.IsVirtualObjectActive && + VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject; + if (isVirtualWithCustomRotation) + { + ModelItemTransformHelper.RestoreObjectStateWithScale(animatedObject, state); + } + else + { + ModelItemTransformHelper.RestoreObjectState(animatedObject, state); + } + + LogManager.Info(string.Format("动画物体已通过状态快照恢复三维姿态: {0}", animatedObject.DisplayName)); return; } diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs index 99c6a99..b49342a 100644 --- a/src/Utils/ModelItemTransformHelper.cs +++ b/src/Utils/ModelItemTransformHelper.cs @@ -507,7 +507,9 @@ namespace NavisworksTransport.Utils /// /// 将物体移动到指定中心点和完整三维朝向,同时保留当前缩放。 - /// 适用于参考杆、辅助几何等以几何中心作为定位基准的场景。 + /// 仅适用于“几何中心就是业务定位点”的实体,例如参考杆、辅助几何、虚拟包围盒。 + /// 不适用于真实模型物体的动画、碰撞验证或场景恢复;真实模型应使用增量三步法, + /// 即先将固定参考点移到原点,再旋转,再移动到目标位置。 /// public static void MoveItemToCenterAndRotationWithCurrentScale(ModelItem item, Point3D targetCenter, Rotation3D targetRotation) { @@ -550,7 +552,9 @@ namespace NavisworksTransport.Utils /// /// 将物体移动到指定中心点和完整三维朝向。 /// 仅应用位置和旋转,不保留当前缩放。 - /// 适用于缩放已经在 Model 层完成的参考杆等场景。 + /// 仅适用于“几何中心就是业务定位点”的实体,例如缩放已经在 Model 层完成的参考杆。 + /// 不适用于真实模型物体的动画、碰撞验证或场景恢复;真实模型应使用增量三步法, + /// 即先将固定参考点移到原点,再旋转,再移动到目标位置。 /// public static void MoveItemToCenterAndRotation(ModelItem item, Point3D targetCenter, Rotation3D targetRotation) { From c9a926356c5ddaff359bd71f58cd0215e80f8431 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Fri, 20 Mar 2026 15:22:58 +0800 Subject: [PATCH 07/87] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E4=BB=BF=E7=9C=9FUI=EF=BC=8C=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E6=95=B0=E6=8D=AE=E5=BA=93=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Core/PathDatabase.cs | 4 ++-- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 24 +++++++++---------- src/UI/WPF/Views/PathEditingView.xaml | 14 ++++------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index 65d02e0..9477166 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -300,8 +300,8 @@ namespace NavisworksTransport // 12. 设置数据库版本(SQLite内置user_version) // 版本号格式:主版本*10000 + 次版本*100 + 修订号 - // 例如:2.1.3 = 20103 - ExecuteNonQuery("PRAGMA user_version = 20103"); // v2.1.3 - 重命名DetectionToleranceMeters为DetectionTolerance + // 例如:2.1.4 = 20104 + ExecuteNonQuery("PRAGMA user_version = 20104"); // v2.1.4 - ClashDetective碰撞对象位姿字段扩展与Rail三维恢复链路调整 // 创建索引 ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)"); diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 877389a..cdbeba0 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1302,7 +1302,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels RenderAssemblyReferenceLine(referenceLine); FocusOnAssemblyReferenceArea(referenceLine.StartPoint, referenceLine.EndPoint); - UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认终点锚点、方向、外端位置和杆体贴合情况"); + UpdateMainStatus("已生成终端安装辅助线,请在三维视图中确认终点锚点、方向、外端位置和辅助线贴合情况"); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); LogManager.Info( $"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " + @@ -1310,7 +1310,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels $"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2}), " + $"方向=({referenceLine.Direction.X:F3}, {referenceLine.Direction.Y:F3}, {referenceLine.Direction.Z:F3}), " + $"资源={AssemblyReferencePathManager.Instance.ReferenceResourceName}"); - }, "生成装配参考杆"); + }, "生成终端安装辅助线"); } private async Task ExecuteSelectAssemblyStartPointAsync() @@ -1329,7 +1329,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (!AssemblyReferencePathManager.Instance.HasReferenceLine) { - throw new InvalidOperationException("请先生成直线装配参考杆"); + throw new InvalidOperationException("请先生成终端安装辅助线"); } CleanupAssemblyReferenceSelection(); @@ -1348,9 +1348,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels } _pathPlanningManager.StartClickTool(PathPointType.StartPoint); - UpdateMainStatus("请在参考杆附近点击一个起点,系统会自动吸附到参考线上并生成直线路径"); + UpdateMainStatus("请在辅助线附近点击一个起点,系统会自动吸附到辅助线上并生成直线路径"); LogManager.Info("[直线装配] 已进入起点拾取模式"); - }, "选择直线装配起点"); + }, "选择终端安装起点"); } private void ExecuteClearAssemblyReferenceRod() @@ -1364,12 +1364,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels ClearAssemblyReferenceLine(); AssemblyStartPointText = "未选择"; OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); - UpdateMainStatus("已隐藏直线装配参考线"); - LogManager.Info("[直线装配] 已隐藏参考线"); + UpdateMainStatus("已隐藏终端安装辅助线"); + LogManager.Info("[直线装配] 已隐藏辅助线"); } catch (Exception ex) { - LogManager.Error($"[直线装配] 隐藏参考线失败: {ex.Message}"); + LogManager.Error($"[直线装配] 隐藏辅助线失败: {ex.Message}"); } } @@ -1393,13 +1393,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels { CreateAssemblyLinearRoute(projectedStartPoint); CleanupAssemblyReferenceSelection(); - UpdateMainStatus("已根据参考杆起点生成直线装配路径"); - }, "生成直线装配路径"); + UpdateMainStatus("已根据辅助线起点生成终端安装路径"); + }, "生成终端安装路径"); } catch (Exception ex) { LogManager.Error($"[直线装配] 处理起点拾取失败: {ex.Message}", ex); - UpdateMainStatus($"直线装配起点拾取失败: {ex.Message}"); + UpdateMainStatus($"终端安装起点拾取失败: {ex.Message}"); CleanupAssemblyReferenceSelection(); } } @@ -1521,7 +1521,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (referenceStartPoint != null && referenceEndPoint != null) { AssemblyTerminalObjectInfo = string.Format( - "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m,参考杆起点=({12:F2}, {13:F2}, {14:F2})", + "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m,辅助线外端=({12:F2}, {13:F2}, {14:F2})", bounds.Center.X, bounds.Center.Y, bounds.Center.Z, diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 5fdc00d..bc48783 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -305,10 +305,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 BorderBrush="#FFD4E7FF" BorderThickness="1"> - - @@ -366,10 +366,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 Text="{Binding AssemblyTerminalObjectName}" Style="{StaticResource ReadOnlyTextBoxStyle}"/> - @@ -394,7 +390,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 AssemblyGuideLine, + /// + /// 直线装配安装参考面样式(绿色半透明) + /// + AssemblyInstallationPlane, + /// /// 吊装路径样式(紫色) /// @@ -338,6 +343,11 @@ namespace NavisworksTransport /// public List ObjectSpaceMarkers { get; set; } + /// + /// 平面标记集合 + /// + public List PlaneMarkers { get; set; } + /// /// 是否显示控制点可视化(用户意图) /// @@ -358,6 +368,7 @@ namespace NavisworksTransport PathLineMarkers = new List(); TangentMarkers = new List(); ObjectSpaceMarkers = new List(); + PlaneMarkers = new List(); LastUpdated = DateTime.Now; } @@ -633,6 +644,11 @@ namespace NavisworksTransport RenderPointMarker(graphics, pointMarker); } + foreach (var planeMarker in visualization.PlaneMarkers) + { + RenderPlaneMarker(graphics, planeMarker); + } + // 渲染控制点连线(半透明) foreach (var controlLineMarker in visualization.ControlLineMarkers) { @@ -802,6 +818,55 @@ namespace NavisworksTransport } } + public void RenderRectanglePlane( + string pathId, + Point3D origin, + Vector3D xVector, + Vector3D yVector, + RenderStyleName renderStyleName, + bool filled = true) + { + if (string.IsNullOrWhiteSpace(pathId)) + { + throw new ArgumentException("pathId 不能为空。", nameof(pathId)); + } + + try + { + var visualization = new PathVisualization + { + PathId = pathId, + PathRoute = new PathRoute(pathId) + { + Id = pathId, + Description = $"矩形平面可视化:{pathId}" + } + }; + + var renderStyle = GetRenderStyle(renderStyleName); + visualization.PlaneMarkers.Add(new PlaneMarker + { + Origin = origin, + XVector = xVector, + YVector = yVector, + Color = renderStyle.Color, + Alpha = renderStyle.Alpha, + Filled = filled + }); + + lock (_lockObject) + { + _pathVisualizations[pathId] = visualization; + } + + RequestViewRefresh(); + } + catch (Exception ex) + { + LogManager.Error($"[平面渲染] 渲染矩形平面失败: {ex.Message}", ex); + } + } + /// /// 清除网格可视化 /// @@ -960,6 +1025,7 @@ namespace NavisworksTransport visualization.PointMarkers.Clear(); visualization.ControlLineMarkers.Clear(); // 确保没有控制点连线 visualization.PathLineMarkers.Clear(); // 确保没有路径连线 + visualization.PlaneMarkers.Clear(); var points = visualization.PathRoute.Points; if (points.Count == 0) return; @@ -2347,6 +2413,9 @@ namespace NavisworksTransport case RenderStyleName.AssemblyGuideLine: return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.85); // Material Green装配基准向量 + case RenderStyleName.AssemblyInstallationPlane: + return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.5); // Material Green安装参考面,50%透明 + case RenderStyleName.HoistingLine: return new RenderStyle(Color.FromByteRGB(156, 39, 176), 0.8); // Material Purple吊装路径,20%透明 @@ -3151,6 +3220,12 @@ namespace NavisworksTransport } } + private void RenderPlaneMarker(Graphics graphics, PlaneMarker marker) + { + graphics.Color(marker.Color, marker.Alpha); + graphics.Rectangle(marker.Origin, marker.XVector, marker.YVector, marker.Filled); + } + /// /// 渲染连线标记(支持直线段和圆弧段) /// @@ -3512,4 +3587,14 @@ namespace NavisworksTransport return $"CircleMarker[序号={SequenceNumber}, 类型={PointType}, 中心=({Center.X:F2},{Center.Y:F2},{Center.Z:F2}), 半径={Radius:F2}]"; } } + + public class PlaneMarker + { + public Point3D Origin { get; set; } + public Vector3D XVector { get; set; } + public Vector3D YVector { get; set; } + public Color Color { get; set; } + public double Alpha { get; set; } + public bool Filled { get; set; } = true; + } } diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index ef84caa..1335e08 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -141,11 +141,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels private bool _hasAssemblyTerminalObject; private bool _isSelectingAssemblyStartPoint; private bool _isSelectingAssemblyEndFacePoints; + private bool _isSelectingAssemblyInstallationPoint; private bool _hasAssemblyEndFaceAnalysis; + private bool _hasAssemblyInstallationReference; private ModelItem _assemblyTerminalObject; private Point3D _assemblyStartPoint; private Point3D _assemblyEndFaceCenterPoint; private Vector3 _assemblyEndFaceNormal; + private Point3D _assemblyInstallationPickPoint; + private Point3D _assemblyInstallationBaseAnchorPoint; + private Point3D _assemblyInstallationAnchorPoint; + private Vector3 _assemblyInstallationPlaneNormal; + private Vector3 _assemblyInstallationPlaneSpanDirection; + private double _assemblyInstallationOffsetDistanceInMeters; private readonly List _assemblyEndFaceSeedPoints = new List(); private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line"; @@ -153,6 +161,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const string AssemblyEndFaceSeedPathId = "assembly_end_face_seed_points"; private const string AssemblyEndFaceCenterPathId = "assembly_end_face_center"; private const string AssemblyEndFaceNormalPathId = "assembly_end_face_normal"; + private const string AssemblyInstallationPickPointPathId = "assembly_install_pick_point"; + private const string AssemblyInstallationAnchorPointPathId = "assembly_install_anchor_point"; + private const string AssemblyInstallationCenterLinePathId = "assembly_install_center_line"; + private const string AssemblyInstallationOffsetLinePathId = "assembly_install_offset_line"; + private const string AssemblyInstallationPlanePathId = "assembly_install_plane"; + private const double AssemblyInstallationPlanePaddingInMeters = 0.5; + private const double AssemblyInstallationLineLengthScale = 1.2; // 自动路径起点和终点的路径对象引用(用于正确的ID管理) private PathRoute _autoPathStartPointRoute = null; @@ -375,6 +390,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels { if (SetProperty(ref _assemblyAnchorVerticalOffsetInMeters, value) && HasAssemblyTerminalObject) { + if (_hasAssemblyInstallationReference) + { + UpdateAssemblyInstallationAnchorFromVerticalOffset(); + } RefreshAssemblyTerminalObjectInfo(); RenderAssemblyAnchorMarker(); RefreshAssemblyReferenceRodIfNeeded(); @@ -969,6 +988,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; } public ICommand GenerateAssemblyReferenceRodCommand { get; private set; } public ICommand SelectAssemblyStartPointCommand { get; private set; } + public ICommand SelectAssemblyInstallationPointCommand { get; private set; } public ICommand ClearAssemblyReferenceRodCommand { get; private set; } public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; } @@ -1007,16 +1027,27 @@ namespace NavisworksTransport.UI.WPF.ViewModels public bool CanExecuteSaveAsPath => SelectedPathRoute != null && SelectedPathRoute.Points.Count > 0; public bool CanGenerateAssemblyReferenceRod => HasAssemblyTerminalObject && + _hasAssemblyInstallationReference && AssemblyReferenceRodLengthInMeters > 0 && AssemblyReferenceRodDiameterInMeters > 0; public bool CanSelectAssemblyStartPoint => HasAssemblyTerminalObject && _pathPlanningManager != null && AssemblyReferencePathManager.Instance.HasReferenceLine && !IsSelectingAssemblyStartPoint; + public bool CanSelectAssemblyInstallationPoint => HasAssemblyTerminalObject && + _pathPlanningManager != null && + !_isSelectingAssemblyStartPoint && + !_isSelectingAssemblyEndFacePoints && + !_isSelectingAssemblyInstallationPoint; public bool CanAnalyzeAssemblyTerminalFace => HasAssemblyTerminalObject && _pathPlanningManager != null && !IsSelectingAssemblyStartPoint && - !IsSelectingAssemblyEndFacePoints; + !IsSelectingAssemblyEndFacePoints && + !_isSelectingAssemblyInstallationPoint; + + public string AssemblyInstallationPointButtonText => _hasAssemblyInstallationReference + ? "重选安装点" + : "选安装点"; public bool IsSelectingAssemblyEndFacePoints => _isSelectingAssemblyEndFacePoints; @@ -1301,6 +1332,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync()); GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod); SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint); + SelectAssemblyInstallationPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(), () => CanSelectAssemblyInstallationPoint); ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod()); AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace); } @@ -1332,12 +1364,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels HasAssemblyTerminalObject = true; AssemblyStartPointText = "未选择"; AssemblyTerminalObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(selectedItem); - InitializeAssemblyAnchorVerticalOffsetFromTerminalObject(); + ResetAssemblyEndFaceAnalysisState(); + ResetAssemblyInstallationReferenceState(); RefreshAssemblyTerminalObjectInfo(); - RenderAssemblyAnchorMarker(); + ClearAssemblyAnchorMarker(); ClearAssemblyEndFaceAnalysisVisuals(); + ClearAssemblyInstallationReferenceVisuals(); OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}"); LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}"); @@ -1450,13 +1485,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels CleanupAssemblyReferenceSelection(); CleanupAssemblyEndFaceSelection(clearVisuals: false); + CleanupAssemblyInstallationSelection(); ClearAssemblyEndFaceAnalysisVisuals(); + ResetAssemblyEndFaceAnalysisState(); + ClearAssemblyInstallationReferenceVisuals(); + ResetAssemblyInstallationReferenceState(); _assemblyEndFaceSeedPoints.Clear(); _pathPlanningManager.DisableMouseHandling(); _isSelectingAssemblyEndFacePoints = true; OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked; PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked; @@ -1472,6 +1512,45 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "分析终端端面"); } + private async Task ExecuteSelectAssemblyInstallationPointAsync() + { + await SafeExecuteAsync(() => + { + if (_pathPlanningManager == null) + { + throw new InvalidOperationException("路径规划管理器未初始化,无法选择安装点。"); + } + + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。"); + } + + CleanupAssemblyReferenceSelection(); + CleanupAssemblyEndFaceSelection(clearVisuals: false); + CleanupAssemblyInstallationSelection(); + ClearAssemblyInstallationReferenceVisuals(); + + _pathPlanningManager.DisableMouseHandling(); + _isSelectingAssemblyInstallationPoint = true; + OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); + + PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked; + PathClickToolPlugin.MouseClicked += OnAssemblyInstallationMouseClicked; + + if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) + { + CleanupAssemblyInstallationSelection(); + throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); + } + + UpdateMainStatus("请在终点箱体表面点击安装点,系统将按该点计算安装参考面和终点锚点。"); + LogManager.Info("[直线装配] 已进入安装点拾取模式"); + }, "选择安装点"); + } + private async void OnAssemblyReferenceMouseClicked(object sender, PickItemResult pickResult) { try @@ -1544,6 +1623,35 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private async void OnAssemblyInstallationMouseClicked(object sender, PickItemResult pickResult) + { + try + { + if (!_isSelectingAssemblyInstallationPoint || pickResult == null) + { + return; + } + + await SafeExecuteAsync(() => + { + if (!IsPickOnAssemblyTerminalObject(pickResult)) + { + UpdateMainStatus("请点击当前终点箱体表面,不要点到其他对象。"); + return; + } + + BuildAndRenderAssemblyInstallationReference(pickResult.Point); + CleanupAssemblyInstallationSelection(); + }, "处理安装点拾取"); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 安装点计算失败: {ex.Message}", ex); + UpdateMainStatus($"安装点计算失败: {ex.Message}"); + CleanupAssemblyInstallationSelection(); + } + } + private void CreateAssemblyLinearRoute(Point3D startPoint) { Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd; @@ -1610,11 +1718,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels ClearAssemblyCenterGuideLine(); ClearAssemblyReferenceLine(); ClearAssemblyEndFaceAnalysisVisuals(); + ClearAssemblyInstallationReferenceVisuals(); if (resetStartPointText) { AssemblyStartPointText = "未选择"; } OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); if (!string.IsNullOrWhiteSpace(statusMessage)) { @@ -1634,6 +1744,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("终点箱体未设置或已失效,无法计算对接点"); } + if (_hasAssemblyInstallationReference && _assemblyInstallationAnchorPoint != null) + { + return _assemblyInstallationAnchorPoint; + } + HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); @@ -1654,6 +1769,39 @@ namespace NavisworksTransport.UI.WPF.ViewModels return adapter.FromCanonicalPoint(canonicalAnchorPoint); } + private void UpdateAssemblyInstallationAnchorFromVerticalOffset() + { + if (!_hasAssemblyInstallationReference || + _assemblyInstallationBaseAnchorPoint == null) + { + return; + } + + double offset = UnitsConverter.ConvertFromMeters(_assemblyAnchorVerticalOffsetInMeters); + _assemblyInstallationAnchorPoint = new Point3D( + _assemblyInstallationBaseAnchorPoint.X + _assemblyInstallationPlaneNormal.X * (float)offset, + _assemblyInstallationBaseAnchorPoint.Y + _assemblyInstallationPlaneNormal.Y * (float)offset, + _assemblyInstallationBaseAnchorPoint.Z + _assemblyInstallationPlaneNormal.Z * (float)offset); + + RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint); + RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection); + } + + private Vector3 GetCurrentAssemblyOpticalAxisDirection() + { + Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); + Point3D sphereCenterPoint = new Point3D( + AssemblySphereCenterX, + AssemblySphereCenterY, + AssemblySphereCenterZ); + + Vector3 opticalAxisDirection = new Vector3( + (float)(opticalAxisReferencePoint.X - sphereCenterPoint.X), + (float)(opticalAxisReferencePoint.Y - sphereCenterPoint.Y), + (float)(opticalAxisReferencePoint.Z - sphereCenterPoint.Z)); + return Vector3.Normalize(opticalAxisDirection); + } + private Point3D GetAssemblyOpticalAxisReferencePoint() { if (_hasAssemblyEndFaceAnalysis && _assemblyEndFaceCenterPoint != null) @@ -1719,18 +1867,35 @@ namespace NavisworksTransport.UI.WPF.ViewModels } var bounds = _assemblyTerminalObject.BoundingBox(); - Point3D anchorPoint = GetAssemblyTerminalAnchorPoint(); string anchorText = GetAssemblyAnchorText(); string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装"; Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); string opticalAxisText = _hasAssemblyEndFaceAnalysis ? string.Format("端面中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z) : string.Format("箱体中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z); + string anchorPointText = _hasAssemblyInstallationReference + ? string.Format("{0}点=({1:F2}, {2:F2}, {3:F2})", + anchorText, + _assemblyInstallationAnchorPoint.X, + _assemblyInstallationAnchorPoint.Y, + _assemblyInstallationAnchorPoint.Z) + : string.Format("{0}点=未选择", anchorText); + string installationText = _hasAssemblyInstallationReference + ? string.Format( + "选定安装点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m", + _assemblyInstallationPickPoint.X, + _assemblyInstallationPickPoint.Y, + _assemblyInstallationPickPoint.Z, + _assemblyInstallationAnchorPoint.X, + _assemblyInstallationAnchorPoint.Y, + _assemblyInstallationAnchorPoint.Z, + _assemblyInstallationOffsetDistanceInMeters) + : "安装点=未选择"; if (referenceStartPoint != null && referenceEndPoint != null) { AssemblyTerminalObjectInfo = string.Format( - "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m,辅助线外端=({13:F2}, {14:F2}, {15:F2})", + "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7},{8},{9},垂直偏移={10:F3}m,辅助线外端=({11:F2}, {12:F2}, {13:F2})", bounds.Center.X, bounds.Center.Y, bounds.Center.Z, @@ -1738,11 +1903,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels bounds.Max.Y - bounds.Min.Y, bounds.Max.Z - bounds.Min.Z, mountText, - anchorText, - referenceEndPoint.X, - referenceEndPoint.Y, - referenceEndPoint.Z, + anchorPointText, opticalAxisText, + installationText, AssemblyAnchorVerticalOffsetInMeters, referenceStartPoint.X, referenceStartPoint.Y, @@ -1751,7 +1914,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels } AssemblyTerminalObjectInfo = string.Format( - "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m", + "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7},{8},{9},垂直偏移={10:F3}m", bounds.Center.X, bounds.Center.Y, bounds.Center.Z, @@ -1759,11 +1922,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels bounds.Max.Y - bounds.Min.Y, bounds.Max.Z - bounds.Min.Z, mountText, - anchorText, - anchorPoint.X, - anchorPoint.Y, - anchorPoint.Z, + anchorPointText, opticalAxisText, + installationText, AssemblyAnchorVerticalOffsetInMeters); } @@ -2015,7 +2176,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels Id = AssemblyEndFaceCenterPathId, Description = "终端安装端面中心" }; - AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.EndPoint); + AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.WayPoint); renderPlugin.RenderPointOnly(markerRoute); } @@ -2053,6 +2214,158 @@ namespace NavisworksTransport.UI.WPF.ViewModels RenderStyleName.AssemblyGuideLine); } + private void BuildAndRenderAssemblyInstallationReference(Point3D pickPoint) + { + Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); + Point3D sphereCenterPoint = new Point3D( + AssemblySphereCenterX, + AssemblySphereCenterY, + AssemblySphereCenterZ); + + Vector3 opticalAxisDirection = new Vector3( + (float)(opticalAxisReferencePoint.X - sphereCenterPoint.X), + (float)(opticalAxisReferencePoint.Y - sphereCenterPoint.Y), + (float)(opticalAxisReferencePoint.Z - sphereCenterPoint.Z)); + + AssemblyInstallationReferenceResult result = AssemblyInstallationReferenceBuilder.Build( + new Vector3((float)opticalAxisReferencePoint.X, (float)opticalAxisReferencePoint.Y, (float)opticalAxisReferencePoint.Z), + opticalAxisDirection, + new Vector3((float)pickPoint.X, (float)pickPoint.Y, (float)pickPoint.Z)); + + _hasAssemblyInstallationReference = true; + _assemblyInstallationPickPoint = pickPoint; + _assemblyInstallationBaseAnchorPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.AnchorPoint); + _assemblyInstallationPlaneNormal = result.PlaneNormal; + _assemblyInstallationPlaneSpanDirection = result.PlaneSpanDirection; + _assemblyInstallationOffsetDistanceInMeters = UnitsConverter.ConvertToMeters(result.OffsetDistance); + _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; + OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); + UpdateAssemblyInstallationAnchorFromVerticalOffset(); + + RenderAssemblyInstallationPickPoint(_assemblyInstallationPickPoint); + RenderAssemblyInstallationCenterLine(AssemblyEndFaceAnalyzer.ToPoint3D(result.InstallLineBasePoint), result.OpticalAxisDirection); + + RefreshAssemblyTerminalObjectInfo(); + RefreshAssemblyReferenceRodIfNeeded(); + UpdateMainStatus( + $"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m"); + LogManager.Info( + $"[直线装配] 安装参考已计算: 选定安装点=({pickPoint.X:F3}, {pickPoint.Y:F3}, {pickPoint.Z:F3}), " + + $"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " + + $"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})"); + OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); + } + + private void RenderAssemblyInstallationPickPoint(Point3D pickPoint) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.RemovePath(AssemblyInstallationPickPointPathId); + var markerRoute = new PathRoute("安装点") + { + Id = AssemblyInstallationPickPointPathId, + Description = "终端安装点" + }; + AddVisualizationPoint(markerRoute, pickPoint, "安装点", PathPointType.WayPoint); + renderPlugin.RenderPointOnly(markerRoute); + } + + private void RenderAssemblyInstallationAnchorPoint(Point3D anchorPoint) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.RemovePath(AssemblyInstallationAnchorPointPathId); + var markerRoute = new PathRoute("安装点") + { + Id = AssemblyInstallationAnchorPointPathId, + Description = "终端安装点" + }; + AddVisualizationPoint(markerRoute, anchorPoint, "安装点", PathPointType.WayPoint); + renderPlugin.RenderPointOnly(markerRoute); + } + + private void RenderAssemblyInstallationCenterLine(Point3D lineBasePoint, Vector3 axisDirection) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.ClearRailBaseline(AssemblyInstallationCenterLinePathId); + double halfLength = GetAssemblyInstallationPlaneSideLength() * AssemblyInstallationLineLengthScale / 2.0; + Point3D startPoint = new Point3D( + lineBasePoint.X - axisDirection.X * (float)halfLength, + lineBasePoint.Y - axisDirection.Y * (float)halfLength, + lineBasePoint.Z - axisDirection.Z * (float)halfLength); + Point3D endPoint = new Point3D( + lineBasePoint.X + axisDirection.X * (float)halfLength, + lineBasePoint.Y + axisDirection.Y * (float)halfLength, + lineBasePoint.Z + axisDirection.Z * (float)halfLength); + renderPlugin.RenderRailBaseline( + AssemblyInstallationCenterLinePathId, + new List { startPoint, endPoint }, + RenderStyleName.AssemblyGuideLine); + } + + private void RenderAssemblyInstallationPlane(Point3D planeCenter, Vector3 axisDirection, Vector3 planeSpanDirection) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + double sideLength = GetAssemblyInstallationPlaneSideLength(); + double halfLength = sideLength / 2.0; + double thickness = UnitsConverter.ConvertFromMeters(0.01); + + Vector3D xVector = new Vector3D( + axisDirection.X * (float)sideLength, + axisDirection.Y * (float)sideLength, + axisDirection.Z * (float)sideLength); + Vector3D yVector = new Vector3D( + planeSpanDirection.X * (float)sideLength, + planeSpanDirection.Y * (float)sideLength, + planeSpanDirection.Z * (float)sideLength); + Point3D origin = new Point3D( + planeCenter.X - axisDirection.X * (float)halfLength - planeSpanDirection.X * (float)halfLength + _assemblyInstallationPlaneNormal.X * thickness, + planeCenter.Y - axisDirection.Y * (float)halfLength - planeSpanDirection.Y * (float)halfLength + _assemblyInstallationPlaneNormal.Y * thickness, + planeCenter.Z - axisDirection.Z * (float)halfLength - planeSpanDirection.Z * (float)halfLength + _assemblyInstallationPlaneNormal.Z * thickness); + + renderPlugin.RemovePath(AssemblyInstallationPlanePathId); + renderPlugin.RenderRectanglePlane( + AssemblyInstallationPlanePathId, + origin, + xVector, + yVector, + RenderStyleName.AssemblyInstallationPlane, + filled: true); + } + + private double GetAssemblyInstallationPlaneSideLength() + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + return UnitsConverter.ConvertFromMeters(1.0); + } + + BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox(); + double maxSize = Math.Max( + bounds.Max.X - bounds.Min.X, + Math.Max(bounds.Max.Y - bounds.Min.Y, bounds.Max.Z - bounds.Min.Z)); + return maxSize + UnitsConverter.ConvertFromMeters(AssemblyInstallationPlanePaddingInMeters) * 2.0; + } + private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter) { if (adapter == null) @@ -2116,11 +2429,45 @@ namespace NavisworksTransport.UI.WPF.ViewModels renderPlugin.RemovePath(AssemblyEndFaceSeedPathId); renderPlugin.RemovePath(AssemblyEndFaceCenterPathId); renderPlugin.ClearRailBaseline(AssemblyEndFaceNormalPathId); + } + + private void ResetAssemblyEndFaceAnalysisState() + { _hasAssemblyEndFaceAnalysis = false; _assemblyEndFaceCenterPoint = null; _assemblyEndFaceNormal = default(Vector3); } + private void ClearAssemblyInstallationReferenceVisuals() + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.RemovePath(AssemblyInstallationPickPointPathId); + renderPlugin.RemovePath(AssemblyInstallationAnchorPointPathId); + renderPlugin.ClearRailBaseline(AssemblyInstallationCenterLinePathId); + renderPlugin.ClearRailBaseline(AssemblyInstallationOffsetLinePathId); + renderPlugin.RemovePath(AssemblyInstallationPlanePathId); + } + + private void ResetAssemblyInstallationReferenceState() + { + _hasAssemblyInstallationReference = false; + _assemblyInstallationPickPoint = null; + _assemblyInstallationBaseAnchorPoint = null; + _assemblyInstallationAnchorPoint = null; + _assemblyInstallationPlaneNormal = default(Vector3); + _assemblyInstallationPlaneSpanDirection = default(Vector3); + _assemblyInstallationOffsetDistanceInMeters = 0.0; + _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; + OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); + OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); + } + private void CleanupAssemblyReferenceSelection() { try @@ -2137,6 +2484,25 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private void CleanupAssemblyInstallationSelection() + { + try + { + PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked; + _isSelectingAssemblyInstallationPoint = false; + _pathPlanningManager?.EnableMouseHandling(); + _pathPlanningManager?.StopClickTool(); + OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 清理参考安装点拾取状态失败: {ex.Message}", ex); + } + } + private void CleanupAssemblyEndFaceSelection(bool clearVisuals) { try @@ -2149,9 +2515,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels { _assemblyEndFaceSeedPoints.Clear(); ClearAssemblyEndFaceAnalysisVisuals(); + ResetAssemblyEndFaceAnalysisState(); } OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); } catch (Exception ex) { @@ -2208,9 +2577,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels RenderAssemblyEndFaceNormal(centerPoint, result.Normal); RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyReferenceRodIfNeeded(); - - AssemblyTerminalObjectInfo = - $"{AssemblyTerminalObjectInfo} | 端面中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount},偏差={result.MaxPlaneDeviation:F6}"; UpdateMainStatus($"端面分析完成:中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount}"); LogManager.Info( $"[直线装配] 端面分析完成: 中心=({centerPoint.X:F3}, {centerPoint.Y:F3}, {centerPoint.Z:F3}), " + diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 0cea737..f71468e 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -263,6 +263,117 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 + + + + + + + public RailPathDefinitionMode RailPathDefinitionMode { get; set; } + /// + /// Rail 路径优先法向(宿主坐标)。 + /// 仅在存在显式安装面/业务法向时使用;为空时退回当前默认求解。 + /// + public Point3D RailPreferredNormal { get; set; } + // 数据库分析相关属性 /// /// 碰撞数量(从数据库加载) @@ -777,6 +783,7 @@ namespace NavisworksTransport IsCurved = false; RailMountMode = RailMountMode.UnderRail; RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; + RailPreferredNormal = null; } /// diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index 36b7d80..13983d2 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -297,6 +297,11 @@ namespace NavisworksTransport /// public Vector3D HorizontalDirection { get; set; } + /// + /// 通行空间的优先 up 方向(例如 Rail 安装面法向) + /// + public Vector3D UpDirection { get; set; } + public override string ToString() { return $"LineMarker[{FromIndex}->{ToIndex}, 类型={SegmentType}, 起点=({StartPoint.X:F2},{StartPoint.Y:F2},{StartPoint.Z:F2}), 终点=({EndPoint.X:F2},{EndPoint.Y:F2},{EndPoint.Z:F2})]"; @@ -1658,7 +1663,8 @@ namespace NavisworksTransport var endPoint = edge.SampledPoints.Last(); // 计算长方体的轴向量(right和up向量) - var (right, up) = CalculateCuboidAxes(startPoint, endPoint); + var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute); + var (right, up) = CalculateCuboidAxes(startPoint, endPoint, preferredUp); // 根据路径类型调整起点和终点位置 // 关键修复:垂直偏移应该沿着长方体的轴线方向(up向量)平移 @@ -1710,7 +1716,8 @@ namespace NavisworksTransport SampledPoints = edge.SampledPoints, Height = height, // 根据模式设置高度 Width = width, // 根据模式设置宽度 - Opacity = lineOpacity // 根据模式设置透明度 + Opacity = lineOpacity, // 根据模式设置透明度 + UpDirection = preferredUp }; visualization.PathLineMarkers.Add(lineMarker); } @@ -1736,7 +1743,8 @@ namespace NavisworksTransport var endPoint = edge.Trajectory.Te; // 计算长方体的轴向量(right和up向量) - var (right, up) = CalculateCuboidAxes(startPoint, endPoint); + var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute); + var (right, up) = CalculateCuboidAxes(startPoint, endPoint, preferredUp); // 根据路径类型调整起点和终点位置 // 关键修复:垂直偏移应该沿着长方体的轴线方向(up向量)平移 @@ -1818,7 +1826,8 @@ namespace NavisworksTransport SampledPoints = adjustedSampledPoints, Height = height, // 根据模式设置高度 Width = width, // 根据模式设置宽度 - Opacity = arcLineOpacity // 根据模式设置透明度 + Opacity = arcLineOpacity, // 根据模式设置透明度 + UpDirection = preferredUp }; visualization.PathLineMarkers.Add(arcMarker); @@ -1838,73 +1847,13 @@ namespace NavisworksTransport /// 路径段终点 /// 水平段方向向量(用于垂直路径渲染时确定通行空间方向) /// 元组(right向量,up向量) - private (Vector3D right, Vector3D up) CalculateCuboidAxes(Point3D startPoint, Point3D endPoint, Vector3D horizontalDirection = null) + private (Vector3D right, Vector3D up) CalculateCuboidAxes(Point3D startPoint, Point3D endPoint, Vector3D upReference = null, Vector3D horizontalDirection = null) { - // 计算路径段的方向向量 - var segmentDirection = new Vector3D( - endPoint.X - startPoint.X, - endPoint.Y - startPoint.Y, - endPoint.Z - startPoint.Z - ); - var segmentLength = Math.Sqrt( - segmentDirection.X * segmentDirection.X + - segmentDirection.Y * segmentDirection.Y + - segmentDirection.Z * segmentDirection.Z - ); - - // 归一化方向向量 - var normalizedDirection = new Vector3D( - segmentDirection.X / segmentLength, - segmentDirection.Y / segmentLength, - segmentDirection.Z / segmentLength - ); - - // 使用宿主 up 方向构造水平 right 向量,避免把世界 Z 写死到 Y-up 项目里。 - var hostUp = GetHostUpVector(); - var right = Cross(normalizedDirection, hostUp); - - // 如果方向向量接近宿主 up(垂直段),则使用水平段方向重新构造 right。 - double parallelToUp = Math.Abs( - normalizedDirection.X * hostUp.X + - normalizedDirection.Y * hostUp.Y + - normalizedDirection.Z * hostUp.Z); - if (parallelToUp > 0.9) - { - if (horizontalDirection != null) - { - right = Cross(horizontalDirection, hostUp); - } - else - { - right = Math.Abs(hostUp.X) < 0.9 - ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0))) - : Normalize(Cross(hostUp, new Vector3D(0, 1, 0))); - } - } - - // 归一化right向量 - var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z); - if (rightLength > 0.001) - { - right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength); - } - - // 计算高度向量(垂直于方向向量和宽度向量)- 这是长方体的轴线方向 - // up = direction × right - var up = new Vector3D( - normalizedDirection.Y * right.Z - normalizedDirection.Z * right.Y, - normalizedDirection.Z * right.X - normalizedDirection.X * right.Z, - normalizedDirection.X * right.Y - normalizedDirection.Y * right.X - ); - - // 归一化up向量 - var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z); - if (upLength > 0.001) - { - up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength); - } - - return (right, up); + return ObjectSpaceOrientationHelper.CalculateAxes( + startPoint, + endPoint, + upReference ?? GetHostUpVector(), + horizontalDirection); } /// @@ -2009,7 +1958,8 @@ namespace NavisworksTransport CalculatePassageSpaceParameters(visualization, isVerticalSegment, isTurn90Horizontal, isFirstOrLastSegment); // 计算长方体的轴向量(right和up向量) - var (right, up) = CalculateCuboidAxes(startPoint.Position, endPoint.Position, horizontalDirection); + var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute); + var (right, up) = CalculateCuboidAxes(startPoint.Position, endPoint.Position, preferredUp, horizontalDirection); // 根据路径类型调整起点和终点位置 // 关键修复:垂直偏移应该沿着长方体的轴线方向(up向量)平移,而不是简单地添加到Z坐标上 @@ -2099,7 +2049,8 @@ namespace NavisworksTransport Height = height, Width = width, Opacity = lineOpacity, - HorizontalDirection = horizontalDirection // 设置水平段方向向量 + HorizontalDirection = horizontalDirection, // 设置水平段方向向量 + UpDirection = preferredUp }; visualization.PathLineMarkers.Add(lineMarker); } @@ -2797,7 +2748,7 @@ namespace NavisworksTransport /// 宽度 /// 高度 /// 水平段方向向量(用于垂直路径渲染时确定通行空间方向) - private void RenderCuboidMarker(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, Vector3D horizontalDirection = null) + private void RenderCuboidMarker(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, Vector3D horizontalDirection = null, Vector3D upReference = null) { try { @@ -2817,47 +2768,11 @@ namespace NavisworksTransport // 归一化方向向量 direction = new Vector3D(direction.X / length, direction.Y / length, direction.Z / length); - var hostUp = GetHostUpVector(); - var right = Cross(direction, hostUp); - - double parallelToUp = Math.Abs( - direction.X * hostUp.X + - direction.Y * hostUp.Y + - direction.Z * hostUp.Z); - if (parallelToUp > 0.9) - { - if (horizontalDirection != null) - { - right = Cross(horizontalDirection, hostUp); - } - else - { - right = Math.Abs(hostUp.X) < 0.9 - ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0))) - : Normalize(Cross(hostUp, new Vector3D(0, 1, 0))); - } - } - - // 归一化right向量 - var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z); - if (rightLength > 0.001) - { - right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength); - } - - // 计算高度向量(垂直于方向向量和宽度向量) - var up = new Vector3D( - direction.Y * right.Z - direction.Z * right.Y, - direction.Z * right.X - direction.X * right.Z, - direction.X * right.Y - direction.Y * right.X - ); - - // 归一化up向量 - var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z); - if (upLength > 0.001) - { - up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength); - } + var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes( + startPoint, + endPoint, + upReference ?? GetHostUpVector(), + horizontalDirection); // 计算长方体的边界框和向量 var halfWidth = width / 2.0; @@ -2894,7 +2809,7 @@ namespace NavisworksTransport /// 高度 /// 指定长度(覆盖默认计算值) /// 水平段方向向量(用于垂直路径渲染时确定通行空间方向) - private void RenderCuboidMarkerWithLength(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, double length, Vector3D horizontalDirection = null) + private void RenderCuboidMarkerWithLength(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, double length, Vector3D horizontalDirection = null, Vector3D upReference = null) { try { @@ -2912,47 +2827,11 @@ namespace NavisworksTransport direction = new Vector3D(direction.X / directionLength, direction.Y / directionLength, direction.Z / directionLength); - var hostUp = GetHostUpVector(); - var right = Cross(direction, hostUp); - - double parallelToUp = Math.Abs( - direction.X * hostUp.X + - direction.Y * hostUp.Y + - direction.Z * hostUp.Z); - if (parallelToUp > 0.9) - { - if (horizontalDirection != null) - { - right = Cross(horizontalDirection, hostUp); - } - else - { - right = Math.Abs(hostUp.X) < 0.9 - ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0))) - : Normalize(Cross(hostUp, new Vector3D(0, 1, 0))); - } - } - - // 归一化right向量 - var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z); - if (rightLength > 0.001) - { - right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength); - } - - // 计算高度向量(垂直于方向向量和宽度向量) - var up = new Vector3D( - direction.Y * right.Z - direction.Z * right.Y, - direction.Z * right.X - direction.X * right.Z, - direction.X * right.Y - direction.Y * right.X - ); - - // 归一化up向量 - var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z); - if (upLength > 0.001) - { - up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength); - } + var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes( + startPoint, + endPoint, + upReference ?? GetHostUpVector(), + horizontalDirection); // 计算长方体的边界框和向量 var halfWidth = width / 2.0; @@ -3009,7 +2888,8 @@ namespace NavisworksTransport lineMarker.EndPoint, width, lineMarker.Height, - lineMarker.HorizontalDirection + lineMarker.HorizontalDirection, + lineMarker.UpDirection ); } } @@ -3052,11 +2932,25 @@ namespace NavisworksTransport width, lineMarker.Height, _passageAlongPath, - lineMarker.HorizontalDirection + lineMarker.HorizontalDirection, + lineMarker.UpDirection ); } } + private Vector3D ResolveObjectSpaceUpDirection(PathRoute route) + { + if (route?.PathType == NavisworksTransport.PathType.Rail && route.RailPreferredNormal != null) + { + return Normalize(new Vector3D( + route.RailPreferredNormal.X, + route.RailPreferredNormal.Y, + route.RailPreferredNormal.Z)); + } + + return GetHostUpVector(); + } + /// /// 计算采样点处的切线方向 /// diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 1335e08..05e2cd6 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -154,6 +154,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private Vector3 _assemblyInstallationPlaneNormal; private Vector3 _assemblyInstallationPlaneSpanDirection; private double _assemblyInstallationOffsetDistanceInMeters; + private readonly List _assemblyInstallationSeedPoints = new List(); private readonly List _assemblyEndFaceSeedPoints = new List(); private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line"; @@ -1347,6 +1348,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels { await SafeExecuteAsync(() => { + ClearNonGridPathVisualizations("[直线装配] 捕获箱体"); + var document = NavisApplication.ActiveDocument; var selectedItem = document?.CurrentSelection?.SelectedItems?.FirstOrDefault(); if (selectedItem == null) @@ -1379,6 +1382,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "捕获终点箱体"); } + private void ClearNonGridPathVisualizations(string context) + { + if (PathPointRenderPlugin.Instance == null) + { + return; + } + + try + { + PathPointRenderPlugin.Instance.ClearPathsExcept( + "grid_visualization_all", + "grid_visualization_channel", + "grid_visualization_unknown", + "grid_visualization_obstacle", + "grid_visualization_door"); + LogManager.Info($"{context}:已清除现有路径可视化显示(保留网格可视化)"); + } + catch (Exception ex) + { + LogManager.Error($"{context}:清除现有路径可视化失败: {ex.Message}", ex); + throw; + } + } + private async Task ExecuteGenerateAssemblyReferenceRodAsync() { await SafeExecuteAsync(() => @@ -1546,7 +1573,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); } - UpdateMainStatus("请在终点箱体表面点击安装点,系统将按该点计算安装参考面和终点锚点。"); + _assemblyInstallationSeedPoints.Clear(); + UpdateMainStatus("请在终点箱体表面连续点击两个安装面点,系统将按两点和光轴计算安装参考面与安装点。"); LogManager.Info("[直线装配] 已进入安装点拾取模式"); }, "选择安装点"); } @@ -1640,7 +1668,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels return; } - BuildAndRenderAssemblyInstallationReference(pickResult.Point); + _assemblyInstallationSeedPoints.Add(pickResult.Point); + RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints); + + int pickedCount = _assemblyInstallationSeedPoints.Count; + LogManager.Info($"[直线装配] 已记录安装面点 {pickedCount}: ({pickResult.Point.X:F3}, {pickResult.Point.Y:F3}, {pickResult.Point.Z:F3})"); + + if (pickedCount < 2) + { + UpdateMainStatus("已记录安装面点 1/2,请继续在同一安装面上点击第二个点。"); + return; + } + + BuildAndRenderAssemblyInstallationReference( + _assemblyInstallationSeedPoints[0], + _assemblyInstallationSeedPoints[1]); CleanupAssemblyInstallationSelection(); }, "处理安装点拾取"); } @@ -1665,6 +1707,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine }; + if (_hasAssemblyInstallationReference) + { + route.RailPreferredNormal = new Point3D( + _assemblyInstallationPlaneNormal.X, + _assemblyInstallationPlaneNormal.Y, + _assemblyInstallationPlaneNormal.Z); + } + route.AddPoint(new PathPoint(startPoint, "装配起点", PathPointType.StartPoint)); route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint)); @@ -1706,7 +1756,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels GetAssemblyAnchorText()); OnPropertyChanged(nameof(PathRoutes)); - LogManager.Info($"[直线装配] 已生成并完成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})"); + LogManager.Info( + $"[直线装配] 已生成并完成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})" + + $"{(route.RailPreferredNormal != null ? $",安装法向=({route.RailPreferredNormal.X:F3}, {route.RailPreferredNormal.Y:F3}, {route.RailPreferredNormal.Z:F3})" : string.Empty)}"); } private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage) @@ -1882,7 +1934,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels : string.Format("{0}点=未选择", anchorText); string installationText = _hasAssemblyInstallationReference ? string.Format( - "选定安装点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m", + "安装面点中点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m", _assemblyInstallationPickPoint.X, _assemblyInstallationPickPoint.Y, _assemblyInstallationPickPoint.Z, @@ -2214,7 +2266,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels RenderStyleName.AssemblyGuideLine); } - private void BuildAndRenderAssemblyInstallationReference(Point3D pickPoint) + private void BuildAndRenderAssemblyInstallationReference(Point3D firstPickPoint, Point3D secondPickPoint) { Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); Point3D sphereCenterPoint = new Point3D( @@ -2230,10 +2282,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels AssemblyInstallationReferenceResult result = AssemblyInstallationReferenceBuilder.Build( new Vector3((float)opticalAxisReferencePoint.X, (float)opticalAxisReferencePoint.Y, (float)opticalAxisReferencePoint.Z), opticalAxisDirection, - new Vector3((float)pickPoint.X, (float)pickPoint.Y, (float)pickPoint.Z)); + new Vector3((float)firstPickPoint.X, (float)firstPickPoint.Y, (float)firstPickPoint.Z), + new Vector3((float)secondPickPoint.X, (float)secondPickPoint.Y, (float)secondPickPoint.Z)); _hasAssemblyInstallationReference = true; - _assemblyInstallationPickPoint = pickPoint; + _assemblyInstallationPickPoint = AssemblyEndFaceAnalyzer.ToPoint3D((result.PickPoint + result.SecondaryPickPoint) * 0.5f); _assemblyInstallationBaseAnchorPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.AnchorPoint); _assemblyInstallationPlaneNormal = result.PlaneNormal; _assemblyInstallationPlaneSpanDirection = result.PlaneSpanDirection; @@ -2242,7 +2295,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); UpdateAssemblyInstallationAnchorFromVerticalOffset(); - RenderAssemblyInstallationPickPoint(_assemblyInstallationPickPoint); + RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints); RenderAssemblyInstallationCenterLine(AssemblyEndFaceAnalyzer.ToPoint3D(result.InstallLineBasePoint), result.OpticalAxisDirection); RefreshAssemblyTerminalObjectInfo(); @@ -2250,14 +2303,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels UpdateMainStatus( $"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m"); LogManager.Info( - $"[直线装配] 安装参考已计算: 选定安装点=({pickPoint.X:F3}, {pickPoint.Y:F3}, {pickPoint.Z:F3}), " + + $"[直线装配] 安装参考已计算: 安装面点1=({firstPickPoint.X:F3}, {firstPickPoint.Y:F3}, {firstPickPoint.Z:F3}), " + + $"安装面点2=({secondPickPoint.X:F3}, {secondPickPoint.Y:F3}, {secondPickPoint.Z:F3}), " + $"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " + $"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})"); OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); } - private void RenderAssemblyInstallationPickPoint(Point3D pickPoint) + private void RenderAssemblyInstallationPickPoints(IReadOnlyList pickPoints) { var renderPlugin = PathPointRenderPlugin.Instance; if (renderPlugin == null) @@ -2271,7 +2325,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels Id = AssemblyInstallationPickPointPathId, Description = "终端安装点" }; - AddVisualizationPoint(markerRoute, pickPoint, "安装点", PathPointType.WayPoint); + for (int i = 0; i < pickPoints.Count; i++) + { + AddVisualizationPoint(markerRoute, pickPoints[i], $"安装面点{i + 1}", PathPointType.WayPoint); + } renderPlugin.RenderPointOnly(markerRoute); } @@ -2462,6 +2519,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels _assemblyInstallationPlaneNormal = default(Vector3); _assemblyInstallationPlaneSpanDirection = default(Vector3); _assemblyInstallationOffsetDistanceInMeters = 0.0; + _assemblyInstallationSeedPoints.Clear(); _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); diff --git a/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs b/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs index 2167c93..16f01eb 100644 --- a/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs +++ b/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs @@ -8,6 +8,7 @@ namespace NavisworksTransport.Utils.GeometryAnalysis public Vector3 OpticalAxisBasePoint { get; set; } public Vector3 OpticalAxisDirection { get; set; } public Vector3 PickPoint { get; set; } + public Vector3 SecondaryPickPoint { get; set; } public Vector3 PlaneNormal { get; set; } public Vector3 PlaneSpanDirection { get; set; } public float OffsetDistance { get; set; } @@ -57,6 +58,72 @@ namespace NavisworksTransport.Utils.GeometryAnalysis OpticalAxisBasePoint = opticalAxisBasePoint, OpticalAxisDirection = axisDir, PickPoint = pickPoint, + SecondaryPickPoint = pickPoint, + PlaneNormal = planeNormal, + PlaneSpanDirection = planeSpanDirection, + OffsetDistance = offsetDistance, + InstallLineBasePoint = installLineBasePoint, + AnchorPoint = anchorPoint + }; + } + + public static AssemblyInstallationReferenceResult Build( + Vector3 opticalAxisBasePoint, + Vector3 opticalAxisDirection, + Vector3 firstPickPoint, + Vector3 secondPickPoint) + { + float axisLength = opticalAxisDirection.Length(); + if (axisLength < 1e-6f) + { + throw new InvalidOperationException("光轴方向长度过小,无法计算安装参考。"); + } + + Vector3 axisDir = opticalAxisDirection / axisLength; + Vector3 pickSegment = secondPickPoint - firstPickPoint; + Vector3 axisAlignedComponent = axisDir * Vector3.Dot(pickSegment, axisDir); + Vector3 planeSpanDirection = pickSegment - axisAlignedComponent; + float spanLength = planeSpanDirection.Length(); + if (spanLength < MinimumOffsetDistance) + { + throw new InvalidOperationException("两次安装点拾取几乎与光轴平行,无法确定安装参考面,请重新选择。"); + } + + planeSpanDirection /= spanLength; + + Vector3 midpoint = (firstPickPoint + secondPickPoint) * 0.5f; + Vector3 planeNormal = Vector3.Cross(axisDir, planeSpanDirection); + float normalLength = planeNormal.Length(); + if (normalLength < 1e-6f) + { + throw new InvalidOperationException("安装参考面法向计算失败,请重新选择。"); + } + + planeNormal /= normalLength; + + Vector3 midpointOffset = midpoint - opticalAxisBasePoint; + float signedOffset = Vector3.Dot(midpointOffset, planeNormal); + if (Math.Abs(signedOffset) < MinimumOffsetDistance) + { + throw new InvalidOperationException("安装面中点距离光轴过小,无法确定安装参考面,请重新选择。"); + } + + if (signedOffset < 0f) + { + planeNormal = -planeNormal; + signedOffset = -signedOffset; + } + + float offsetDistance = signedOffset; + Vector3 installLineBasePoint = opticalAxisBasePoint + planeNormal * offsetDistance; + Vector3 anchorPoint = ProjectPointToLine(midpoint, installLineBasePoint, axisDir); + + return new AssemblyInstallationReferenceResult + { + OpticalAxisBasePoint = opticalAxisBasePoint, + OpticalAxisDirection = axisDir, + PickPoint = firstPickPoint, + SecondaryPickPoint = secondPickPoint, PlaneNormal = planeNormal, PlaneSpanDirection = planeSpanDirection, OffsetDistance = offsetDistance, diff --git a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs index e20e521..f1d5faa 100644 --- a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs +++ b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs @@ -17,6 +17,23 @@ namespace NavisworksTransport.Utils.CoordinateSystem Vector3 nextPoint, Vector3 canonicalUp, out RailLocalFrame frame) + { + return TryCreateLocalFrame( + previousPoint, + currentPoint, + nextPoint, + canonicalUp, + null, + out frame); + } + + public static bool TryCreateLocalFrame( + Vector3 previousPoint, + Vector3 currentPoint, + Vector3 nextPoint, + Vector3 canonicalUp, + Vector3? preferredNormal, + out RailLocalFrame frame) { frame = null; @@ -25,6 +42,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem currentPoint, nextPoint, canonicalUp, + preferredNormal, out var forward, out var lateral, out var up)) @@ -44,6 +62,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem out Vector3 forward, out Vector3 lateral, out Vector3 up) + { + return TryCreateBasis( + previousPoint, + currentPoint, + nextPoint, + canonicalUp, + null, + out forward, + out lateral, + out up); + } + + public static bool TryCreateBasis( + Vector3 previousPoint, + Vector3 currentPoint, + Vector3 nextPoint, + Vector3 canonicalUp, + Vector3? preferredNormal, + out Vector3 forward, + out Vector3 lateral, + out Vector3 up) { forward = default; lateral = default; @@ -61,7 +100,13 @@ namespace NavisworksTransport.Utils.CoordinateSystem } forward = Vector3.Normalize(tangent); - Vector3 projectedUp = canonicalUp - Vector3.Dot(canonicalUp, forward) * forward; + Vector3 referenceUp = canonicalUp; + if (preferredNormal.HasValue && preferredNormal.Value.LengthSquared() >= TangentEpsilon) + { + referenceUp = Vector3.Normalize(preferredNormal.Value); + } + + Vector3 projectedUp = referenceUp - Vector3.Dot(referenceUp, forward) * forward; if (projectedUp.LengthSquared() < TangentEpsilon) { projectedUp = Math.Abs(forward.Z) < 0.9f @@ -95,6 +140,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem nextPoint, canonicalUp, convention, + null, 0.0, out rotation); } @@ -107,6 +153,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem ModelAxisConvention convention, double localUpRotationDegrees, out Quaternion rotation) + { + return TryCreateQuaternion( + previousPoint, + currentPoint, + nextPoint, + canonicalUp, + convention, + null, + localUpRotationDegrees, + out rotation); + } + + public static bool TryCreateQuaternion( + Vector3 previousPoint, + Vector3 currentPoint, + Vector3 nextPoint, + Vector3 canonicalUp, + ModelAxisConvention convention, + Vector3? preferredNormal, + double localUpRotationDegrees, + out Quaternion rotation) { rotation = Quaternion.Identity; @@ -115,7 +182,15 @@ namespace NavisworksTransport.Utils.CoordinateSystem throw new ArgumentNullException(nameof(convention)); } - if (!TryCreateBasis(previousPoint, currentPoint, nextPoint, canonicalUp, out var forward, out _, out var up)) + if (!TryCreateBasis( + previousPoint, + currentPoint, + nextPoint, + canonicalUp, + preferredNormal, + out var forward, + out _, + out var up)) { return false; } diff --git a/src/Utils/CoordinateSystem/ObjectSpaceOrientationHelper.cs b/src/Utils/CoordinateSystem/ObjectSpaceOrientationHelper.cs new file mode 100644 index 0000000..65d9e2d --- /dev/null +++ b/src/Utils/CoordinateSystem/ObjectSpaceOrientationHelper.cs @@ -0,0 +1,92 @@ +using System; +using System.Numerics; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + public static class ObjectSpaceOrientationHelper + { + public static (Vector3D right, Vector3D up) CalculateAxes( + Point3D startPoint, + Point3D endPoint, + Vector3D upReference, + Vector3D horizontalDirection = null) + { + var segmentDirection = new Vector3( + (float)(endPoint.X - startPoint.X), + (float)(endPoint.Y - startPoint.Y), + (float)(endPoint.Z - startPoint.Z)); + + var (right, up) = CalculateAxes( + segmentDirection, + ToNumerics(upReference), + horizontalDirection == null ? (Vector3?)null : ToNumerics(horizontalDirection)); + + return (ToNavisworks(right), ToNavisworks(up)); + } + + public static (Vector3 right, Vector3 up) CalculateAxes( + Vector3 segmentDirection, + Vector3 upReference, + Vector3? horizontalDirection = null) + { + double segmentLength = Math.Sqrt( + segmentDirection.X * segmentDirection.X + + segmentDirection.Y * segmentDirection.Y + + segmentDirection.Z * segmentDirection.Z); + + if (segmentLength < 1e-9) + { + throw new InvalidOperationException("路径段长度过小,无法计算通行空间方向。"); + } + + var normalizedDirection = Vector3.Normalize(segmentDirection); + var normalizedUpReference = Normalize(upReference); + var right = Cross(normalizedDirection, normalizedUpReference); + + double parallelToUp = Math.Abs(Vector3.Dot(normalizedDirection, normalizedUpReference)); + if (parallelToUp > 0.9) + { + if (horizontalDirection.HasValue) + { + right = Cross(horizontalDirection.Value, normalizedUpReference); + } + else + { + right = Math.Abs(normalizedUpReference.X) < 0.9f + ? Normalize(Cross(normalizedUpReference, new Vector3(1f, 0f, 0f))) + : Normalize(Cross(normalizedUpReference, new Vector3(0f, 1f, 0f))); + } + } + + right = Normalize(right); + var up = Normalize(Cross(normalizedDirection, right)); + return (right, up); + } + + private static Vector3 Cross(Vector3 a, Vector3 b) + { + return Vector3.Cross(a, b); + } + + private static Vector3 Normalize(Vector3 vector) + { + if (vector.LengthSquared() < 1e-9f) + { + return Vector3.Zero; + } + + return Vector3.Normalize(vector); + } + + private static Vector3 ToNumerics(Vector3D vector) + { + return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z); + } + + private static Vector3D ToNavisworks(Vector3 vector) + { + return new Vector3D(vector.X, vector.Y, vector.Z); + } + } +} diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs index 2b74480..8fd306a 100644 --- a/src/Utils/RailPathPoseHelper.cs +++ b/src/Utils/RailPathPoseHelper.cs @@ -135,7 +135,7 @@ namespace NavisworksTransport.Utils var adapterForRail = CoordinateSystemManager.Instance.CreateHostAdapter(); var canonicalReferencePointForRail = adapterForRail.ToCanonicalPoint(referencePoint); - if (!TryCreateCanonicalLocalFrame(previousPoint, referencePoint, nextPoint, out RailLocalFrame frame)) + if (!TryCreateCanonicalLocalFrame(route, previousPoint, referencePoint, nextPoint, out RailLocalFrame frame)) { var canonicalCenterPointForRailFallback = new Point3D( canonicalReferencePointForRail.X, @@ -165,6 +165,22 @@ namespace NavisworksTransport.Utils out Matrix3 linearTransform) { return TryCreateRailLinearTransform( + null, + previousPoint, + currentPoint, + nextPoint, + out linearTransform); + } + + public static bool TryCreateRailLinearTransform( + PathRoute route, + Point3D previousPoint, + Point3D currentPoint, + Point3D nextPoint, + out Matrix3 linearTransform) + { + return TryCreateRailLinearTransform( + route, previousPoint, currentPoint, nextPoint, @@ -177,6 +193,7 @@ namespace NavisworksTransport.Utils /// 通过模型局部轴约定显式指定本地哪个轴代表 forward/up。 /// public static bool TryCreateRailLinearTransform( + PathRoute route, Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, @@ -184,6 +201,7 @@ namespace NavisworksTransport.Utils out Matrix3 linearTransform) { return TryCreateRailLinearTransform( + route, previousPoint, currentPoint, nextPoint, @@ -193,6 +211,7 @@ namespace NavisworksTransport.Utils } public static bool TryCreateRailLinearTransform( + PathRoute route, Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, @@ -217,6 +236,7 @@ namespace NavisworksTransport.Utils canonicalCurrentPoint, canonicalNextPoint, HostCoordinateAdapter.CanonicalUpVector3, + ResolvePreferredNormal(route), out RailLocalFrame frame)) { return false; @@ -231,6 +251,7 @@ namespace NavisworksTransport.Utils canonicalNextPoint, HostCoordinateAdapter.CanonicalUpVector3, axisConvention, + ResolvePreferredNormal(route), localUpRotationDegrees, out var correctedRotation) ? correctedRotation @@ -270,6 +291,22 @@ namespace NavisworksTransport.Utils out Rotation3D rotation) { return TryCreateRailRotation( + null, + previousPoint, + currentPoint, + nextPoint, + out rotation); + } + + public static bool TryCreateRailRotation( + PathRoute route, + Point3D previousPoint, + Point3D currentPoint, + Point3D nextPoint, + out Rotation3D rotation) + { + return TryCreateRailRotation( + route, previousPoint, currentPoint, nextPoint, @@ -285,6 +322,24 @@ namespace NavisworksTransport.Utils out Rotation3D rotation) { return TryCreateRailRotation( + null, + previousPoint, + currentPoint, + nextPoint, + axisConvention, + out rotation); + } + + public static bool TryCreateRailRotation( + PathRoute route, + Point3D previousPoint, + Point3D currentPoint, + Point3D nextPoint, + ModelAxisConvention axisConvention, + out Rotation3D rotation) + { + return TryCreateRailRotation( + route, previousPoint, currentPoint, nextPoint, @@ -294,6 +349,7 @@ namespace NavisworksTransport.Utils } public static bool TryCreateRailRotation( + PathRoute route, Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, @@ -305,6 +361,7 @@ namespace NavisworksTransport.Utils LogRotationConstructorConventionOnce(); if (!TryCreateRailLinearTransform( + route, previousPoint, currentPoint, nextPoint, @@ -446,7 +503,7 @@ namespace NavisworksTransport.Utils private static Vector3D ResolveRailNormal(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint) { - if (TryCreateCanonicalLocalFrame(previousPoint, currentPoint, nextPoint, out RailLocalFrame frame)) + if (TryCreateCanonicalLocalFrame(null, previousPoint, currentPoint, nextPoint, out RailLocalFrame frame)) { return ToNavVector(frame.Normal); } @@ -479,6 +536,7 @@ namespace NavisworksTransport.Utils } private static bool TryCreateCanonicalLocalFrame( + PathRoute route, Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, @@ -490,9 +548,37 @@ namespace NavisworksTransport.Utils ToNumerics(adapter.ToCanonicalPoint(currentPoint)), ToNumerics(adapter.ToCanonicalPoint(nextPoint)), HostCoordinateAdapter.CanonicalUpVector3, + ResolvePreferredNormal(route), out frame); } + private static Vector3? ResolvePreferredNormal(PathRoute route) + { + if (route?.RailPreferredNormal == null) + { + return null; + } + + var preferredNormal = new Vector3( + (float)route.RailPreferredNormal.X, + (float)route.RailPreferredNormal.Y, + (float)route.RailPreferredNormal.Z); + + if (preferredNormal.LengthSquared() < 1e-9f) + { + return null; + } + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + Vector3 canonicalPreferredNormal = adapter.ToCanonicalVector3(preferredNormal); + if (canonicalPreferredNormal.LengthSquared() < 1e-9f) + { + return null; + } + + return Vector3.Normalize(canonicalPreferredNormal); + } + private static Vector3D Normalize(Vector3D vector) { double lengthSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z; From d52b1aef082912d637175f1bc18b2b7bc0000fe0 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 23 Mar 2026 21:00:02 +0800 Subject: [PATCH 21/87] Checkpoint animation rotation refactor state --- AGENTS.md | 13 ++ TransportPlugin.csproj | 3 + .../CanonicalPlanarPoseBuilderTests.cs | 74 ++++++++ .../CanonicalRailPoseBuilderTests.cs | 28 +++ .../HostCoordinateAdapterTests.cs | 15 ++ .../ObjectSpaceOrientationHelperTests.cs | 16 ++ deploy-plugin.bat | 27 ++- ...oordinate-system-canonical-space-design.md | 54 +++++- doc/requirement/todo_features.md | 7 +- src/Core/Animation/PathAnimationManager.cs | 159 +++++++++++------- src/Core/PathPointRenderPlugin.cs | 19 ++- src/Core/VirtualObjectManager.cs | 4 +- .../ViewModels/AnimationControlViewModel.cs | 156 ++++++++--------- src/UI/WPF/Views/EditRotationWindow.xaml | 60 ++++++- src/UI/WPF/Views/EditRotationWindow.xaml.cs | 121 +++++++++++-- .../CanonicalPlanarPoseBuilder.cs | 52 +++++- .../CanonicalRailPoseBuilder.cs | 108 ++++++------ .../CoordinateSystem/HostCoordinateAdapter.cs | 26 +++ .../CoordinateSystem/LocalAxisPoseBuilder.cs | 76 +++++++++ .../LocalEulerRotationCorrection.cs | 111 ++++++++++++ .../ObjectSpaceOrientationHelper.cs | 21 +++ .../RotatedObjectExtentHelper.cs | 58 +++++++ src/Utils/RailPathPoseHelper.cs | 45 ++++- 23 files changed, 1015 insertions(+), 238 deletions(-) create mode 100644 src/Utils/CoordinateSystem/LocalAxisPoseBuilder.cs create mode 100644 src/Utils/CoordinateSystem/LocalEulerRotationCorrection.cs create mode 100644 src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs diff --git a/AGENTS.md b/AGENTS.md index d5d4b7e..9e55c6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -308,6 +308,19 @@ double distance = GeometryHelper.Distance(pointA, pointB); - 需要坐标转换?→ 使用 `CoordinateConverter` - 需要路径相关工具?→ 使用 `PathHelper` +#### 5. 临时定位代码必须回收 + +为定位问题临时加入的兜底调用、强制刷新、额外同步、绕过分支等代码,如果后续确认**不是真正根因**,在根因修复后必须恢复或删除,不能把这类临时补丁长期保留在正式代码中。 + +```csharp +// ❌ 错误:为掩盖问题长期保留的强制调用 +DoNormalUpdate(); +ForceRefreshAgain(); // 只是为了“看起来好了” + +// ✅ 正确:先定位根因,再移除临时补丁 +DoNormalUpdate(); +``` + ### 单位系统 - 极其重要 **所有网格地图和路径规划计算必须使用模型单位,严禁混用米制单位。** diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index af09c90..0b5454e 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -334,11 +334,14 @@ + + + diff --git a/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs index b0b6b0a..89b058c 100644 --- a/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs +++ b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NavisworksTransport.Utils.CoordinateSystem; +using System; using System.Numerics; namespace NavisworksTransport.UnitTests.CoordinateSystem @@ -55,6 +56,52 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.AreEqual(1.0, linear.M33, 1e-6); } + [TestMethod] + public void ZeroLocalEulerCorrection_ShouldMatchCurrentPlanarBaseline() + { + var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp); + + Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( + new Vector3(5, 0, 0), + Vector3.UnitZ, + convention, + out Quaternion baselineRotation)); + + Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( + new Vector3(5, 0, 0), + Vector3.UnitZ, + convention, + LocalEulerRotationCorrection.Zero, + out Quaternion correctedRotation)); + + AssertQuaternionEquivalent(baselineRotation, correctedRotation); + } + + [TestMethod] + public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithPlanarForward() + { + var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp); + var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0); + var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp); + Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction); + + Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( + new Vector3(5, 0, 0), + Vector3.UnitZ, + convention, + correctionQuaternion, + out Quaternion rotation)); + + LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp); + Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation); + + Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward); + Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp); + + AssertVector(mappedForward, 1, 0, 0); + AssertVector(mappedUp, 0, 0, 1); + } + private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z) { switch (column) @@ -79,5 +126,32 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem break; } } + + private static void AssertVector(Vector3 actual, double x, double y, double z) + { + Assert.AreEqual(x, actual.X, 1e-6); + Assert.AreEqual(y, actual.Y, 1e-6); + Assert.AreEqual(z, actual.Z, 1e-6); + } + + private static void AssertQuaternionEquivalent(Quaternion expected, Quaternion actual) + { + double dot = Math.Abs( + expected.X * actual.X + + expected.Y * actual.Y + + expected.Z * actual.Z + + expected.W * actual.W); + + Assert.AreEqual(1.0, dot, 1e-6); + } + + private static Vector3 TransformLocalVector(Matrix4x4 linear, Vector3 localVector) + { + Vector3 world = new Vector3( + linear.M11 * localVector.X + linear.M12 * localVector.Y + linear.M13 * localVector.Z, + linear.M21 * localVector.X + linear.M22 * localVector.Y + linear.M23 * localVector.Z, + linear.M31 * localVector.X + linear.M32 * localVector.Y + linear.M33 * localVector.Z); + return Vector3.Normalize(world); + } } } diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs index deef6f7..74b6405 100644 --- a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs +++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs @@ -137,6 +137,34 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem AssertVector(mappedUp, 0, 0, 1); } + [TestMethod] + public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithRailTangent() + { + var convention = ModelAxisConvention.CreateRailAssetConvention(); + var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0); + var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp); + Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction); + + Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion( + new Vector3(0, 0, 0), + new Vector3(1, 0, 0), + new Vector3(2, 0, 0), + Vector3.UnitZ, + convention, + null, + correctionQuaternion, + out Quaternion rotation)); + + LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp); + Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation); + + Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward); + Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp); + + AssertVector(mappedForward, 1, 0, 0); + AssertVector(mappedUp, 0, 0, 1); + } + [TestMethod] public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame() { diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs index 3d54d44..9f77b59 100644 --- a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs +++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NavisworksTransport.Utils.CoordinateSystem; using System.Numerics; +using System; using Autodesk.Navisworks.Api; namespace NavisworksTransport.UnitTests.CoordinateSystem @@ -143,6 +144,20 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.AreEqual(0.0000, hostLinear.M33, 1e-3); } + [TestMethod] + public void YUp_HostRotationCorrection_ShouldMapHostYAxisToCanonicalUp() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0); + + Quaternion canonicalCorrection = adapter.CreateCanonicalRotationCorrection(correction); + Vector3 rotatedForward = Vector3.Transform(Vector3.UnitX, canonicalCorrection); + + Assert.AreEqual(0.0, rotatedForward.X, 1e-6); + Assert.AreEqual(1.0, rotatedForward.Y, 1e-6); + Assert.AreEqual(0.0, rotatedForward.Z, 1e-6); + } + private static void AssertPoint(Vector3 point, double x, double y, double z) { Assert.AreEqual(x, point.X, 1e-9); diff --git a/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs index a15baf4..f60f624 100644 --- a/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs +++ b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs @@ -36,5 +36,21 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.AreEqual(0.0, up.Y, 1e-6); Assert.AreEqual(0.0, up.Z, 1e-6); } + + [TestMethod] + public void TryCalculateAxes_ShouldReturnFalse_ForZeroLengthSegment() + { + var segmentDirection = Vector3.Zero; + var upReference = new Vector3(0f, 1f, 0f); + + bool ok = ObjectSpaceOrientationHelper.TryCalculateAxes( + segmentDirection, + upReference, + out var axes); + + Assert.IsFalse(ok); + Assert.AreEqual(Vector3.Zero, axes.right); + Assert.AreEqual(Vector3.Zero, axes.up); + } } } diff --git a/deploy-plugin.bat b/deploy-plugin.bat index 9c39165..0c5a3cd 100644 --- a/deploy-plugin.bat +++ b/deploy-plugin.bat @@ -7,6 +7,21 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^ "$ErrorActionPreference = 'Stop';" ^ "$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^ "$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^ + "$deployDllNames = @(" ^ + " 'TransportPlugin.dll'," ^ + " 'geometry4Sharp.dll'," ^ + " 'Newtonsoft.Json.dll'," ^ + " 'Roy-T.AStar.dll'," ^ + " 'SQLite.Interop.dll'," ^ + " 'System.Data.SQLite.dll'," ^ + " 'Tomlyn.dll'" ^ + ");" ^ + "$stalePluginFiles = @(" ^ + " 'Autodesk.Navisworks.Api.dll'," ^ + " 'NavisworksTransport.UnitTests.dll'," ^ + " 'Microsoft.VisualStudio.TestPlatform.TestFramework.dll'," ^ + " 'Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll'" ^ + ");" ^ "function Test-FileUnlocked([string]$path) {" ^ " if (-not (Test-Path $path)) { return $true }" ^ " try {" ^ @@ -39,14 +54,22 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^ "foreach ($keyFile in $keyTargetFiles) { Wait-FileUnlocked $keyFile $deadline }" ^ "" ^ "New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^ - "Copy-Item (Join-Path $buildDir '*.dll') $targetDir -Force;" ^ + "foreach ($staleFileName in $stalePluginFiles) {" ^ + " $stalePath = Join-Path $targetDir $staleFileName;" ^ + " if (Test-Path $stalePath) { Remove-Item $stalePath -Force }" ^ + "}" ^ + "foreach ($dllName in $deployDllNames) {" ^ + " $sourceDll = Join-Path $buildDir $dllName;" ^ + " if (-not (Test-Path $sourceDll)) { throw ('Deployment source file missing: ' + $sourceDll) }" ^ + " Copy-Item $sourceDll $targetDir -Force;" ^ + "}" ^ "if (Test-Path (Join-Path $buildDir 'resources')) {" ^ " New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^ " Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^ "}" ^ "" ^ "$sourceFiles = @();" ^ - "$sourceFiles += Get-ChildItem $buildDir -File -Filter '*.dll';" ^ + "foreach ($dllName in $deployDllNames) { $sourceFiles += Get-Item (Join-Path $buildDir $dllName) }" ^ "if (Test-Path (Join-Path $buildDir 'resources')) {" ^ " $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^ "}" ^ diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md index 7aa02af..9c06e51 100644 --- a/doc/design/2026/coordinate-system-canonical-space-design.md +++ b/doc/design/2026/coordinate-system-canonical-space-design.md @@ -41,6 +41,7 @@ - Navisworks 世界坐标统一视为**外部坐标** - 程序内部统一使用一套**规范内部坐标** - 工程语义使用独立的**业务基准坐标** +- 只有插件自带资源才允许引入**资产坐标系** - 坐标转换只发生在少数边界入口 - 业务逻辑禁止直接依赖宿主坐标语义 @@ -95,6 +96,7 @@ flowchart LR 特点: - 是程序的输入/输出坐标 +- UI 文本框、对话框、日志、鼠标拾取结果,统一按宿主坐标系解释和显示 - 可能来自 `Y-up` 项目,也可能来自 `Z-up` 项目 - 不应直接当成内部计算坐标 @@ -118,6 +120,29 @@ flowchart LR - 项目 up 方向的业务解释 - 终端安装参考面 - 轨道参考面 + +### 3.2.1 术语约束 + +后续文档、代码注释和日志中,统一使用以下说法: + +- **宿主坐标系(Host Space)** + - 指 Navisworks 文档坐标系 + - `Y-up` / `Z-up` 的判断只属于这一层 +- **内部坐标系(Canonical Space)** + - 指程序内部统一使用的 `Z-up` 坐标系 + - 仅供内部计算使用,禁止直接暴露给 UI +- **资产坐标系(Asset Space)** + - 只用于插件自带资源 + - 当前明确只有两类:`虚拟物体` 与 `单位圆柱体(参考杆资源)` + - 用于描述这些资源文件自身的 `Forward/Up/Side` 轴约定 + +禁止继续使用含糊的“本地坐标系”说法,因为它容易混淆: + +- 宿主坐标系 +- 内部坐标系 +- 资产坐标系 + +后续凡是 UI 输入/输出,必须明确是**宿主坐标系**语义;凡是姿态或几何内部计算,必须明确是**内部坐标系**语义;凡是虚拟物体/单位圆柱体资源朝向,必须明确是**资产坐标系**语义。 - 业务锚点 这层不能偷用世界原点,也不能偷用宿主世界轴。 @@ -154,24 +179,35 @@ flowchart LR 都必须基于这组定义来解释,不能继续偷用“世界 Z 就是 up”的旧假设。 -### 3.4 模型局部轴约定是独立层 +### 3.4 资产坐标系是独立层 -除了宿主坐标系和内部统一坐标系,还必须区分**模型局部轴约定**。 +除了宿主坐标系和内部统一坐标系,还必须区分**资产坐标系**。 + +注意,这一层**不是所有模型都有**。当前项目里只有插件自带资源需要它: + +- 虚拟物体资源 +- 单位圆柱体参考杆资源 这是另一层独立定义,至少包含: -- `LocalForwardAxis` -- `LocalUpAxis` +- `AssetForwardAxis` +- `AssetUpAxis` 例如: -- 虚拟物体资源通常按程序约定构建,可能是 `Local X = Forward, Local Z = Up` -- 真实模型如果来自 `Y-up` 项目,则很可能是 `Local X = Forward, Local Y = Up` +- 虚拟物体资源通常按程序约定构建,可能是 `Asset X = Forward, Asset Z = Up` +- 单位圆柱体参考杆资源当前约定为 `Asset X = 杆轴方向, Asset Z = 截面 Up` + +而对于客户真实模型: + +- 不单独引入“资产坐标系”概念 +- UI 和数据输入输出仍只认宿主坐标系 +- 内部算法如需统一姿态,先进入 Canonical Space,再叠加业务参考信息 这意味着: - 即使宿主坐标系转换已经正确 -- 如果模型局部轴约定没有显式处理 +- 如果资产坐标系语义没有显式处理 - 动画和姿态仍然会出现“路径对了、通行空间对了、模型自己站歪了”的现象 因此,后续姿态系统必须显式区分: @@ -179,7 +215,7 @@ flowchart LR 1. 宿主坐标系定义 2. 内部统一坐标系定义 3. 业务基准定义 -4. 模型局部轴约定 +4. 资产坐标系约定(仅限虚拟物体和单位圆柱体等插件自带资源) ### 3.5 Rail 局部坐标系与动画跟踪点 @@ -599,6 +635,8 @@ WPF 插件的最终部署,必须依赖完整主项目构建产物,而不应 - **Navisworks 世界坐标统一视为外部坐标** - **程序内部统一使用 Canonical Space(Z-up)** - **业务基准点单独建模** +- **UI 输入输出统一使用宿主坐标系** +- **资产坐标系只属于插件自带资源,不得泛化成“所有模型都有本地轴”** - **坐标转换只发生在边界层** 这是一条更接近业界三维软件成熟做法的路线。 diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md index 17d59b3..b886dc7 100644 --- a/doc/requirement/todo_features.md +++ b/doc/requirement/todo_features.md @@ -2,9 +2,14 @@ ## 功能点 +### [2026/3/20] + +1. [x] (优化)实现Yup模型兼容 +2. [x] (功能)运动物体在起点支持三个方向的旋转 + ### [2026/3/18] -1. [x](功能)增加轨上路径(角度可倾斜) +1. [x] (功能)增加终端仿真需要的轨上和轨下路径(角度可倾斜) ### [2026/3/11] diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index e6ef4fa..3650993 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -88,12 +88,19 @@ namespace NavisworksTransport.Core.Animation } } + internal enum AnimatedObjectMode + { + None, + RealObject, + VirtualObject + } + public class PathAnimationManager { private static PathAnimationManager _instance; private static readonly Dictionary _animationHashToRecordId = new Dictionary(); // 动画配置哈希到检测记录ID的映射 private ModelItem _animatedObject; - private bool _isVirtualObject = false; // 是否使用虚拟物体 + private AnimatedObjectMode _animatedObjectMode = AnimatedObjectMode.None; private double _virtualObjectLength = 0; // 虚拟物体长度(模型单位) private double _virtualObjectWidth = 0; // 虚拟物体宽度(模型单位) private double _virtualObjectHeight = 0; // 虚拟物体高度(模型单位) @@ -105,7 +112,7 @@ namespace NavisworksTransport.Core.Animation private bool _manualCollisionOverrideEnabled = false; // === 角度修正 === - private double _objectRotationCorrection = 0.0; // 物体角度修正值(度,顺时针) + private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正 // === 碰撞排除列表缓存管理(从LogisticsAnimationManager迁移)=== private ModelItem _currentCachedAnimationObject; @@ -182,6 +189,12 @@ namespace NavisworksTransport.Core.Animation private bool _savedObjectHasCustomRotation = false; private bool _hasSavedObjectState = false; + private bool IsVirtualObjectMode => _animatedObjectMode == AnimatedObjectMode.VirtualObject; + private bool IsRealObjectMode => _animatedObjectMode == AnimatedObjectMode.RealObject; + + private ModelItem CurrentControlledObject => + IsVirtualObjectMode ? VirtualObjectManager.Instance.CurrentVirtualObject : _animatedObject; + /// /// 当前正在进行动画的对象 /// @@ -397,7 +410,7 @@ namespace NavisworksTransport.Core.Animation // 🔥 支持虚拟物体的恢复 ModelItem objectToRestore = null; - bool isVirtual = _isVirtualObject; + bool isVirtual = IsVirtualObjectMode; if (isVirtual) { @@ -458,7 +471,19 @@ namespace NavisworksTransport.Core.Animation public void SetAnimatedObject(ModelItem animatedObject) { _animatedObject = animatedObject; - _isVirtualObject = (animatedObject == null); + _animatedObjectMode = animatedObject == null + ? AnimatedObjectMode.None + : AnimatedObjectMode.RealObject; + + if (animatedObject != null) + { + _originalTransform = animatedObject.Transform; + _originalCenter = animatedObject.BoundingBox().Center; + _trackedPosition = GetTrackedObjectPosition(animatedObject); + _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform); + _trackedRotation = _originalTransform.Factor().Rotation; + _hasTrackedRotation = true; + } } /// @@ -467,7 +492,9 @@ namespace NavisworksTransport.Core.Animation /// public void SetVirtualObjectParameters(bool isVirtualObject, double length, double width, double height) { - _isVirtualObject = isVirtualObject; + _animatedObjectMode = isVirtualObject + ? AnimatedObjectMode.VirtualObject + : (_animatedObject != null ? AnimatedObjectMode.RealObject : AnimatedObjectMode.None); _virtualObjectLength = length; // 模型单位 _virtualObjectWidth = width; // 模型单位 _virtualObjectHeight = height; // 模型单位 @@ -592,8 +619,8 @@ namespace NavisworksTransport.Core.Animation { // 🔥 重要:先恢复物体到原始状态(CAD位置和原始朝向),确保每次计算都从相同的状态开始 // 这样可以避免之前旋转导致的包围盒尺寸计算不准确的问题 - // 注意:也要处理虚拟物体(_isVirtualObject为true时_animatedObject可能为null) - if (_animatedObject != null || _isVirtualObject) + // 注意:也要处理虚拟物体(当前控制对象可能不在 _animatedObject 中) + if (CurrentControlledObject != null || IsVirtualObjectMode) { RestoreObjectToCADPosition(); LogManager.Info($"[移动到起点] 已恢复物体到原始状态, _currentYaw={_currentYaw * 180 / Math.PI:F2}°"); @@ -603,6 +630,12 @@ namespace NavisworksTransport.Core.Animation if (animatedObject != null) { _animatedObject = animatedObject; + bool isVirtualObject = + VirtualObjectManager.Instance.IsVirtualObjectActive && + ReferenceEquals(animatedObject, VirtualObjectManager.Instance.CurrentVirtualObject); + _animatedObjectMode = isVirtualObject + ? AnimatedObjectMode.VirtualObject + : AnimatedObjectMode.RealObject; _originalTransform = animatedObject.Transform; _originalCenter = animatedObject.BoundingBox().Center; _trackedPosition = GetTrackedObjectPosition(animatedObject); @@ -611,6 +644,9 @@ namespace NavisworksTransport.Core.Animation _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform); _trackedRotation = _originalTransform.Factor().Rotation; _hasTrackedRotation = true; + + LogManager.Info( + $"[移动到起点] 更新动画对象内部状态: 对象={animatedObject.DisplayName}, 虚拟物体={IsVirtualObjectMode}"); } if (pathPoints != null) @@ -629,7 +665,7 @@ namespace NavisworksTransport.Core.Animation double pathDirectionYaw = Math.Atan2(_pathPoints[1].Y - _pathPoints[0].Y, _pathPoints[1].X - _pathPoints[0].X); double yaw; - LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection:F1}度"); + LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection}"); // 根据路径类型调整朝向 if (_route.PathType == PathType.Hoisting) @@ -646,12 +682,12 @@ namespace NavisworksTransport.Core.Animation LogManager.Debug($"[移动到起点] 地面/空轨路径使用路径方向: {yaw * 180 / Math.PI:F2}度"); } - // 应用角度修正值(顺时针,转换为弧度) - if (_objectRotationCorrection != 0.0) + // 旧 yaw 链仅保留给未进入完整三维姿态的遗留路径。 + if (!_objectRotationCorrection.IsZero) { - double correctionRad = _objectRotationCorrection * Math.PI / 180.0; + double correctionRad = _objectRotationCorrection.ZDegrees * Math.PI / 180.0; yaw += correctionRad; - LogManager.Debug($"[移动到起点] 应用角度修正: {_objectRotationCorrection:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度"); + LogManager.Debug($"[移动到起点] 应用遗留Z轴修正: {_objectRotationCorrection.ZDegrees:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度"); } // 根据路径类型调整起点位置 @@ -1147,9 +1183,9 @@ namespace NavisworksTransport.Core.Animation LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}"); double actualYawRadians = yawRadians; - if (!frame.HasCustomRotation && _objectRotationCorrection != 0.0) + if (!frame.HasCustomRotation && !_objectRotationCorrection.IsZero) { - actualYawRadians += _objectRotationCorrection * Math.PI / 180.0; + actualYawRadians += _objectRotationCorrection.ZDegrees * Math.PI / 180.0; } var collisionResult = new CollisionResult @@ -1548,7 +1584,7 @@ namespace NavisworksTransport.Core.Animation { var firstFrame = _animationFrames[0]; - LogManager.Debug($"[动画开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualObject={_isVirtualObject}"); + LogManager.Debug($"[动画开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, 模式={_animatedObjectMode}"); // 🔥 确保物体在起点位置和朝向 // 注意:MoveObjectToPathStart 已在 StartAnimation 中调用,这里只是日志记录 @@ -1829,7 +1865,7 @@ namespace NavisworksTransport.Core.Animation _detectionTolerance, _currentRouteId, _animatedObject, - _isVirtualObject, + IsVirtualObjectMode, _animationFrameRate, _animationDuration, _virtualObjectLength, @@ -2411,8 +2447,8 @@ namespace NavisworksTransport.Core.Animation { try { - bool isRailRealObject = _route?.PathType == PathType.Rail && !_isVirtualObject && _animatedObject != null; - if (_isVirtualObject) + bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && _animatedObject != null; + if (IsVirtualObjectMode) { VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation); } @@ -2475,7 +2511,7 @@ namespace NavisworksTransport.Core.Animation } else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) { - LogHostActualPoseAxes(_isVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : _animatedObject, "[平面姿态应用后宿主姿态]", false); + LogHostActualPoseAxes(CurrentControlledObject, "[平面姿态应用后宿主姿态]", false); } } catch (Exception ex) @@ -2688,7 +2724,7 @@ namespace NavisworksTransport.Core.Animation return false; } - if (_isVirtualObject) + if (IsVirtualObjectMode) { return VirtualObjectManager.Instance.IsVirtualObjectActive && ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, obj); @@ -2723,7 +2759,7 @@ namespace NavisworksTransport.Core.Animation try { var originalAnimatedObject = _animatedObject; - if (!_isVirtualObject) + if (!IsVirtualObjectMode) { _animatedObject = obj; } @@ -3243,7 +3279,7 @@ namespace NavisworksTransport.Core.Animation /// private double GetAnimatedObjectHeight() { - if (_isVirtualObject) + if (IsVirtualObjectMode) { return _virtualObjectHeight; } @@ -3334,7 +3370,7 @@ namespace NavisworksTransport.Core.Animation _pathName = pathName; _currentRouteId = routeId; - _isVirtualObject = isVirtualObject; // 设置是否使用虚拟物体 + _animatedObjectMode = isVirtualObject ? AnimatedObjectMode.VirtualObject : AnimatedObjectMode.RealObject; _virtualObjectLength = virtualObjectLength; // 模型单位 _virtualObjectWidth = virtualObjectWidth; // 模型单位 _virtualObjectHeight = virtualObjectHeight; // 模型单位 @@ -3351,12 +3387,12 @@ namespace NavisworksTransport.Core.Animation // 保持当前的 _currentYaw(因为物体可能已经被 MoveObjectToPathStart 旋转) // 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值 - LogManager.Debug($"[CreateAnimation] 保持_currentYaw={_currentYaw * 180 / Math.PI:F2}度不变, _isVirtualObject={_isVirtualObject}"); + LogManager.Debug($"[CreateAnimation] 保持_currentYaw={_currentYaw * 180 / Math.PI:F2}度不变, 模式={_animatedObjectMode}"); } // 设置动画参数并预计算动画帧 // 注意:物体应该在调用CreateAnimation之前已经通过MoveObjectToPathStart旋转到起点 - LogManager.Debug($"[CreateAnimation开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualObject={_isVirtualObject}"); + LogManager.Debug($"[CreateAnimation开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, 模式={_animatedObjectMode}"); SetupAnimation(animatedObject, durationSeconds, _route); LogManager.Debug($"[CreateAnimation结束] _currentYaw之后={_currentYaw * 180 / Math.PI:F2}度"); // 设置动画状态为Ready(动画已生成,可以播放) @@ -3405,7 +3441,7 @@ namespace NavisworksTransport.Core.Animation private ModelAxisConvention GetCurrentRailModelAxisConvention() { - if (_isVirtualObject) + if (IsVirtualObjectMode) { return ModelAxisConvention.CreateVirtualObjectAssetConvention(); } @@ -3413,14 +3449,14 @@ namespace NavisworksTransport.Core.Animation var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType); LogManager.Info( - $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={_isVirtualObject}, " + + $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " + $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}"); return convention; } private ModelAxisConvention GetCurrentModelAxisConvention() { - if (_isVirtualObject) + if (IsVirtualObjectMode) { return ModelAxisConvention.CreateVirtualObjectAssetConvention(); } @@ -3487,6 +3523,7 @@ namespace NavisworksTransport.Core.Animation Vector3 canonicalPrevious = ToNumerics(adapter.ToCanonicalPoint(previousPoint)); Vector3 canonicalCurrent = ToNumerics(adapter.ToCanonicalPoint(currentPoint)); Vector3 canonicalNext = ToNumerics(adapter.ToCanonicalPoint(nextPoint)); + Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection); Vector3 forward = canonicalNext - canonicalPrevious; if (forward.LengthSquared() < 1e-6f) @@ -3494,12 +3531,11 @@ namespace NavisworksTransport.Core.Animation forward = canonicalNext - canonicalCurrent; } - forward = ApplyPlanarRotationCorrection(forward); - if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( forward, HostCoordinateAdapter.CanonicalUpVector3, convention, + correctionQuaternion, out var quaternion)) { return false; @@ -3516,12 +3552,12 @@ namespace NavisworksTransport.Core.Animation var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); var convention = GetCurrentModelAxisConvention(); Vector3 canonicalForward = adapter.ToCanonicalVector3(new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z)); - canonicalForward = ApplyPlanarRotationCorrection(canonicalForward); - + Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection); if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( canonicalForward, HostCoordinateAdapter.CanonicalUpVector3, convention, + correctionQuaternion, out var quaternion)) { return false; @@ -3531,25 +3567,10 @@ namespace NavisworksTransport.Core.Animation return true; } - private Vector3 ApplyPlanarRotationCorrection(Vector3 canonicalForward) - { - if (Math.Abs(_objectRotationCorrection) < 1e-9) - { - return canonicalForward; - } - - float correctionRadians = (float)(_objectRotationCorrection * Math.PI / 180.0); - Quaternion correction = Quaternion.CreateFromAxisAngle( - Vector3.Normalize(HostCoordinateAdapter.CanonicalUpVector3), - correctionRadians); - return Vector3.Transform(canonicalForward, correction); - } - /// - /// 设置物体角度修正值(度,顺时针) + /// 设置物体绕宿主 X/Y/Z 轴的角度修正 /// - /// 角度修正值(度) - public void SetObjectRotationCorrection(double rotationCorrection) + public void SetObjectRotationCorrection(LocalEulerRotationCorrection rotationCorrection) { _objectRotationCorrection = rotationCorrection; @@ -3560,7 +3581,7 @@ namespace NavisworksTransport.Core.Animation { // 重新计算并应用朝向 MoveObjectToPathStart(); - LogManager.Info($"[角度修正] 物体角度已更新: {_objectRotationCorrection:F1}°"); + LogManager.Info($"[角度修正] 物体角度已更新: {_objectRotationCorrection}"); } catch (Exception ex) { @@ -3570,13 +3591,36 @@ namespace NavisworksTransport.Core.Animation } /// - /// 直接设置物体角度修正值(不触发物体旋转) + /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。 /// - /// 角度修正值(度) - public void SetObjectRotationCorrectionDirect(double rotationCorrection) + public void SetObjectRotationCorrection(double rotationCorrection) + { + SetObjectRotationCorrection(CreateLegacyUpAxisCorrection(rotationCorrection)); + } + + /// + /// 直接设置物体绕宿主 X/Y/Z 轴的角度修正(不触发物体旋转) + /// + public void SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection rotationCorrection) { _objectRotationCorrection = rotationCorrection; - LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection:F1}°(不触发旋转)"); + LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)"); + } + + /// + /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。 + /// + public void SetObjectRotationCorrectionDirect(double rotationCorrection) + { + SetObjectRotationCorrectionDirect(CreateLegacyUpAxisCorrection(rotationCorrection)); + } + + private LocalEulerRotationCorrection CreateLegacyUpAxisCorrection(double rotationCorrection) + { + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + return adapter.HostType == CoordinateSystemType.YUp + ? new LocalEulerRotationCorrection(0.0, rotationCorrection, 0.0) + : new LocalEulerRotationCorrection(0.0, 0.0, rotationCorrection); } #region 动画实现方法 @@ -3732,11 +3776,6 @@ namespace NavisworksTransport.Core.Animation } double actualYaw = frameData.YawRadians; - if (_objectRotationCorrection != 0.0) - { - double correctionRad = _objectRotationCorrection * Math.PI / 180.0; - actualYaw += correctionRad; - } if (frameData.HasCustomRotation) { @@ -3858,7 +3897,7 @@ namespace NavisworksTransport.Core.Animation sb.Append("|"); // 包含角度修正(确保角度修正改变时重新检测) - sb.Append($"RotationCorrection:{_objectRotationCorrection:F2}deg"); + sb.Append($"RotationCorrection:{_objectRotationCorrection}"); sb.Append("|"); // 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测) diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index 13983d2..a06c3d2 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -1886,6 +1886,19 @@ namespace NavisworksTransport { var startPoint = sortedPoints[i]; var endPoint = sortedPoints[i + 1]; + double segmentDx = endPoint.Position.X - startPoint.Position.X; + double segmentDy = endPoint.Position.Y - startPoint.Position.Y; + double segmentDz = endPoint.Position.Z - startPoint.Position.Z; + double segmentLength = Math.Sqrt( + segmentDx * segmentDx + + segmentDy * segmentDy + + segmentDz * segmentDz); + + if (segmentLength < 0.001) + { + LogManager.Debug($"[路径渲染] 跳过零长度路径段: 索引={i}, 路径={visualization.PathRoute?.Name}"); + continue; + } // 对于吊装路径,判断是否是垂直段和起吊/下降段 bool isVerticalSegment = false; @@ -1911,11 +1924,7 @@ namespace NavisworksTransport // 沿宿主 up 判断垂直段,避免把世界 Z 误当成向上轴 var hostUp = GetHostUpVector(); - double dx = endPoint.Position.X - startPoint.Position.X; - double dy = endPoint.Position.Y - startPoint.Position.Y; - double dz = endPoint.Position.Z - startPoint.Position.Z; - double upDelta = dx * hostUp.X + dy * hostUp.Y + dz * hostUp.Z; - double segmentLength = Math.Sqrt(dx * dx + dy * dy + dz * dz); + double upDelta = segmentDx * hostUp.X + segmentDy * hostUp.Y + segmentDz * hostUp.Z; double horizontalDist = Math.Sqrt(Math.Max(0.0, segmentLength * segmentLength - upDelta * upDelta)); // 如果沿宿主 up 的变化占主导(垂直段),或者是第一个/最后一个线段 bool isZDominant = Math.Abs(upDelta) > horizontalDist * 2.0; diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs index f8e7647..311d93b 100644 --- a/src/Core/VirtualObjectManager.cs +++ b/src/Core/VirtualObjectManager.cs @@ -213,7 +213,9 @@ namespace NavisworksTransport.Core try { LogVirtualObjectGeometry("[虚拟物体姿态] 应用前"); - ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation); + // 虚拟物体当前尺寸是通过永久变换中的缩放实现的。 + // 应用完整姿态时必须保留当前缩放,否则会破坏虚拟物体的资产姿态和几何语义。 + ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_virtualObjectModelItem, position, rotation); var actualBounds = _virtualObjectModelItem.BoundingBox(); Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0); LogManager.Info( diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 59c3aa1..ffd37f2 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Numerics; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; @@ -355,7 +356,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private double _safetyMarginInMeters; // 检测间隙(米),从路径编辑同步 // 角度修正相关字段 - private double _objectRotationCorrection; // 物体角度修正值(度,顺时针) + private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正 // 移动物体原始尺寸(米,在选择物体时保存) private double _objectOriginalLength; // 物体原始长度(X方向) @@ -708,8 +709,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels VirtualObjectLengthInMeters, VirtualObjectWidthInMeters, VirtualObjectHeightInMeters); // 重置角度修正值(虚拟物体重新选择时重置) - ObjectRotationCorrection = 0.0; - LogManager.Debug($"[切换虚拟物体] 已重置角度修正值为0度"); + ObjectRotationCorrection = LocalEulerRotationCorrection.Zero; + LogManager.Debug("[切换虚拟物体] 已重置角度修正值为0"); // 当切换到虚拟物体模式时,根据路径类型打开通行空间可视化 UpdatePassageSpaceVisualization(); @@ -792,9 +793,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels } /// - /// 物体角度修正值(度,顺时针) + /// 物体绕宿主 X/Y/Z 轴的角度修正。 /// - public double ObjectRotationCorrection + public LocalEulerRotationCorrection ObjectRotationCorrection { get => _objectRotationCorrection; set @@ -1261,8 +1262,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels SelectAnimatedObjectCommand = new RelayCommand(() => { // 重置角度修正值(每个物体的旋转独立) - ObjectRotationCorrection = 0.0; - LogManager.Debug($"[选择物体] 已重置角度修正值为0度"); + ObjectRotationCorrection = LocalEulerRotationCorrection.Zero; + LogManager.Debug("[选择物体] 已重置角度修正值为0"); ExecuteSelectAnimatedObject(); }); ClearAnimatedObjectCommand = new RelayCommand(ExecuteClearAnimatedObject, () => HasSelectedAnimatedObject); @@ -1714,19 +1715,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 3. 设置新物体(会触发UpdatePassageSpaceVisualization) SelectedAnimatedObject = newObject; + _pathAnimationManager?.SetAnimatedObject(newObject); LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}"); + // 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。 + if (UseVirtualObject) + { + UseVirtualObject = false; + LogManager.Debug("[选择物体] 已切换到实体物体模式"); + } + // 只有选择不同的物体时,才重置角度修正值 if (!isSameObject) { // 重置 ViewModel 中的角度修正值(会自动同步到 PathAnimationManager) - ObjectRotationCorrection = 0.0; - LogManager.Debug($"[选择物体] 已重置角度修正值为0度(新物体)"); + ObjectRotationCorrection = LocalEulerRotationCorrection.Zero; + LogManager.Debug("[选择物体] 已重置角度修正值为0(新物体)"); } else { LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)"); } + + // 显式同步一次到当前路径起点,避免依赖 setter 分支导致真实物体停留在 CAD 原位。 + MoveAnimatedObjectToPathStart(); } catch (Exception ex) { @@ -2151,8 +2163,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels } // 2. 重置角度修正值为0 - ObjectRotationCorrection = 0.0; - LogManager.Debug("[清除物体] 已重置角度修正值为0度"); + ObjectRotationCorrection = LocalEulerRotationCorrection.Zero; + LogManager.Debug("[清除物体] 已重置角度修正值为0"); // 3. 重置属性 SelectedAnimatedObject = null; @@ -2182,9 +2194,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels var dialog = new Views.EditRotationWindow(_objectRotationCorrection); if (dialog.ShowDialog() == true) { - ObjectRotationCorrection = dialog.RotationAngle; - LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection:F1}°"); - UpdateMainStatus($"物体角度修正: {_objectRotationCorrection:F1}°"); + ObjectRotationCorrection = dialog.RotationCorrection; + LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}"); + UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}"); // 应用角度修正到物体 UpdateObjectRotation(); @@ -2211,7 +2223,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels { // 将角度修正传递给动画管理器 _pathAnimationManager.SetObjectRotationCorrection(_objectRotationCorrection); - LogManager.Debug($"[角度修正] 已更新物体旋转: {_objectRotationCorrection:F1}°"); + LogManager.Debug($"[角度修正] 已更新物体旋转: {_objectRotationCorrection}"); } } catch (Exception ex) @@ -3191,7 +3203,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels IsVirtualObject = UseVirtualObject, AnimatedObjectName = UseVirtualObject ? "虚拟物体" : SelectedAnimatedObject?.DisplayName, DetectAllObjects = !IsManualCollisionTargetEnabled, - ObjectRotationCorrection = _objectRotationCorrection, + ObjectRotationCorrection = _objectRotationCorrection.ZDegrees, Description = $"{scenarioName}于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}", TestName = testName, TestTime = DateTime.Now, @@ -3356,7 +3368,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels UseVirtualObject ? VirtualObjectLengthInMeters * factor : (double?)null, // 米 → 模型单位 UseVirtualObject ? VirtualObjectWidthInMeters * factor : (double?)null, UseVirtualObject ? VirtualObjectHeightInMeters * factor : (double?)null, - _objectRotationCorrection, + _objectRotationCorrection.ZDegrees, IsManualCollisionTargetEnabled ); @@ -4302,54 +4314,56 @@ namespace NavisworksTransport.UI.WPF.ViewModels } /// - /// 计算旋转后的有效宽度和长度 + /// 计算本地三轴预旋转后的有效尺寸 /// 返回值为模型单位(与Navisworks API一致) /// - /// (有效宽度, 有效长度) - 模型单位 - private (double effectiveWidth, double effectiveLength) CalculateRotatedDimensions() + /// (沿路径, 垂直路径, 法线) - 模型单位 + private (double effectiveAlongPath, double effectiveAcrossPath, double effectiveNormalToPath) CalculateRotatedDimensions() { - double baseLengthMeters, baseWidthMeters; + double forwardSize; + double sideSize; + double upSize; + ModelAxisConvention axisConvention; if (UseVirtualObject) { - baseLengthMeters = VirtualObjectLengthInMeters; - baseWidthMeters = VirtualObjectWidthInMeters; + double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(); + forwardSize = VirtualObjectLengthInMeters * metersToUnits; + sideSize = VirtualObjectWidthInMeters * metersToUnits; + upSize = VirtualObjectHeightInMeters * metersToUnits; + axisConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention(); } else if (SelectedAnimatedObject != null) { - // 使用保存的原始尺寸(米),避免物体旋转后包围盒尺寸变化 - baseLengthMeters = _objectOriginalLength; - baseWidthMeters = _objectOriginalWidth; + double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(); + forwardSize = _objectOriginalLength * metersToUnits; + sideSize = _objectOriginalWidth * metersToUnits; + upSize = _objectOriginalHeight * metersToUnits; + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + axisConvention = CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail + ? ModelAxisConvention.CreateRailAssetConvention() + : ModelAxisConvention.CreateDefaultForHost(adapter.HostType); } else { - return (0, 0); + return (0, 0, 0); } - // 转换为模型单位 - var metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(); - double baseLength = baseLengthMeters * metersToUnits; - double baseWidth = baseWidthMeters * metersToUnits; + Quaternion correctionQuaternion = CoordinateSystemManager.Instance.CreateHostAdapter() + .CreateCanonicalRotationCorrection(_objectRotationCorrection); + var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents( + axisConvention, + forwardSize, + sideSize, + upSize, + correctionQuaternion); + double unitsToMeters = UnitsConverter.GetUnitsToMetersConversionFactor(); + LogManager.Debug( + $"[角度修正] 旋转后尺寸: 原始({forwardSize * unitsToMeters:F2}m×{sideSize * unitsToMeters:F2}m×{upSize * unitsToMeters:F2}m) -> " + + $"有效({result.forwardExtent * unitsToMeters:F2}m×{result.sideExtent * unitsToMeters:F2}m×{result.upExtent * unitsToMeters:F2}m), 角度={_objectRotationCorrection}"); - // 如果没有角度修正,直接返回原始尺寸 - if (_objectRotationCorrection == 0.0) - { - return (baseWidth, baseLength); - } - - // 计算旋转后的有效尺寸(投影) - double rotationRad = _objectRotationCorrection * Math.PI / 180.0; - double cos = Math.Abs(Math.Cos(rotationRad)); - double sin = Math.Abs(Math.Sin(rotationRad)); - - // 旋转后的宽度 = |宽度*cos| + |长度*sin| - double effectiveWidth = baseWidth * cos + baseLength * sin; - // 旋转后的长度 = |长度*cos| + |宽度*sin| - double effectiveLength = baseLength * cos + baseWidth * sin; - - LogManager.Debug($"[角度修正] 旋转后尺寸: 原始({baseLengthMeters:F2}m×{baseWidthMeters:F2}m) -> 有效({effectiveLength / metersToUnits:F2}m×{effectiveWidth / metersToUnits:F2}m), 角度={_objectRotationCorrection:F1}°"); - - return (effectiveWidth, effectiveLength); + return result; } /// @@ -4368,7 +4382,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels } // 计算旋转后的有效尺寸(考虑角度修正) - (double effectiveWidth, double effectiveLength) = CalculateRotatedDimensions(); + (double effectiveLength, double effectiveWidth, double effectiveHeight) = CalculateRotatedDimensions(); var renderPlugin = PathPointRenderPlugin.Instance; if (renderPlugin == null) @@ -4383,8 +4397,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 将安全间隙从米转换为模型单位 var metersToUnitsPassage = UnitsConverter.GetMetersToUnitsConversionFactor(); double safetyMargin = _safetyMarginInMeters * metersToUnitsPassage; - double virtualObjectHeight = VirtualObjectHeightInMeters * metersToUnitsPassage; - if (UseVirtualObject) { // 虚拟物体的xyz定义:X方向 = 长度(前进方向),Y方向 = 宽度(侧面),Z方向 = 高度 @@ -4392,11 +4404,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels { // 空中路径(空轨或吊装):高度上下都加间隙 passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) - passageNormalToPath = virtualObjectHeight + 2 * safetyMargin; // Z方向(高度)+ 2*间隙(法线方向) + passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 法线方向高度 + 2*间隙 passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) // 吊装路径分段参数 passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙 - passageNormalToPathHorizontal = virtualObjectHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙 + passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙 LogManager.Debug($"[通行空间可视化] 空中路径使用虚拟物体尺寸: 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); } else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground) @@ -4404,7 +4416,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 地面路径:物体的长度方向(X轴,前进方向)朝向路径方向 // 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) - passageNormalToPath = virtualObjectHeight + safetyMargin; // Z方向(高度)+ 间隙(法线方向,只有上方) + passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向高度 + 间隙(法线方向,只有上方) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) // 地面路径不需要分段参数 passageNormalToPathVertical = passageNormalToPath; @@ -4419,7 +4431,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // - 轨下安装:顶面贴路径,底面留间隙 // - 轨上安装:底面贴路径,顶面留间隙 passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) - passageNormalToPath = virtualObjectHeight + safetyMargin; // 法线方向单侧留间隙 + passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向单侧留间隙 passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) // 空轨路径不需要分段参数 passageNormalToPathVertical = passageNormalToPath; @@ -4429,26 +4441,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels } else if (SelectedAnimatedObject != null) { - // 使用选择物体的局部轴语义尺寸(模型单位) - var bbox = SelectedAnimatedObject.BoundingBox(); - var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); - var axisConvention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType); - double sizeAlongPath = axisConvention.GetForwardSize(bbox); - double sizeAcrossPath = axisConvention.GetSideSize(bbox); - double sizeNormalToPath = axisConvention.GetUpSize(bbox); - // 根据路径类型确定通行空间的尺寸 if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting) { // 空中路径(空轨或吊装): // 高度上下都加间隙 passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) - passageNormalToPath = sizeNormalToPath + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向) + passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) // 吊装路径分段参数 passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙 - passageNormalToPathHorizontal = sizeNormalToPath + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙 - LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); + passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙 + LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); } else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground) { @@ -4456,12 +4460,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 物体的长度方向(X轴,前进方向)朝向路径方向 // 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) - passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方) + passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) // 地面路径不需要分段参数 passageNormalToPathVertical = passageNormalToPath; passageNormalToPathHorizontal = passageNormalToPath; - LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); + LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); } else // Rail { @@ -4472,11 +4476,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels // - 轨下安装:顶面贴路径,底面留间隙 // - 轨上安装:底面贴路径,顶面留间隙 passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) - passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向) + passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) passageNormalToPathVertical = passageNormalToPath; passageNormalToPathHorizontal = passageNormalToPath; - LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); + LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); } } else @@ -4950,8 +4954,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels } // 重置角度修正值为0 - ObjectRotationCorrection = 0.0; - LogManager.Debug("[重置状态] 已重置角度修正值为0度"); + ObjectRotationCorrection = LocalEulerRotationCorrection.Zero; + LogManager.Debug("[重置状态] 已重置角度修正值为0"); // 清空选中对象 SelectedAnimatedObject = null; @@ -5261,7 +5265,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels DetectAllObjects = !IsManualCollisionTargetEnabled, // 角度修正配置 - ObjectRotationCorrection = _objectRotationCorrection, + ObjectRotationCorrection = _objectRotationCorrection.ZDegrees, // 🔥 关联的检测记录ID DetectionRecordId = detectionRecordId @@ -5346,7 +5350,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels $"虚拟物体: {queueItem.IsVirtualObject}, " + $"帧率: {queueItem.FrameRate}, " + $"时长: {queueItem.DurationSeconds:F2}秒, " + - $"角度修正: {queueItem.ObjectRotationCorrection:F1}°, " + + $"角度修正: {_objectRotationCorrection}, " + $"ID: {itemId}"); UpdateMainStatus($"已添加到批处理队列: {CurrentPathRoute.Name}"); diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml index f23922e..5eda71f 100644 --- a/src/UI/WPF/Views/EditRotationWindow.xaml +++ b/src/UI/WPF/Views/EditRotationWindow.xaml @@ -1,7 +1,7 @@  - + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - + 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轴向上"/> + + public Point3D RailPreferredNormal { get; set; } + /// + /// Rail 安装法向偏移(模型单位)。 + /// 沿 Rail 最终法向对安装参考点施加额外偏移,用于快速微调路径。 + /// + public double RailNormalOffset { get; set; } + // 数据库分析相关属性 /// /// 碰撞数量(从数据库加载) @@ -784,6 +790,7 @@ namespace NavisworksTransport RailMountMode = RailMountMode.UnderRail; RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; RailPreferredNormal = null; + RailNormalOffset = 0.0; } /// @@ -807,6 +814,7 @@ namespace NavisworksTransport RailMountMode = RailMountMode.UnderRail; RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; RailPreferredNormal = null; + RailNormalOffset = 0.0; } /// @@ -1042,6 +1050,7 @@ namespace NavisworksTransport SafetyMargin = SafetyMargin, RailMountMode = RailMountMode, RailPathDefinitionMode = RailPathDefinitionMode, + RailNormalOffset = RailNormalOffset, RailPreferredNormal = RailPreferredNormal != null ? new Point3D(RailPreferredNormal.X, RailPreferredNormal.Y, RailPreferredNormal.Z) : null diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 6d0fbdd..52dc0ff 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -2196,7 +2196,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels var dialog = new Views.EditRotationWindow(_objectRotationCorrection); if (dialog.ShowDialog() == true) { - ObjectRotationCorrection = dialog.RotationCorrection; + var placementRequest = dialog.AdjustmentRequest; + if (placementRequest.PreserveInitialPose) + { + ApplyTranslationOnlyObjectPlacement(); + return; + } + + _pathAnimationManager?.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose); + ObjectRotationCorrection = placementRequest.RotationCorrection; LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}"); UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}"); @@ -2214,6 +2222,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private void ApplyTranslationOnlyObjectPlacement() + { + try + { + if (_pathAnimationManager == null || !HasSelectedAnimatedObject) + { + return; + } + + _objectRotationCorrection = LocalEulerRotationCorrection.Zero; + _pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero); + _pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.PreserveInitialPose); + OnPropertyChanged(nameof(ObjectRotationCorrection)); + + if (_pathAnimationManager.MoveObjectToPathStartPreservingInitialPose()) + { + LogManager.Info("[角度修正] 已按平移模式将物体移动到路径起点,保持初始位姿"); + UpdateMainStatus("物体已平移到路径起点并保持初始位姿"); + } + else + { + LogManager.Warning("[角度修正] 平移模式未能将物体移动到路径起点"); + UpdateMainStatus("平移模式执行失败"); + } + + UpdatePassageSpaceVisualization(); + } + catch (Exception ex) + { + LogManager.Error($"平移到路径起点失败: {ex.Message}"); + UpdateMainStatus($"平移到路径起点失败: {ex.Message}"); + } + } + /// /// 更新物体旋转(应用角度修正) /// diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index ef3b6e3..cb660e2 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -125,6 +125,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters(); private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0; + private const double RailNormalOffsetNudgeStepInMeters = 0.1; private const double DefaultAssemblySphereCenterX = 0.0; private const double DefaultAssemblySphereCenterY = 0.0; private const double DefaultAssemblySphereCenterZ = 0.0; @@ -267,6 +268,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(CanUsePathLines)); OnPropertyChanged(nameof(IsRailRouteSelected)); OnPropertyChanged(nameof(SelectedRailMountMode)); + OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); if (!CanUsePathLines && ShowPathLines) { // 吊装和空轨路径不能使用路径线,自动关闭 @@ -348,6 +350,38 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + public double SelectedRailNormalOffsetInMeters + { + get + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail) + { + return 0.0; + } + + return UnitsConverter.ConvertToMeters(coreRoute.RailNormalOffset); + } + set + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail) + { + return; + } + + double offsetInModelUnits = UnitsConverter.ConvertFromMeters(value); + if (Math.Abs(coreRoute.RailNormalOffset - offsetInModelUnits) < 1e-6) + { + return; + } + + coreRoute.RailNormalOffset = offsetInModelUnits; + ApplyRailRouteConfigurationChange(coreRoute, $"安装法向偏移已更新为 {value:F3} 米"); + OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); + } + } + public string AssemblyTerminalObjectName { get => _assemblyTerminalObjectName; @@ -992,6 +1026,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand SelectAssemblyInstallationPointCommand { get; private set; } public ICommand ClearAssemblyReferenceRodCommand { get; private set; } public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; } + public ICommand DecreaseSelectedRailNormalOffsetCommand { get; private set; } + public ICommand IncreaseSelectedRailNormalOffsetCommand { get; private set; } // 多层吊装命令 public ICommand SelectMultiLevelStartPointCommand { get; private set; } @@ -1196,6 +1232,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}"); } + private void AdjustSelectedRailNormalOffset(double deltaInMeters) + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail) + { + return; + } + + double currentOffsetInMeters = UnitsConverter.ConvertToMeters(coreRoute.RailNormalOffset); + double nextOffsetInMeters = Math.Round(currentOffsetInMeters + deltaInMeters, 3); + SelectedRailNormalOffsetInMeters = nextOffsetInMeters; + } + /// /// 从配置文件加载参数 /// @@ -1336,6 +1385,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels SelectAssemblyInstallationPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(), () => CanSelectAssemblyInstallationPoint); ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod()); AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace); + DecreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(-RailNormalOffsetNudgeStepInMeters)); + IncreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(RailNormalOffsetNudgeStepInMeters)); } #endregion @@ -1704,7 +1755,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels Description = $"直线装配路径 - {AssemblyTerminalObjectName}", PathType = PathType.Rail, RailMountMode = AssemblyMountMode, - RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine + RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine, + RailNormalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters) }; if (_hasAssemblyInstallationReference) diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml index 5eda71f..a1fe247 100644 --- a/src/UI/WPF/Views/EditRotationWindow.xaml +++ b/src/UI/WPF/Views/EditRotationWindow.xaml @@ -1,7 +1,7 @@  + Margin="0,0,10,0"/> public double? CustomTurnRadius { get; set; } + /// + /// 仅用于视图显示的点直径(模型单位)。 + /// 为 null 或非正数时使用默认点类型尺寸。 + /// + [XmlIgnore] + public double? VisualizationDiameter { get; set; } + /// /// 方向类型(仅吊装路径使用) /// 用于标识吊装路径点的移动方向:垂直、纵向(X轴)、横向(Y轴) @@ -395,6 +402,7 @@ namespace NavisworksTransport Notes = string.Empty; SpeedLimit = 0; CustomTurnRadius = null; + VisualizationDiameter = null; Direction = HoistingPointDirection.Vertical; } @@ -415,9 +423,25 @@ namespace NavisworksTransport Notes = string.Empty; SpeedLimit = 0; CustomTurnRadius = null; + VisualizationDiameter = null; Direction = HoistingPointDirection.Vertical; } + /// + /// 解析最终用于渲染的点半径(模型单位)。 + /// + /// 默认点半径(模型单位) + /// 最终半径(模型单位) + public double ResolveVisualizationRadius(double defaultRadius) + { + if (!VisualizationDiameter.HasValue || VisualizationDiameter.Value <= 0) + { + return defaultRadius; + } + + return VisualizationDiameter.Value * 0.5; + } + /// /// 返回路径点描述字符串 /// diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index e6b5efd..5823d70 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -2268,7 +2268,8 @@ namespace NavisworksTransport { Center = point.Position, Normal = GetHostUpVector(), - Radius = isGridVisualization ? GetRadiusForGridVisualization() : GetRadiusForPointType(point.Type), + Radius = point.ResolveVisualizationRadius( + isGridVisualization ? GetRadiusForGridVisualization() : GetRadiusForPointType(point.Type)), Color = isGridVisualization ? gridStyle.Color : GetColorForPointType(point.Type), Alpha = isGridVisualization ? gridStyle.Alpha : 1.0, Filled = true, diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index cb660e2..902e4ae 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -125,6 +125,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters(); private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0; + private const double AssemblyVisualizationPointDiameterInMeters = 0.10; private const double RailNormalOffsetNudgeStepInMeters = 0.1; private const double DefaultAssemblySphereCenterX = 0.0; private const double DefaultAssemblySphereCenterY = 0.0; @@ -2259,7 +2260,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels for (int i = 0; i < _assemblyEndFaceSeedPoints.Count; i++) { - AddVisualizationPoint(markerRoute, _assemblyEndFaceSeedPoints[i], $"端面点{i + 1}", PathPointType.WayPoint); + AddVisualizationPoint( + markerRoute, + _assemblyEndFaceSeedPoints[i], + $"端面点{i + 1}", + PathPointType.WayPoint, + UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters)); } renderPlugin.RenderPointOnly(markerRoute); @@ -2280,11 +2286,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels Id = AssemblyEndFaceCenterPathId, Description = "终端安装端面中心" }; - AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.WayPoint); + AddVisualizationPoint( + markerRoute, + centerPoint, + "端面中心", + PathPointType.WayPoint, + UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters)); renderPlugin.RenderPointOnly(markerRoute); } - private static void AddVisualizationPoint(PathRoute route, Point3D position, string name, PathPointType type) + private static void AddVisualizationPoint( + PathRoute route, + Point3D position, + string name, + PathPointType type, + double? visualizationDiameter = null) { if (route == null) { @@ -2293,7 +2309,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels route.Points.Add(new PathPoint(position, name, type) { - Index = route.Points.Count + Index = route.Points.Count, + VisualizationDiameter = visualizationDiameter }); } @@ -2379,7 +2396,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels }; for (int i = 0; i < pickPoints.Count; i++) { - AddVisualizationPoint(markerRoute, pickPoints[i], $"安装面点{i + 1}", PathPointType.WayPoint); + AddVisualizationPoint( + markerRoute, + pickPoints[i], + $"安装面点{i + 1}", + PathPointType.WayPoint, + UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters)); } renderPlugin.RenderPointOnly(markerRoute); } @@ -2398,7 +2420,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels Id = AssemblyInstallationAnchorPointPathId, Description = "终端安装点" }; - AddVisualizationPoint(markerRoute, anchorPoint, "安装点", PathPointType.WayPoint); + AddVisualizationPoint( + markerRoute, + anchorPoint, + "安装点", + PathPointType.WayPoint, + UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters)); renderPlugin.RenderPointOnly(markerRoute); } From ad8d86e79f895b905cbd39dbc75a8d6f347d910b Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 26 Mar 2026 13:45:48 +0800 Subject: [PATCH 33/87] Restore assembly pick focus with space --- src/Core/PathInputMonitor.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Core/PathInputMonitor.cs b/src/Core/PathInputMonitor.cs index 6f376c7..57a5525 100644 --- a/src/Core/PathInputMonitor.cs +++ b/src/Core/PathInputMonitor.cs @@ -43,17 +43,17 @@ namespace NavisworksTransport if (key == 32) // 空格键 { var pathManager = PathPlanningManager.GetActivePathManager(); - if (pathManager != null && - (pathManager.PathEditState == PathEditState.Creating || - pathManager.PathEditState == PathEditState.AddingPoints || - pathManager.PathEditState == PathEditState.EditingPoint)) + if (pathManager != null) { var currentTool = Application.MainDocument.Tool.Value; if (currentTool != Tool.CustomToolPlugin) { - LogManager.Info($"[InputMonitor] 用户按空格键,当前工具为{currentTool},重新激活工具"); - pathManager.ReactivateToolPlugin(); - return true; + LogManager.Info($"[InputMonitor] 用户按空格键,当前工具为{currentTool},强制恢复ToolPlugin焦点"); + bool restored = pathManager.ForceReinitializeToolPlugin(subscribeToEvents: false); + if (restored) + { + return true; + } } } } @@ -67,4 +67,4 @@ namespace NavisworksTransport } } } -} \ No newline at end of file +} From 9f1924a79774cc9637979aa0a9b265ce431f5dde Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 26 Mar 2026 13:46:51 +0800 Subject: [PATCH 34/87] Add real object pose source migration draft --- ...real-object-pose-source-migration-draft.md | 423 ++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 doc/design/2026/real-object-pose-source-migration-draft.md diff --git a/doc/design/2026/real-object-pose-source-migration-draft.md b/doc/design/2026/real-object-pose-source-migration-draft.md new file mode 100644 index 0000000..364a370 --- /dev/null +++ b/doc/design/2026/real-object-pose-source-migration-draft.md @@ -0,0 +1,423 @@ +# 真实物体位姿来源迁移草案 + +## 1. 背景 + +当前真实物体位姿链路中,`fragment` 承担了两种不同语义: + +- 读取真实物体的**参考姿态** +- 间接参与判断真实物体的**当前显示姿态** + +这两个语义不应混在一起。 + +当前工程已确认: + +- `ModelItem.Transform` 不能默认视为真实物体当前姿态或原始姿态 +- `fragment` 代表姿态适合做“参考姿态解释” +- `ModelGeometry.ActiveTransform` 更适合做“当前实际显示姿态”读取 + +因此,本次迁移目标不是“简单移除 fragment”,而是: + +1. 先把**参考姿态来源**与**当前显示变换来源**拆开 +2. 优先把“当前显示姿态读取”切到准确变换 API +3. 保留 fragment 参考姿态链作为稳定基线 +4. 在测试和 shadow 验证足够充分后,再评估参考姿态主来源是否切换 + +--- + +## 2. 当前问题 + +### 2.1 语义混用 + +当前真实物体链路同时关心: + +- 物体原始业务姿态是什么 +- 物体此刻在 Navisworks 里实际显示成什么姿态 +- 增量变换叠加后,应以哪一层为“当前状态” + +如果这三者没有统一入口,就容易出现: + +- 起点姿态正确,但播放过程漂移 +- `ResetPermanentTransform` 后内部状态与宿主状态不同步 +- 通行空间、碰撞恢复、动画终点使用了不同的姿态基线 + +### 2.2 fragment 并不是“当前姿态 API” + +fragment 的 `GetLocalToWorldMatrix()` 更接近几何层矩阵。 + +它适合: + +- 统计代表姿态 +- 解释真实物体的原始参考框架 +- 参与 COM 几何分析 + +它不应优先承担: + +- 当前 override 后显示姿态读取 +- 动画增量变换后的宿主状态读回 + +### 2.3 直接回退 `ModelItem.Transform` 风险很高 + +对于很多 Revit/复合件导入模型: + +- `ModelItem.Transform` 可能是单位旋转 +- 不反映当前 override 后姿态 +- 不足以表达真实物体的业务参考姿态 + +所以迁移不能走“fragment -> ModelItem.Transform”的单步替换。 + +--- + +## 3. 迁移目标 + +### 3.1 核心目标 + +建立两条明确分离的来源链: + +- **当前显示变换链** + - 用于读取当前实际显示姿态 + - 优先使用 `ModelGeometry.ActiveTransform` + +- **参考姿态链** + - 用于表达真实物体的原始业务姿态 + - 当前仍以 fragment 解释链为稳定主来源 + +### 3.2 非目标 + +本阶段不做以下事情: + +- 不重写 `Ground / Hoisting / Rail` 的姿态求解器 +- 不直接移除现有 fragment 参考姿态链 +- 不修改 Canonical / Host / Asset 三层坐标语义 +- 不在没有验证前,把 `ModelItem.Transform` 升级为真实姿态主来源 + +--- + +## 4. 目标架构 + +### 4.1 新的职责划分 + +建议新增两类 provider: + +#### A. `ICurrentDisplayTransformProvider` + +职责: + +- 读取当前几何实际显示姿态 +- 读取当前几何实际旋转 +- 读取当前几何原始姿态与 override 后姿态 + +建议首个实现: + +- `GeometryActiveTransformProvider` + +优先数据源: + +1. `ModelGeometry.ActiveTransform` +2. `ModelGeometry.PermanentOverrideTransform` +3. `ModelGeometry.OriginalTransform` +4. 明确失败,不偷偷 fallback 到不可靠语义 + +#### B. `IReferencePoseProvider` + +职责: + +- 提供真实物体参考姿态 +- 输出解释后的 `Rotation + AxisX/Y/Z` +- 对上层屏蔽具体来源是 fragment 还是 geometry + +建议实现顺序: + +1. `FragmentReferencePoseProvider` + - 复用当前 `RealObjectReferencePoseResolver` +2. `GeometryReferencePoseProvider` + - 后续用于 shadow 验证 + - 初期不接主链 + +### 4.2 上层调用关系 + +建议上层只依赖语义接口: + +- `PathAnimationManager` + - 读取参考姿态时,只依赖 `IReferencePoseProvider` + - 读取当前宿主实际姿态时,只依赖 `ICurrentDisplayTransformProvider` + +- `ModelItemTransformHelper` + - 统一封装 `ActiveTransform / OriginalTransform / delta transform` + - 成为“当前显示变换链”的基础工具层 + +- `RealObjectPlanarPoseSolver` +- `CanonicalRailPoseBuilder` + - 保持纯数学求解,不感知来源替换 + +--- + +## 5. 分阶段迁移方案 + +### 阶段 0:冻结语义与测试基线 + +目标: + +- 不改正式行为 +- 先把关键业务场景和期望值固定下来 + +产出: + +- 参考姿态语义测试 +- 当前显示姿态读取测试 +- 增量变换恢复测试 +- `YUp / ZUp` 双宿主测试矩阵 + +通过条件: + +- 现有 fragment 主链全部通过基线测试 + +### 阶段 1:抽象来源接口,但不改行为 + +目标: + +- 只做代码结构调整 +- 不改变当前正式结果 + +建议动作: + +- 新增 `ICurrentDisplayTransformProvider` +- 新增 `IReferencePoseProvider` +- 让 `PathAnimationManager` 从直接依赖 helper/fragment,改为依赖 provider +- 默认配置仍然使用 `FragmentReferencePoseProvider` + +通过条件: + +- 行为无变化 +- 回归测试全绿 + +### 阶段 2:迁移“当前显示姿态读取” + +目标: + +- 把“当前姿态读取”切到准确变换 API + +建议动作: + +- 将以下读当前姿态的场景统一迁到 `ModelGeometry.ActiveTransform` + - 当前旋转读回 + - 当前显示姿态调试日志 + - override 后的实际姿态验证 + - 动画恢复/碰撞恢复前的状态同步 + +注意: + +- 这一阶段不改“参考姿态”来源 +- 只改“当前实际显示变换”的读取 + +通过条件: + +- 起点/播放/暂停/终点/恢复行为与当前主链一致 +- `ResetPermanentTransform` 后内部跟踪状态不再依赖 `ModelItem.Transform` + +### 阶段 3:引入 `GeometryReferencePoseProvider` shadow 验证 + +目标: + +- 并行验证 geometry 是否能稳定替代 fragment 参考姿态 + +建议动作: + +- 新增几何级参考姿态读取实现 +- 不接正式主链 +- 只在日志/调试模式下输出与 fragment 基线的差异 + +建议记录差异: + +- quaternion 夹角差 +- `AxisX / AxisY / AxisZ` 与基线的夹角差 +- 多 geometry 是否一致 +- `YUp / ZUp` 下解释后是否仍满足宿主语义 + +判定规则建议: + +- 若 geometry 内部姿态不一致,则自动判定“不适合作为语义参考姿态主来源” +- 若与 fragment 基线夹角超阈值,则继续保留 fragment 主来源 + +### 阶段 4:按路径类型逐步接入 + +目标: + +- 在证据足够充分后,才让新的参考姿态来源参与正式链路 + +建议接入顺序: + +1. `Rail` +2. `Ground` +3. `Hoisting` + +原因: + +- `Rail` 当前姿态框架最明确,收益最大 +- `Ground / Hoisting` 对“对象前进轴语义”更敏感 + +通过条件: + +- `Rail 0°` 基线不变 +- Ground 逐帧姿态不退化 +- 通行空间与真实物体仍共用同一尺寸语义 + +--- + +## 6. 建议新增/调整的代码结构 + +### 6.1 建议新增文件 + +- `src/Utils/CoordinateSystem/ICurrentDisplayTransformProvider.cs` +- `src/Utils/CoordinateSystem/IReferencePoseProvider.cs` +- `src/Utils/CoordinateSystem/GeometryActiveTransformProvider.cs` +- `src/Utils/CoordinateSystem/FragmentReferencePoseProvider.cs` +- `src/Utils/CoordinateSystem/GeometryReferencePoseProvider.cs` + +### 6.2 建议优先调整的现有文件 + +- `src/Core/Animation/PathAnimationManager.cs` + - 只依赖 provider + - 去掉对 fragment/helper 的直接耦合 + +- `src/Utils/ModelItemTransformHelper.cs` + - 成为统一的当前显示变换读取入口 + +- `src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs` + - 收敛为 fragment provider 的内部实现 + - 不再直接被业务层广泛调用 + +### 6.3 不建议本阶段改动的文件 + +- `CanonicalPlanarPoseBuilder` +- `CanonicalRailPoseBuilder` +- `CanonicalTrackedPositionResolver` +- `RailPathPoseHelper` + +原因: + +- 这些文件主要负责姿态求解与偏移语义 +- 不是本次来源迁移的根因层 + +--- + +## 7. 测试先行策略 + +### 7.1 测试原则 + +本次迁移必须遵循: + +1. 先补测试 +2. 再抽象接口 +3. 再切换读取来源 +4. 最后才考虑正式接入 + +### 7.2 第一批必须补的测试 + +#### A. 参考姿态基线测试 + +建议位置: + +- `UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs` + +锁定内容: + +- fragment 多片段平均后的代表姿态 +- `Fragment默认Up` 为 `Y/Z` 时的解释结果 +- `YUp / ZUp` 宿主下输出轴语义一致 + +#### B. 当前显示姿态读取测试 + +建议新增: + +- `UnitTests/CoordinateSystem/CurrentDisplayTransformProviderTests.cs` + +锁定内容: + +- `ActiveTransform` 优先级 +- `OriginalTransform / PermanentOverrideTransform / ActiveTransform` 的语义区分 +- override 后读取结果与预期一致 + +#### C. 动画增量恢复测试 + +建议新增: + +- `UnitTests/CoordinateSystem/IncrementalTransformRestoreTests.cs` + +锁定内容: + +- reset 后状态同步 +- 当前跟踪姿态与宿主状态一致 +- 不再错误依赖 `ModelItem.Transform` + +#### D. 业务回归测试 + +建议补到现有测试中: + +- `Ground + 真实物体` +- `Rail + 真实物体` +- `YUp / ZUp` +- 起点 / 逐帧 / 终点 / restore + +### 7.3 第二批 shadow 验证测试 + +建议新增: + +- `UnitTests/CoordinateSystem/GeometryReferencePoseProviderTests.cs` + +锁定内容: + +- 单 geometry 对象能否稳定导出参考姿态 +- 多 geometry 姿态不一致时是否正确拒绝 +- geometry 基线与 fragment 基线差异是否在阈值内 + +--- + +## 8. 验收标准 + +### 8.1 阶段 1-2 验收 + +- 正式行为无肉眼可见退化 +- 所有已有姿态回归测试通过 +- `PathAnimationManager` 不再把“当前姿态读取”和“参考姿态来源”混用 + +### 8.2 阶段 3 验收 + +- 能输出 geometry vs fragment 的稳定差异报告 +- 能区分“可替代对象”和“不可替代对象” + +### 8.3 阶段 4 验收 + +- `Rail 0°` 基线不变 +- `Ground / Hoisting` 逐帧行为不退化 +- 通行空间、碰撞恢复、终点诊断不出现新的语义分叉 + +--- + +## 9. 当前推荐执行顺序 + +建议下一步按下面顺序推进: + +1. 先补测试,不接正式代码 +2. 抽 provider 接口,不改默认实现 +3. 先切“当前显示姿态读取” +4. 做 geometry 参考姿态 shadow 验证 +5. 证据足够后,再讨论 fragment 主来源是否退出 + +--- + +## 10. 当前结论 + +本次迁移的关键不是“删掉 fragment”,而是: + +- 先把语义拆开 +- 先把准确 API 用在该用的地方 +- 先用测试固定业务场景 +- 再用 shadow 验证去证明新的参考姿态来源是否真的可靠 + +在当前阶段,**最稳的路线**是: + +- fragment 继续作为真实物体参考姿态主来源 +- `ModelGeometry.ActiveTransform` 接管当前显示姿态读取 +- 测试先行 +- 分阶段接入 + From a5d1db6416b7dbdf00407e0a4ae64dd1dced8c77 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 26 Mar 2026 22:49:45 +0800 Subject: [PATCH 35/87] Refine collision analysis dialog and rail assembly markers --- .../ViewModels/AnimationControlViewModel.cs | 98 ++++++------ src/UI/WPF/ViewModels/PathEditingViewModel.cs | 6 +- src/UI/WPF/Views/CollisionAnalysisDialog.xaml | 145 ++++++++++-------- .../WPF/Views/CollisionAnalysisDialog.xaml.cs | 44 +++++- 4 files changed, 178 insertions(+), 115 deletions(-) diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 52dc0ff..7fbe3f1 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -3103,50 +3103,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels { var dialog = new CollisionAnalysisDialog(hotspots, totalCollisions); DialogHelper.SetOwnerSafely(dialog); - var result = dialog.ShowDialog(); - - if (result == null) - { - // 用户取消 - UpdateMainStatus("预计算碰撞分析已取消"); - return; - } - - if (dialog.ExcludedObjects.Count > 0) - { - // 用户选择了排除某些物体并重新生成 - LogManager.Info($"[碰撞分析] 用户选择排除 {dialog.ExcludedObjects.Count} 个物体并重新生成"); - - // 🔥 添加排除对象到UI列表(合并,自动去重) - AddExcludedObjectsToUIList(dialog.ExcludedObjects); - - // 🔥 从UI列表获取所有排除对象(包括之前手工添加的),设置到Manager并重新生成 - var allExcludedObjects = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList(); - _pathAnimationManager.SetExcludedObjectsAndClearCache(allExcludedObjects); - - // 触发重新生成(使用 Dispatcher 避免阻塞 UI) - System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - ExecuteGenerateAnimation(); - }), DispatcherPriority.Background); - - UpdateMainStatus($"已排除 {allExcludedObjects.Count} 个物体,正在重新生成..."); - } - else - { - // 用户选择直接继续(无排除对象) - LogManager.Info("[碰撞分析] 用户选择直接继续生成"); - UpdateMainStatus($"预计算碰撞分析完成,共 {totalCollisions} 个候选碰撞"); - - // 🔥 保存检测记录到数据库(保存测试配置) - var recordId = SaveCollisionDetectionRecord(); - if (recordId.HasValue && IsReusedHistoryRecord(recordId.Value)) - { - // 用户选择使用历史记录,跳过后续流程 - LogManager.Info("[碰撞分析] 用户选择使用历史记录,跳过动画生成"); - return; - } - } + dialog.Closed += (_, __) => HandleCollisionAnalysisDialogClosed(dialog, totalCollisions); + dialog.Show(); + dialog.Activate(); } catch (Exception ex) { @@ -3154,6 +3113,57 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private void HandleCollisionAnalysisDialogClosed(CollisionAnalysisDialog dialog, int totalCollisions) + { + try + { + if (dialog == null) + { + return; + } + + switch (dialog.SelectedAction) + { + case CollisionAnalysisDialog.CollisionAnalysisAction.Regenerate: + LogManager.Info($"[碰撞分析] 用户选择排除 {dialog.ExcludedObjects.Count} 个物体并重新生成"); + + AddExcludedObjectsToUIList(dialog.ExcludedObjects); + + var allExcludedObjects = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList(); + _pathAnimationManager.SetExcludedObjectsAndClearCache(allExcludedObjects); + + System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + ExecuteGenerateAnimation(); + }), DispatcherPriority.Background); + + UpdateMainStatus($"已排除 {allExcludedObjects.Count} 个物体,正在重新生成..."); + break; + + case CollisionAnalysisDialog.CollisionAnalysisAction.Continue: + LogManager.Info("[碰撞分析] 用户选择直接继续生成"); + UpdateMainStatus($"预计算碰撞分析完成,共 {totalCollisions} 个候选碰撞"); + + var recordId = SaveCollisionDetectionRecord(); + if (recordId.HasValue && IsReusedHistoryRecord(recordId.Value)) + { + LogManager.Info("[碰撞分析] 用户选择使用历史记录,跳过动画生成"); + } + break; + + case CollisionAnalysisDialog.CollisionAnalysisAction.Cancel: + case CollisionAnalysisDialog.CollisionAnalysisAction.None: + default: + UpdateMainStatus("预计算碰撞分析已取消"); + break; + } + } + catch (Exception ex) + { + LogManager.Error($"[碰撞分析] 处理分析窗口结果失败: {ex.Message}", ex); + } + } + /// /// 保存检测记录到数据库 /// 在生成动画完成后调用,创建检测记录并保存当时的完整配置 diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 902e4ae..7a279b4 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -125,7 +125,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters(); private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0; - private const double AssemblyVisualizationPointDiameterInMeters = 0.10; + private const double AssemblyVisualizationPointDiameterInMeters = 0.05; private const double RailNormalOffsetNudgeStepInMeters = 0.1; private const double DefaultAssemblySphereCenterX = 0.0; private const double DefaultAssemblySphereCenterY = 0.0; @@ -1749,7 +1749,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private void CreateAssemblyLinearRoute(Point3D startPoint) { Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd; - string routeName = $"直线装配_{_pathPlanningManager.Routes.Count + 1}"; + string routeName = $"人工_{DateTime.Now:MMdd_HHmmss}"; var route = new PathRoute(routeName) { @@ -1768,7 +1768,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels _assemblyInstallationPlaneNormal.Z); } - route.AddPoint(new PathPoint(startPoint, "装配起点", PathPointType.StartPoint)); + route.AddPoint(new PathPoint(startPoint, "起点", PathPointType.StartPoint)); route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint)); if (!_pathPlanningManager.AddRoute(route)) diff --git a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml index 8eed1aa..5e2679b 100644 --- a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml +++ b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml @@ -8,7 +8,6 @@ Height="720" Width="720" WindowStartupLocation="CenterOwner" ResizeMode="CanResize" - Topmost="True" Background="White"> @@ -97,67 +96,89 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + public partial class CollisionAnalysisDialog : Window { + public enum CollisionAnalysisAction + { + None, + Continue, + Regenerate, + Cancel + } + private List _viewModels; public List ExcludedObjects { get; private set; } + public CollisionAnalysisAction SelectedAction { get; private set; } = CollisionAnalysisAction.None; // 使用项目中定义的预计算碰撞结果高亮类别(紫色 #9C27B0) private const string PrecomputedHighlightCategory = ModelHighlightHelper.PrecomputeCollisionResultsCategory; @@ -122,17 +131,14 @@ namespace NavisworksTransport.UI.WPF.Views // 清除预计算高亮 ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory); - // 根据是否有排除对象设置对话框结果 if (ExcludedObjects.Count > 0) { - // 有排除对象,需要重新生成 - DialogResult = true; + SelectedAction = CollisionAnalysisAction.Regenerate; LogManager.Info($"[碰撞分析] 用户选择排除 {ExcludedObjects.Count} 个物体并重新生成"); } else { - // 没有排除对象,直接继续 - DialogResult = false; + SelectedAction = CollisionAnalysisAction.Continue; LogManager.Info("[碰撞分析] 用户选择直接继续生成(无排除对象)"); } @@ -144,10 +150,36 @@ namespace NavisworksTransport.UI.WPF.Views // 清除预计算高亮 ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory); - DialogResult = null; + SelectedAction = CollisionAnalysisAction.Cancel; Close(); } + private void SelectAllButton_Click(object sender, RoutedEventArgs e) + { + if (_viewModels == null) + { + return; + } + + foreach (var viewModel in _viewModels) + { + viewModel.IsExcluded = true; + } + } + + private void ClearAllButton_Click(object sender, RoutedEventArgs e) + { + if (_viewModels == null) + { + return; + } + + foreach (var viewModel in _viewModels) + { + viewModel.IsExcluded = false; + } + } + /// /// 视图模型 /// From 7db5a7a201ad9c477ff8bd8b056c0774308ad09c Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 28 Mar 2026 12:36:24 +0800 Subject: [PATCH 36/87] Add path duplication workflow --- NavisworksTransport.UnitTests.csproj | 2 + UnitTests/Core/PathHelperTests.cs | 28 ++++++++ UnitTests/Core/PathRouteCloneTests.cs | 71 +++++++++++++++++++ src/Core/PathPlanningModels.cs | 48 +++++++++++-- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 47 ++++++++++++ src/UI/WPF/Views/PathEditingView.xaml | 4 ++ src/Utils/PathHelper.cs | 23 +++++- 7 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 UnitTests/Core/PathHelperTests.cs create mode 100644 UnitTests/Core/PathRouteCloneTests.cs diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index 0a8140c..aa14ce5 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -53,7 +53,9 @@ + + diff --git a/UnitTests/Core/PathHelperTests.cs b/UnitTests/Core/PathHelperTests.cs new file mode 100644 index 0000000..36bf856 --- /dev/null +++ b/UnitTests/Core/PathHelperTests.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.UnitTests.Core +{ + [TestClass] + public class PathHelperTests + { + [TestMethod] + public void BuildDuplicatedPathName_ShouldRebuildTrailingTimestamp() + { + var now = new DateTime(2026, 3, 28, 9, 45, 12); + + var duplicatedName = PathHelper.BuildDuplicatedPathName("人工_0328_081530", now); + + Assert.AreEqual("副本_人工_0328_094512", duplicatedName); + } + + [TestMethod] + public void BuildDuplicatedPathName_ShouldKeepPlainNameWithoutTimestamp() + { + var duplicatedName = PathHelper.BuildDuplicatedPathName("我的路径"); + + Assert.AreEqual("副本_我的路径", duplicatedName); + } + } +} diff --git a/UnitTests/Core/PathRouteCloneTests.cs b/UnitTests/Core/PathRouteCloneTests.cs new file mode 100644 index 0000000..a1e5c75 --- /dev/null +++ b/UnitTests/Core/PathRouteCloneTests.cs @@ -0,0 +1,71 @@ +using Autodesk.Navisworks.Api; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NavisworksTransport.UnitTests.Core +{ + [TestClass] + public class PathRouteCloneTests + { + [TestMethod] + public void Clone_ShouldPreserveRailSpecificPropertiesAndGenerateNewIds() + { + var route = new PathRoute("原路径") + { + PathType = PathType.Rail, + RailMountMode = RailMountMode.OverRail, + RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine, + RailNormalOffset = 12.5, + RailPreferredNormal = new Point3D(0, 1, 0) + }; + + route.Points.Add(new PathPoint(new Point3D(1, 2, 3), "起点", PathPointType.StartPoint)); + route.Points.Add(new PathPoint(new Point3D(4, 5, 6), "终点", PathPointType.EndPoint)); + + var clonedRoute = route.Clone(); + + Assert.AreEqual(PathType.Rail, clonedRoute.PathType); + Assert.AreEqual(RailMountMode.OverRail, clonedRoute.RailMountMode); + Assert.AreEqual(RailPathDefinitionMode.RailCenterLine, clonedRoute.RailPathDefinitionMode); + Assert.AreEqual(12.5, clonedRoute.RailNormalOffset, 1e-6); + Assert.IsNotNull(clonedRoute.RailPreferredNormal); + Assert.AreNotEqual(route.Id, clonedRoute.Id); + Assert.AreEqual(2, clonedRoute.Points.Count); + Assert.AreNotEqual(route.Points[0].Id, clonedRoute.Points[0].Id); + Assert.AreEqual(route.Points[0].Name, clonedRoute.Points[0].Name); + Assert.AreEqual(route.Points[0].Type, clonedRoute.Points[0].Type); + } + + [TestMethod] + public void Clone_ShouldCopyGroundEdgesAndRemapPointReferences() + { + var route = new PathRoute("地面路径") + { + PathType = PathType.Ground, + TurnRadius = 3.0, + IsCurved = true + }; + + var startPoint = new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint); + var endPoint = new PathPoint(new Point3D(10, 0, 0), "终点", PathPointType.EndPoint); + route.Points.Add(startPoint); + route.Points.Add(endPoint); + route.Edges.Add(new PathEdge + { + StartPointId = startPoint.Id, + EndPointId = endPoint.Id, + SegmentType = PathSegmentType.Straight, + PhysicalLength = 10.0 + }); + + var clonedRoute = route.Clone(); + + Assert.AreEqual(PathType.Ground, clonedRoute.PathType); + Assert.AreEqual(1, clonedRoute.Edges.Count); + Assert.AreEqual(3.0, clonedRoute.TurnRadius, 1e-6); + Assert.IsTrue(clonedRoute.IsCurved); + Assert.AreEqual(clonedRoute.Points[0].Id, clonedRoute.Edges[0].StartPointId); + Assert.AreEqual(clonedRoute.Points[1].Id, clonedRoute.Edges[0].EndPointId); + Assert.AreNotEqual(route.Edges[0].Id, clonedRoute.Edges[0].Id); + } + } +} diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index 2cbd84b..eabea26 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -1058,7 +1058,7 @@ namespace NavisworksTransport Id = Guid.NewGuid().ToString(), EstimatedTime = EstimatedTime, // TotalLength:计算属性,自动从几何数据计算,克隆时会重新计算 - CreatedTime = CreatedTime, + CreatedTime = DateTime.Now, LastModified = DateTime.Now, Description = Description, AssociatedChannelIds = new List(AssociatedChannelIds), @@ -1072,14 +1072,18 @@ namespace NavisworksTransport MaxObjectWidth = MaxObjectWidth, MaxObjectHeight = MaxObjectHeight, SafetyMargin = SafetyMargin, + TurnRadius = TurnRadius, + IsCurved = IsCurved, + PathType = PathType, + LiftHeight = LiftHeight, RailMountMode = RailMountMode, RailPathDefinitionMode = RailPathDefinitionMode, RailNormalOffset = RailNormalOffset, - RailPreferredNormal = RailPreferredNormal != null - ? new Point3D(RailPreferredNormal.X, RailPreferredNormal.Y, RailPreferredNormal.Z) - : null + RailPreferredNormal = RailPreferredNormal }; + var pointIdMap = new Dictionary(); + // 克隆所有路径点 foreach (var point in Points) { @@ -1088,9 +1092,43 @@ namespace NavisworksTransport Id = Guid.NewGuid().ToString(), Index = point.Index, CreatedTime = point.CreatedTime, - Notes = point.Notes + Notes = point.Notes, + SpeedLimit = point.SpeedLimit, + CustomTurnRadius = point.CustomTurnRadius, + VisualizationDiameter = point.VisualizationDiameter, + Direction = point.Direction }; clonedRoute.Points.Add(clonedPoint); + pointIdMap[point.Id] = clonedPoint.Id; + } + + foreach (var edge in Edges) + { + var clonedEdge = new PathEdge + { + Id = Guid.NewGuid().ToString(), + StartPointId = pointIdMap.ContainsKey(edge.StartPointId) ? pointIdMap[edge.StartPointId] : edge.StartPointId, + EndPointId = pointIdMap.ContainsKey(edge.EndPointId) ? pointIdMap[edge.EndPointId] : edge.EndPointId, + SegmentType = edge.SegmentType, + PhysicalLength = edge.PhysicalLength, + Trajectory = edge.Trajectory != null + ? new ArcTrajectory + { + Ts = edge.Trajectory.Ts, + Te = edge.Trajectory.Te, + ArcCenter = edge.Trajectory.ArcCenter, + RequestedRadius = edge.Trajectory.RequestedRadius, + ActualRadius = edge.Trajectory.ActualRadius, + DeflectionAngle = edge.Trajectory.DeflectionAngle, + ArcLength = edge.Trajectory.ArcLength + } + : null, + SampledPoints = edge.SampledPoints != null + ? new List(edge.SampledPoints) + : new List() + }; + + clonedRoute.Edges.Add(clonedEdge); } return clonedRoute; diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 7a279b4..019a605 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1005,6 +1005,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand NewRailPathCommand { get; private set; } public ICommand NewHoistingPathCommand { get; private set; } public ICommand NewMultiLevelHoistingPathCommand { get; private set; } + public ICommand DuplicatePathCommand { get; private set; } public ICommand DeletePathCommand { get; private set; } public ICommand RenamePathCommand { get; private set; } public ICommand StartEditCommand { get; private set; } @@ -1364,6 +1365,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels PickMultiLevelTargetCommand = new RelayCommand((item) => ExecutePickMultiLevelTarget(item)); CreateMultiLevelPathCommand = new RelayCommand(async () => await ExecuteCreateMultiLevelPathAsync(), () => CanCreateMultiLevelPath); CancelMultiLevelHoistingCommand = new RelayCommand(() => ExecuteCancelMultiLevelHoisting()); + DuplicatePathCommand = new RelayCommand(async () => await ExecuteDuplicatePathAsync()); DeletePathCommand = new RelayCommand(async () => await ExecuteDeletePathAsync()); RenamePathCommand = new RelayCommand(async () => await ExecuteRenamePathAsync()); StartEditCommand = new RelayCommand(async () => await ExecuteAddPathPointAsync(), () => CanExecuteStartEdit); @@ -3595,6 +3597,51 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "删除路径"); } + private async Task ExecuteDuplicatePathAsync() + { + if (SelectedPathRoute == null || _pathPlanningManager == null) return; + + await SafeExecuteAsync(async () => + { + var sourceRoute = SelectedPathRoute.Route ?? + _pathPlanningManager.Routes.FirstOrDefault(r => r.Id == SelectedPathRoute.Id); + if (sourceRoute == null) + { + throw new InvalidOperationException("未找到要复制的路径数据"); + } + + UpdateMainStatus($"正在复制路径: {sourceRoute.Name}..."); + + var duplicatedRoute = sourceRoute.Clone(); + duplicatedRoute.Name = PathHelper.BuildDuplicatedPathName(sourceRoute.Name); + duplicatedRoute.CreatedTime = DateTime.Now; + duplicatedRoute.LastModified = DateTime.Now; + + if (!_pathPlanningManager.AddRoute(duplicatedRoute)) + { + throw new InvalidOperationException($"复制路径失败: {sourceRoute.Name}"); + } + + var duplicatedPathViewModel = new PathRouteViewModel(isFromDatabase: true) + { + Route = duplicatedRoute, + IsActive = false + }; + duplicatedPathViewModel.SetTimeInfo(duplicatedRoute.CreatedTime, duplicatedRoute.LastModified); + + foreach (var point in CreatePathPointViewModelsFromCoreRoute(duplicatedRoute)) + { + duplicatedPathViewModel.Points.Add(point); + } + + PathRoutes.Add(duplicatedPathViewModel); + SelectedPathRoute = duplicatedPathViewModel; + UpdateMainStatus($"已复制路径: {sourceRoute.Name} -> {duplicatedPathViewModel.Name}"); + + await Task.CompletedTask; + }, "复制路径"); + } + #endregion #region 坐标编辑命令 diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 5cef289..84e645a 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -214,6 +214,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 Command="{Binding NewHoistingPathCommand}" Style="{StaticResource ActionButtonStyle}" ToolTip="创建吊装路径(支持多层)"/> + + public static string BuildDuplicatedPathName(string originalName, DateTime? now = null) + { + var normalizedName = string.IsNullOrWhiteSpace(originalName) ? "路径" : originalName.Trim(); + var timestampPattern = new Regex(@"^(.*)_\d{4}_\d{6}$"); + var match = timestampPattern.Match(normalizedName); + + if (!match.Success) + { + return $"副本_{normalizedName}"; + } + + var baseName = match.Groups[1].Value; + var timestamp = (now ?? DateTime.Now).ToString("MMdd_HHmmss"); + return $"副本_{baseName}_{timestamp}"; + } + /// /// 计算从HTML文件到目标文件的相对路径 /// @@ -203,4 +224,4 @@ namespace NavisworksTransport.Utils } } } -} \ No newline at end of file +} From 95500c97175d9527fa6422c81c03c2d35c94c7db Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 28 Mar 2026 12:42:54 +0800 Subject: [PATCH 37/87] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB=E4=B8=BADebug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Core/Animation/PathAnimationManager.cs | 18 +++++++++--------- src/Core/PathDatabase.cs | 2 +- .../ViewModels/AnimationControlViewModel.cs | 4 ++-- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 8 +------- src/Utils/ModelItemTransformHelper.cs | 12 ++++++------ src/Utils/RailPathPoseHelper.cs | 4 ++-- 6 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index c991f10..4ed30db 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -2758,7 +2758,7 @@ namespace NavisworksTransport.Core.Animation { var actualHostPosition = GetTrackedObjectPosition(controlledObject); currentPositionForTransform = actualHostPosition; - LogManager.Info( + LogManager.Debug( $"[动画姿态入口] {controlledObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})"); if (IsRealObjectMode && @@ -2767,12 +2767,12 @@ namespace NavisworksTransport.Core.Animation { var trackedLinear = new Transform3D(currentRotation).Linear; var actualLinear = new Transform3D(actualGeometryRotation).Linear; - LogManager.Info( + LogManager.Debug( $"[平面姿态基线诊断] {controlledObject.DisplayName} 跟踪旋转: " + $"X=({trackedLinear.Get(0, 0):F4},{trackedLinear.Get(1, 0):F4},{trackedLinear.Get(2, 0):F4}), " + $"Y=({trackedLinear.Get(0, 1):F4},{trackedLinear.Get(1, 1):F4},{trackedLinear.Get(2, 1):F4}), " + $"Z=({trackedLinear.Get(0, 2):F4},{trackedLinear.Get(1, 2):F4},{trackedLinear.Get(2, 2):F4})"); - LogManager.Info( + LogManager.Debug( $"[平面姿态基线诊断] {controlledObject.DisplayName} 实际几何旋转: " + $"X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " + $"Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " + @@ -2805,15 +2805,15 @@ namespace NavisworksTransport.Core.Animation var currentLinear = new Transform3D(currentRotation).Linear; var targetLinear = new Transform3D(appliedTargetRotation).Linear; - LogManager.Info( + LogManager.Debug( $"[动画姿态入口] {controlledObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " + $"目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})"); - LogManager.Info( + LogManager.Debug( $"[动画姿态入口] {controlledObject.DisplayName} 跟踪姿态: " + $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " + $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " + $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})"); - LogManager.Info( + LogManager.Debug( $"[动画姿态入口] {controlledObject.DisplayName} 目标姿态: " + $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + @@ -4050,7 +4050,7 @@ namespace NavisworksTransport.Core.Animation out var selectedForwardAxis, out var selectedForwardWorldAxis)) { - LogManager.Info( + LogManager.Debug( $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " + $"真实物体参考姿态生效, Forward={convention.ForwardAxis}, Up={convention.UpAxis}, " + $"选中Forward轴={selectedForwardAxis}, 选中Forward世界轴=({selectedForwardWorldAxis.X:F4},{selectedForwardWorldAxis.Y:F4},{selectedForwardWorldAxis.Z:F4})"); @@ -4059,7 +4059,7 @@ namespace NavisworksTransport.Core.Animation } convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType); - LogManager.Info( + LogManager.Debug( $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " + $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}, 来源=默认轴约定"); return true; @@ -4358,7 +4358,7 @@ namespace NavisworksTransport.Core.Animation Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion); Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion); - LogManager.Info( + LogManager.Debug( $"[Rail真实物体角度修正] BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " + $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " + $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " + diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index 9a0299b..857d56a 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -1208,7 +1208,7 @@ namespace NavisworksTransport } } - LogManager.Info($"[GetClashDetectiveCollisionObjects] 查询到 {results.Count} 个碰撞对象,DetectionRecordId={detectionRecordId}"); + LogManager.Info($"[获取碰撞对象] 查询到 {results.Count} 个碰撞对象,DetectionRecordId={detectionRecordId}"); } catch (Exception ex) { diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 7fbe3f1..242fcd0 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -3660,7 +3660,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels } var records = pathDatabase.GetDetectionResultsByPath(pathName); - LogManager.Info($"[刷新碰撞历史] 查询到 {records.Count} 条记录,路径={pathName}"); + LogManager.Debug($"[刷新碰撞历史] 查询到 {records.Count} 条记录,路径={pathName}"); _clashDetectiveResults.Clear(); foreach (var record in records) @@ -3693,7 +3693,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels PathId = record.ObjectPathId }; resultViewModel.AnimatedObject = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.ResolvePathId(animatedObjectPathId); - LogManager.Info($"[碰撞历史] 已加载运动物体: {resultViewModel.AnimatedObject?.DisplayName ?? "未找到"} for TestName={record.TestName}"); + LogManager.Debug($"[碰撞历史] 已加载运动物体: {resultViewModel.AnimatedObject?.DisplayName ?? "未找到"} for TestName={record.TestName}"); } catch (Exception ex) { diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 019a605..36b2e4d 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -6095,8 +6095,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 处理从数据库加载的历史路径 foreach (var coreRoute in e.Routes) { - LogManager.Info($"*** 开始处理数据库加载路径: {coreRoute.Name} ***"); - // 创建对应的 WPF ViewModel var dbPathViewModel = new PathRouteViewModel(isFromDatabase: true) { @@ -6115,26 +6113,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels dbPathViewModel.Points.Add(point); } - LogManager.Info($"*** 已创建PathRouteViewModel(数据库路径),包含 {dbPathViewModel.Points.Count} 个点 ***"); - // 检查是否已存在同名路径,避免重复添加 var existingPath = PathRoutes.FirstOrDefault(p => p.Name == coreRoute.Name); if (existingPath == null) { // 添加到 UI 列表 PathRoutes.Add(dbPathViewModel); - LogManager.Info($"*** 数据库路径已添加到UI列表: {coreRoute.Name},当前PathRoutes.Count = {PathRoutes.Count} ***"); } else { - LogManager.Info($"*** 数据库路径已存在,跳过: {coreRoute.Name} ***"); + LogManager.Debug($"*** 数据库路径已存在,跳过: {coreRoute.Name} ***"); } } // 更新状态消息 string statusMessage = $"✅ 已从{e.LoadSource}加载 {e.RouteCount} 条历史路径"; UpdateMainStatus(statusMessage); - LogManager.Info($"*** 状态已更新: {statusMessage} ***"); // 自动提取并显示所有空轨模型的基准线 ExtractAndRenderRailBaselinePaths(); diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs index 58460e5..dd2f95a 100644 --- a/src/Utils/ModelItemTransformHelper.cs +++ b/src/Utils/ModelItemTransformHelper.cs @@ -540,24 +540,24 @@ namespace NavisworksTransport.Utils targetPosition.Y - rotatedCurrentPosition.Y, targetPosition.Z - rotatedCurrentPosition.Z); - LogManager.Info( + LogManager.Debug( $"[模型增量姿态] {item.DisplayName} 当前=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " + $"目标=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " + $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})"); - LogManager.Info( + LogManager.Debug( $"[模型增量姿态] {item.DisplayName} 当前旋转: " + $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " + $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " + $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})"); - LogManager.Info( + LogManager.Debug( $"[模型增量姿态] {item.DisplayName} 目标旋转: " + $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); - LogManager.Info( + LogManager.Debug( $"[模型增量姿态] {item.DisplayName} 增量旋转: " + $"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " + $"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " + @@ -635,13 +635,13 @@ namespace NavisworksTransport.Utils var targetLinear = new Transform3D(targetRotation).Linear; var actualPosition = actualBounds.Center; - LogManager.Info( + LogManager.Debug( $"[模型增量姿态] {item.DisplayName} 立即读回位置(可能滞后): " + $"实际=({actualPosition.X:F3},{actualPosition.Y:F3},{actualPosition.Z:F3}), " + $"期望=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " + $"偏差=({actualPosition.X - targetPosition.X:F3},{actualPosition.Y - targetPosition.Y:F3},{actualPosition.Z - targetPosition.Z:F3})"); - LogManager.Info( + LogManager.Debug( $"[模型增量姿态] {item.DisplayName} 立即读回旋转(可能滞后/不反映override): " + $"实际X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " + $"实际Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " + diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs index b485e47..22edc97 100644 --- a/src/Utils/RailPathPoseHelper.cs +++ b/src/Utils/RailPathPoseHelper.cs @@ -291,7 +291,7 @@ namespace NavisworksTransport.Utils hostXAxis.Y, hostYAxis.Y, hostZAxis.Y, hostXAxis.Z, hostYAxis.Z, hostZAxis.Z); - LogManager.Info( + LogManager.Debug( $"[Rail姿态] Canonical切向=({canonicalForward.X:F4},{canonicalForward.Y:F4},{canonicalForward.Z:F4}), " + $"Canonical侧向=({canonicalLateral.X:F4},{canonicalLateral.Y:F4},{canonicalLateral.Z:F4}), " + $"Canonical法向=({canonicalUp.X:F4},{canonicalUp.Y:F4},{canonicalUp.Z:F4}), " + @@ -414,7 +414,7 @@ namespace NavisworksTransport.Utils rotation = CreateRotationFromLinearTransform(linearTransform); var verifiedLinear = new Transform3D(rotation).Linear; - LogManager.Info( + LogManager.Debug( $"[Rail旋转验证] X=({verifiedLinear.Get(0, 0):F4},{verifiedLinear.Get(1, 0):F4},{verifiedLinear.Get(2, 0):F4}), " + $"Y=({verifiedLinear.Get(0, 1):F4},{verifiedLinear.Get(1, 1):F4},{verifiedLinear.Get(2, 1):F4}), " + $"Z=({verifiedLinear.Get(0, 2):F4},{verifiedLinear.Get(1, 2):F4},{verifiedLinear.Get(2, 2):F4})"); From e4788c17a3c41e96355a7082d115c6ee17341a8c Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 28 Mar 2026 13:54:48 +0800 Subject: [PATCH 38/87] Improve rail path installation point editing --- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 177 +++++++++++++++++- src/UI/WPF/Views/PathEditingView.xaml | 45 ++++- 2 files changed, 205 insertions(+), 17 deletions(-) diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 36b2e4d..8700eed 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -156,6 +156,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private Vector3 _assemblyInstallationPlaneNormal; private Vector3 _assemblyInstallationPlaneSpanDirection; private double _assemblyInstallationOffsetDistanceInMeters; + private string _assemblyInstallationReferenceRouteId; private readonly List _assemblyInstallationSeedPoints = new List(); private readonly List _assemblyEndFaceSeedPoints = new List(); private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; @@ -213,14 +214,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels { // 路径变更时清除选中的路径点 SelectedPathPoint = null; + ClearAssemblyInstallationReferenceVisuals(); // 🔧 修复:同步PathPlanningManager的CurrentRoute if (_pathPlanningManager != null && value != null) { // 查找对应的Core路径对象 - var coreRoute = _pathPlanningManager.Routes?.FirstOrDefault(r => r.Name == value.Name); + var coreRoute = _pathPlanningManager.Routes?.FirstOrDefault(r => r.Id == value.Id) + ?? _pathPlanningManager.Routes?.FirstOrDefault(r => r.Name == value.Name); if (coreRoute != null) { + if (coreRoute.PathType == PathType.Rail) + { + AssemblyMountMode = coreRoute.RailMountMode; + } + // 🔥 先设置可视化模式(在 SetCurrentRoute 之前,确保事件触发时状态已就绪) UpdatePassageSpaceVisualizationForPathType(coreRoute.PathType); @@ -270,6 +278,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(IsRailRouteSelected)); OnPropertyChanged(nameof(SelectedRailMountMode)); OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); + OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); if (!CanUsePathLines && ShowPathLines) { // 吊装和空轨路径不能使用路径线,自动关闭 @@ -346,6 +356,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels } coreRoute.RailMountMode = value; + AssemblyMountMode = value; ApplyRailRouteConfigurationChange(coreRoute, $"安装方式已更新为 {value}"); OnPropertyChanged(nameof(SelectedRailMountMode)); } @@ -378,7 +389,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels } coreRoute.RailNormalOffset = offsetInModelUnits; - ApplyRailRouteConfigurationChange(coreRoute, $"安装法向偏移已更新为 {value:F3} 米"); + if (IsAssemblyInstallationReferenceActiveForRoute(coreRoute)) + { + UpdateAssemblyInstallationAnchorFromVerticalOffset(); + PersistAssemblyInstallationReferenceToRailRoute(coreRoute, $"安装法向偏移已更新为 {value:F3} 米"); + } + else + { + ApplyRailRouteConfigurationChange(coreRoute, $"安装法向偏移已更新为 {value:F3} 米"); + } OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); } } @@ -426,9 +445,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels { if (SetProperty(ref _assemblyAnchorVerticalOffsetInMeters, value) && HasAssemblyTerminalObject) { + var coreRoute = GetSelectedCoreRoute(); if (_hasAssemblyInstallationReference) { UpdateAssemblyInstallationAnchorFromVerticalOffset(); + if (IsAssemblyInstallationReferenceActiveForRoute(coreRoute)) + { + PersistAssemblyInstallationReferenceToRailRoute(coreRoute, $"安装法向偏移已更新为 {value:F3} 米"); + } } RefreshAssemblyTerminalObjectInfo(); RenderAssemblyAnchorMarker(); @@ -543,7 +567,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (_selectedPathRoute != null && _selectedPathRoute.Points.Count > 0) { // 查找对应的Core路径对象 - var coreRoute = _pathPlanningManager?.Routes?.FirstOrDefault(r => r.Name == _selectedPathRoute.Name); + var coreRoute = _pathPlanningManager?.Routes?.FirstOrDefault(r => r.Id == _selectedPathRoute.Id) + ?? _pathPlanningManager?.Routes?.FirstOrDefault(r => r.Name == _selectedPathRoute.Name); if (coreRoute != null) { // 使用PathPlanningManager的绘制方法来保持一致性 @@ -556,6 +581,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + RefreshSelectedRailInstallationReferenceVisuals(); + // 3. 恢复网格可视化(如果网格可视化已启用且当前路径有关联的GridMap) if (_pathPlanningManager?.IsAnyGridVisualizationEnabled == true) { @@ -1074,6 +1101,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels AssemblyReferencePathManager.Instance.HasReferenceLine && !IsSelectingAssemblyStartPoint; public bool CanSelectAssemblyInstallationPoint => HasAssemblyTerminalObject && + IsRailRouteSelected && + SelectedPathRoute != null && _pathPlanningManager != null && !_isSelectingAssemblyStartPoint && !_isSelectingAssemblyEndFacePoints && @@ -1084,7 +1113,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels !IsSelectingAssemblyEndFacePoints && !_isSelectingAssemblyInstallationPoint; - public string AssemblyInstallationPointButtonText => _hasAssemblyInstallationReference + public string AssemblyInstallationPointButtonText => HasSelectedRailInstallationReference() ? "重选安装点" : "选安装点"; @@ -1219,7 +1248,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels return null; } - return _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == SelectedPathRoute.Name); + return _pathPlanningManager.Routes?.FirstOrDefault(route => route.Id == SelectedPathRoute.Id) + ?? _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == SelectedPathRoute.Name); } private void ApplyRailRouteConfigurationChange(PathRoute coreRoute, string logMessage) @@ -1234,6 +1264,115 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}"); } + private bool HasSelectedRailInstallationReference() + { + var coreRoute = GetSelectedCoreRoute(); + return coreRoute != null && + coreRoute.PathType == PathType.Rail && + coreRoute.RailPreferredNormal != null; + } + + private bool IsAssemblyInstallationReferenceActiveForRoute(PathRoute route) + { + return route != null && + route.PathType == PathType.Rail && + _hasAssemblyInstallationReference && + _assemblyInstallationAnchorPoint != null && + !string.IsNullOrWhiteSpace(_assemblyInstallationReferenceRouteId) && + string.Equals(_assemblyInstallationReferenceRouteId, route.Id, StringComparison.Ordinal); + } + + private int GetRailRouteInstallationPointIndex(PathRoute route) + { + if (route?.Points == null || route.Points.Count == 0) + { + return -1; + } + + for (int i = route.Points.Count - 1; i >= 0; i--) + { + if (route.Points[i].Type == PathPointType.EndPoint) + { + return i; + } + } + + return route.Points.Count - 1; + } + + private void PersistAssemblyInstallationReferenceToRailRoute(PathRoute route, string logMessage) + { + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + if (route.PathType != PathType.Rail) + { + throw new InvalidOperationException("只有 Rail 路径支持安装点参数更新。"); + } + + if (!_hasAssemblyInstallationReference || _assemblyInstallationAnchorPoint == null) + { + throw new InvalidOperationException("当前没有可应用的安装点参考。"); + } + + int installationPointIndex = GetRailRouteInstallationPointIndex(route); + if (installationPointIndex < 0) + { + throw new InvalidOperationException("当前 Rail 路径没有可更新的终点。"); + } + + if (!_pathPlanningManager.UpdatePathPointWithConstraints(route, installationPointIndex, _assemblyInstallationAnchorPoint)) + { + throw new InvalidOperationException("更新 Rail 路径安装点失败。"); + } + + route.RailPreferredNormal = new Point3D( + _assemblyInstallationPlaneNormal.X, + _assemblyInstallationPlaneNormal.Y, + _assemblyInstallationPlaneNormal.Z); + route.RailNormalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters); + route.LastModified = DateTime.Now; + _assemblyInstallationReferenceRouteId = route.Id; + + _pathPlanningManager.SavePathToDatabase(route); + _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false); + _pathPlanningManager.RaiseVisualizationStateChanged(); + + if (SelectedPathRoute != null && SelectedPathRoute.Id == route.Id) + { + SyncPathViewModelFromCoreRoute(SelectedPathRoute, route, preserveSelection: true); + OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); + RefreshAssemblyTerminalObjectInfo(); + RefreshSelectedRailInstallationReferenceVisuals(); + } + + LogManager.Info($"[Rail构型] {route.Name}: {logMessage}"); + } + + private void RefreshSelectedRailInstallationReferenceVisuals() + { + if (!_isSelectingAssemblyInstallationPoint) + { + ClearAssemblyInstallationReferenceVisuals(); + return; + } + + var coreRoute = GetSelectedCoreRoute(); + if (!IsAssemblyInstallationReferenceActiveForRoute(coreRoute)) + { + ClearAssemblyInstallationReferenceVisuals(); + return; + } + + RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints); + RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint); + RenderAssemblyInstallationCenterLine(_assemblyInstallationPickPoint, GetCurrentAssemblyOpticalAxisDirection()); + RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection); + } + private void AdjustSelectedRailNormalOffset(double deltaInMeters) { var coreRoute = GetSelectedCoreRoute(); @@ -1564,6 +1703,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。"); } + var selectedRailRoute = GetSelectedCoreRoute(); + if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail) + { + throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。"); + } + + AssemblyMountMode = selectedRailRoute.RailMountMode; + CleanupAssemblyReferenceSelection(); CleanupAssemblyEndFaceSelection(clearVisuals: false); CleanupAssemblyInstallationSelection(); @@ -1737,6 +1884,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels BuildAndRenderAssemblyInstallationReference( _assemblyInstallationSeedPoints[0], _assemblyInstallationSeedPoints[1]); + var selectedRailRoute = GetSelectedCoreRoute(); + if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail) + { + PersistAssemblyInstallationReferenceToRailRoute( + selectedRailRoute, + "已重选安装点并同步更新 Rail 路径安装参数"); + } CleanupAssemblyInstallationSelection(); }, "处理安装点拾取"); } @@ -1890,8 +2044,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels _assemblyInstallationBaseAnchorPoint.Y + _assemblyInstallationPlaneNormal.Y * (float)offset, _assemblyInstallationBaseAnchorPoint.Z + _assemblyInstallationPlaneNormal.Z * (float)offset); - RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint); - RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection); + if (_isSelectingAssemblyInstallationPoint) + { + RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint); + RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection); + } + else + { + ClearAssemblyInstallationReferenceVisuals(); + } } private Vector3 GetCurrentAssemblyOpticalAxisDirection() @@ -2600,6 +2761,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels _assemblyInstallationPlaneNormal = default(Vector3); _assemblyInstallationPlaneSpanDirection = default(Vector3); _assemblyInstallationOffsetDistanceInMeters = 0.0; + _assemblyInstallationReferenceRouteId = null; _assemblyInstallationSeedPoints.Clear(); _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); @@ -2631,6 +2793,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels _isSelectingAssemblyInstallationPoint = false; _pathPlanningManager?.EnableMouseHandling(); _pathPlanningManager?.StopClickTool(); + ClearAssemblyInstallationReferenceVisuals(); OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 84e645a..b9c1eac 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -522,6 +522,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 + + private void OnCopyClick(object sender, RoutedEventArgs e) + { + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", X, Y, Z); + bool success = ClipboardHelper.TrySetText(coordinate, "编辑路径点坐标"); + ShowButtonFeedback(CopyButton, success); + } + /// /// 粘贴坐标 /// diff --git a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs index cd83161..f6591a6 100644 --- a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs +++ b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs @@ -192,16 +192,9 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyCenterClick(object sender, RoutedEventArgs e) { - try - { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", CenterX, CenterY, CenterZ); - System.Windows.Clipboard.SetText(coordinate); - ShowButtonFeedback(CopyCenterButton); - } - catch (Exception ex) - { - LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}"); - } + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", CenterX, CenterY, CenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyCenterButton, success); } /// @@ -209,16 +202,9 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyTopCenterClick(object sender, RoutedEventArgs e) { - try - { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ); - System.Windows.Clipboard.SetText(coordinate); - ShowButtonFeedback(CopyTopButton); - } - catch (Exception ex) - { - LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}"); - } + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyTopButton, success); } /// @@ -226,33 +212,32 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyBottomCenterClick(object sender, RoutedEventArgs e) { - try - { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ); - System.Windows.Clipboard.SetText(coordinate); - ShowButtonFeedback(CopyBottomButton); - } - catch (Exception ex) - { - LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}"); - } + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyBottomButton, success); } /// - /// 显示按钮点击反馈(绿色背景闪烁) + /// 显示按钮点击反馈 /// - private void ShowButtonFeedback(Button button) + private void ShowButtonFeedback(Button button, bool success) { if (button == null) return; var originalBackground = button.Background; var originalBorderBrush = button.BorderBrush; - // 变为绿色表示成功 - button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(76, 175, 80)); - button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 142, 60)); + if (success) + { + button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(76, 175, 80)); + button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 142, 60)); + } + else + { + button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(244, 67, 54)); + button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(211, 47, 47)); + } - // 800毫秒后恢复 var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(800) }; timer.Tick += (s, e) => { diff --git a/src/Utils/ClipboardHelper.cs b/src/Utils/ClipboardHelper.cs new file mode 100644 index 0000000..c6fed38 --- /dev/null +++ b/src/Utils/ClipboardHelper.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Windows; + +namespace NavisworksTransport +{ + /// + /// 剪贴板工具方法 + /// + public static class ClipboardHelper + { + private const int MaxAttempts = 6; + private const int RetryDelayMilliseconds = 80; + + /// + /// 尝试写入文本到剪贴板 + /// + public static bool TrySetText(string text, string context = null) + { + string clipboardText = text ?? string.Empty; + Exception lastException = null; + + for (int attempt = 0; attempt < MaxAttempts; attempt++) + { + if (TrySetTextOnStaThread(clipboardText, out lastException)) + { + return true; + } + + if (attempt < MaxAttempts - 1) + { + Thread.Sleep(RetryDelayMilliseconds); + } + } + + string prefix = string.IsNullOrWhiteSpace(context) ? "[剪贴板]" : $"[{context}]"; + LogManager.Debug($"{prefix} 写入剪贴板失败: {lastException?.Message ?? "未知错误"}"); + return false; + } + + private static bool TrySetTextOnStaThread(string text, out Exception exception) + { + bool success = false; + Exception capturedException = null; + + Thread thread = new Thread(() => + { + try + { + Clipboard.SetDataObject(text, true); + success = true; + } + catch (Exception ex) + { + capturedException = ex; + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.IsBackground = true; + thread.Start(); + thread.Join(); + + exception = capturedException; + return success; + } + } +} From 382923fa325b8a04d882f3b1397827c1cfc7658e Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sun, 29 Mar 2026 01:17:06 +0800 Subject: [PATCH 43/87] Relax fragment pose parsing and add fallback --- .../RealObjectReferencePoseResolverTests.cs | 27 ++++++ src/Core/Animation/PathAnimationManager.cs | 93 ++++++++++++++----- .../RealObjectReferencePoseResolver.cs | 86 +++++++++-------- 3 files changed, 144 insertions(+), 62 deletions(-) diff --git a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs index 5d6d355..0df379c 100644 --- a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs +++ b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs @@ -91,6 +91,33 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.AreEqual(1.0, zAlignment, 1e-6); } + [TestMethod] + public void YUp_WorldAxisMapping_ShouldAcceptSlightlyRotatedRawFragmentFrame() + { + var fragmentMatrices = new List + { + CreateMatrix( + axisX: Vector3.Normalize(new Vector3(0.9980f, 0.0638f, 0.0f)), + axisY: new Vector3(0.0f, 0.0f, -1.0f), + axisZ: Vector3.Normalize(new Vector3(-0.0638f, 0.9980f, 0.0f))) + }; + + bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices( + fragmentMatrices, + fragmentDefaultUpAxis: "Y", + hostUpAxis: "Y", + out RealObjectReferencePose pose); + + Assert.IsTrue(ok); + Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostWorldXAxisLocalAxis); + Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostWorldYAxisLocalAxis); + Assert.AreEqual(LocalAxisDirection.NegativeY, pose.HostWorldZAxisLocalAxis); + Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostSemanticXAxisLocalAxis); + Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostSemanticYAxisLocalAxis); + Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostSemanticZAxisLocalAxis); + Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostUpLocalAxis); + } + private static double[] CreateMatrix(Vector3 axisX, Vector3 axisY, Vector3 axisZ) { return new double[] diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 2c87bc5..3fbc733 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -4764,35 +4764,15 @@ namespace NavisworksTransport.Core.Animation return true; } - NotifyRealObjectReferencePoseFailure(_animatedObject); - return false; + return TryCaptureFallbackRealObjectReferenceRotation(_animatedObject, out referenceRotation); } catch (Exception ex) { - LogManager.Error($"[真实物体参考姿态] 读取原始参考位姿失败: {ex.Message}"); - return false; + LogManager.Warning($"[真实物体参考姿态] 读取 fragment 参考位姿失败,改用默认姿态: {ex.Message}"); + return TryCaptureFallbackRealObjectReferenceRotation(_animatedObject, out referenceRotation); } } - 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; @@ -4845,6 +4825,73 @@ namespace NavisworksTransport.Core.Animation } } + private bool TryCaptureFallbackRealObjectReferenceRotation(ModelItem sourceObject, out Quaternion referenceRotation) + { + referenceRotation = Quaternion.Identity; + + if (sourceObject == null) + { + return false; + } + + try + { + Rotation3D fallbackRotation3D = sourceObject.Transform?.Factor().Rotation ?? Rotation3D.Identity; + referenceRotation = new Quaternion( + (float)fallbackRotation3D.A, + (float)fallbackRotation3D.B, + (float)fallbackRotation3D.C, + (float)fallbackRotation3D.D); + + if (referenceRotation.LengthSquared() < 1e-6f) + { + referenceRotation = Quaternion.Identity; + } + else + { + referenceRotation = Quaternion.Normalize(referenceRotation); + } + + _realObjectReferenceRotation = referenceRotation; + _realObjectReferenceAxisX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, referenceRotation)); + _realObjectReferenceAxisY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, referenceRotation)); + _realObjectReferenceAxisZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, referenceRotation)); + _realObjectReferenceHostWorldXAxisLocalAxis = LocalAxisDirection.PositiveX; + _realObjectReferenceHostWorldYAxisLocalAxis = LocalAxisDirection.PositiveY; + _realObjectReferenceHostWorldZAxisLocalAxis = LocalAxisDirection.PositiveZ; + _realObjectReferenceHostSemanticXAxisLocalAxis = LocalAxisDirection.PositiveX; + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + if (adapter.HostType == CoordinateSystemType.YUp) + { + _realObjectReferenceHostSemanticYAxisLocalAxis = LocalAxisDirection.PositiveY; + _realObjectReferenceHostSemanticZAxisLocalAxis = LocalAxisDirection.PositiveZ; + _realObjectReferenceHostUpLocalAxis = LocalAxisDirection.PositiveY; + } + else + { + _realObjectReferenceHostSemanticYAxisLocalAxis = LocalAxisDirection.PositiveY; + _realObjectReferenceHostSemanticZAxisLocalAxis = LocalAxisDirection.PositiveZ; + _realObjectReferenceHostUpLocalAxis = LocalAxisDirection.PositiveZ; + } + + _hasRealObjectReferenceRotation = true; + + LogManager.Warning( + $"[真实物体参考姿态] {sourceObject.DisplayName} fragment 姿态不可用,已退回默认参考姿态继续业务流程: " + + $"FallbackX=({_realObjectReferenceAxisX.X:F4},{_realObjectReferenceAxisX.Y:F4},{_realObjectReferenceAxisX.Z:F4}), " + + $"FallbackY=({_realObjectReferenceAxisY.X:F4},{_realObjectReferenceAxisY.Y:F4},{_realObjectReferenceAxisY.Z:F4}), " + + $"FallbackZ=({_realObjectReferenceAxisZ.X:F4},{_realObjectReferenceAxisZ.Y:F4},{_realObjectReferenceAxisZ.Z:F4}), " + + $"宿主Up对应本地轴={_realObjectReferenceHostUpLocalAxis}"); + return true; + } + catch (Exception ex) + { + LogManager.Error($"[真实物体参考姿态] {sourceObject.DisplayName} 默认参考姿态回退失败: {ex.Message}"); + return false; + } + } + private void SyncTrackedRotationToObjectReference(ModelItem sourceObject, bool isVirtualObject) { if (sourceObject == null) diff --git a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs index 767daf4..b21dfa4 100644 --- a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs +++ b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs @@ -169,17 +169,35 @@ namespace NavisworksTransport.Utils.CoordinateSystem hostWorldYAxisLocalAxis = LocalAxisDirection.PositiveY; hostWorldZAxisLocalAxis = LocalAxisDirection.PositiveZ; - if (!TryMatchRawAxisDirection(Vector3.UnitX, rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, out hostWorldXAxisLocalAxis)) + if (!TryMatchRawAxisDirection( + Vector3.UnitX, + rawFrame.AxisX, + rawFrame.AxisY, + rawFrame.AxisZ, + 0.5f, + out hostWorldXAxisLocalAxis)) { return false; } - if (!TryMatchRawAxisDirection(Vector3.UnitY, rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, out hostWorldYAxisLocalAxis)) + if (!TryMatchRawAxisDirection( + Vector3.UnitY, + rawFrame.AxisX, + rawFrame.AxisY, + rawFrame.AxisZ, + 0.5f, + out hostWorldYAxisLocalAxis)) { return false; } - if (!TryMatchRawAxisDirection(Vector3.UnitZ, rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, out hostWorldZAxisLocalAxis)) + if (!TryMatchRawAxisDirection( + Vector3.UnitZ, + rawFrame.AxisX, + rawFrame.AxisY, + rawFrame.AxisZ, + 0.5f, + out hostWorldZAxisLocalAxis)) { return false; } @@ -203,6 +221,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, + 0.95f, out hostSemanticXAxisLocalAxis)) { return false; @@ -213,6 +232,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, + 0.95f, out hostSemanticYAxisLocalAxis)) { return false; @@ -223,6 +243,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, + 0.95f, out hostSemanticZAxisLocalAxis)) { return false; @@ -236,48 +257,35 @@ namespace NavisworksTransport.Utils.CoordinateSystem Vector3 rawAxisX, Vector3 rawAxisY, Vector3 rawAxisZ, + float minimumAlignment, out LocalAxisDirection axisDirection) { axisDirection = LocalAxisDirection.PositiveX; - const float tolerance = 1e-4f; + float bestAlignment = float.NegativeInfinity; - if (Vector3.Distance(targetAxis, rawAxisX) < tolerance) + TryUpdateBestMatch(targetAxis, rawAxisX, LocalAxisDirection.PositiveX, ref bestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, -rawAxisX, LocalAxisDirection.NegativeX, ref bestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, rawAxisY, LocalAxisDirection.PositiveY, ref bestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, -rawAxisY, LocalAxisDirection.NegativeY, ref bestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, rawAxisZ, LocalAxisDirection.PositiveZ, ref bestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, -rawAxisZ, LocalAxisDirection.NegativeZ, ref bestAlignment, ref axisDirection); + + return bestAlignment >= minimumAlignment; + } + + private static void TryUpdateBestMatch( + Vector3 targetAxis, + Vector3 candidateAxis, + LocalAxisDirection candidateDirection, + ref float bestAlignment, + ref LocalAxisDirection bestDirection) + { + float alignment = Vector3.Dot(Vector3.Normalize(targetAxis), Vector3.Normalize(candidateAxis)); + if (alignment > bestAlignment) { - axisDirection = LocalAxisDirection.PositiveX; - return true; + bestAlignment = alignment; + bestDirection = candidateDirection; } - - if (Vector3.Distance(targetAxis, -rawAxisX) < tolerance) - { - axisDirection = LocalAxisDirection.NegativeX; - return true; - } - - if (Vector3.Distance(targetAxis, rawAxisY) < tolerance) - { - axisDirection = LocalAxisDirection.PositiveY; - return true; - } - - if (Vector3.Distance(targetAxis, -rawAxisY) < tolerance) - { - axisDirection = LocalAxisDirection.NegativeY; - return true; - } - - if (Vector3.Distance(targetAxis, rawAxisZ) < tolerance) - { - axisDirection = LocalAxisDirection.PositiveZ; - return true; - } - - if (Vector3.Distance(targetAxis, -rawAxisZ) < tolerance) - { - axisDirection = LocalAxisDirection.NegativeZ; - return true; - } - - return false; } public static bool TryDetectDefaultUpAxis( From 766b5af887f53910b96060aa9c0d15abe60df258 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sun, 29 Mar 2026 18:28:41 +0800 Subject: [PATCH 44/87] Stop auto-switching to path view before animation playback --- .../ViewModels/AnimationControlViewModel.cs | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 242fcd0..5b7049f 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -1500,25 +1500,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels { try { - // 自动调整视角到路径中心 - try - { - var coreRoute = CurrentPathRoute?.Route; - if (coreRoute != null) - { - ViewpointHelper.AdjustViewpointToPathCenter(coreRoute); - LogManager.Info($"正向播放前:已自动调整视角到路径中心: {CurrentPathRoute.Name}"); - } - else - { - LogManager.Warning($"正向播放前:CurrentPathRoute.Route 为 null"); - } - } - catch (Exception ex) - { - LogManager.Error($"正向播放前:调整视角失败: {ex.Message}"); - } - _pathAnimationManager?.PlayForward(); UpdateMediaControlProperties(); LogManager.Info("执行正向播放命令"); @@ -1537,25 +1518,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels { try { - // 自动调整视角到路径中心 - try - { - var coreRoute = CurrentPathRoute?.Route; - if (coreRoute != null) - { - ViewpointHelper.AdjustViewpointToPathCenter(coreRoute); - LogManager.Info($"反向播放前:已自动调整视角到路径中心: {CurrentPathRoute.Name}"); - } - else - { - LogManager.Warning($"反向播放前:CurrentPathRoute.Route 为 null"); - } - } - catch (Exception ex) - { - LogManager.Error($"反向播放前:调整视角失败: {ex.Message}"); - } - _pathAnimationManager?.PlayReverse(); UpdateMediaControlProperties(); LogManager.Info("执行反向播放命令"); From 909337439995e04d326aaf92d9d3a86a84c86f67 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sun, 29 Mar 2026 23:36:43 +0800 Subject: [PATCH 45/87] Use actual hoisting pose baseline for real objects --- NavisworksTransport.UnitTests.csproj | 1 + TransportPlugin.csproj | 1 + .../HoistingRealObjectPoseHelperTests.cs | 73 ++++++++++ .../RealObjectReferencePoseResolverTests.cs | 21 +++ src/Core/Animation/PathAnimationManager.cs | 64 +++++++++ .../HoistingRealObjectPoseHelper.cs | 135 ++++++++++++++++++ .../RealObjectReferencePoseResolver.cs | 38 +++-- 7 files changed, 320 insertions(+), 13 deletions(-) create mode 100644 UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs create mode 100644 src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index aa14ce5..4fefe8e 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -65,6 +65,7 @@ + diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index a697ca9..bc517ad 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -345,6 +345,7 @@ + diff --git a/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs b/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs new file mode 100644 index 0000000..90ca32f --- /dev/null +++ b/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class HoistingRealObjectPoseHelperTests + { + [TestMethod] + public void ShouldKeepActualUpAxis_WhenAligningHoistingForward() + { + Quaternion actualRotation = Quaternion.Normalize(new Quaternion(0.70710677f, 0.0f, 0.0f, 0.70710677f)); + Vector3 hostUp = Vector3.UnitY; + Vector3 targetForward = -Vector3.UnitX; + + bool ok = HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose( + actualRotation, + targetForward, + hostUp, + out Quaternion resultRotation, + out LocalAxisDirection selectedDirection, + out Vector3 selectedAxisLocal, + out Vector3 projectedForward); + + Assert.IsTrue(ok); + Assert.AreEqual(LocalAxisDirection.PositiveX, selectedDirection); + AssertVector(selectedAxisLocal, 1.0, 0.0, 0.0); + AssertVector(projectedForward, 1.0, 0.0, 0.0); + + Vector3 transformedUp = Vector3.Normalize(Vector3.Transform(-Vector3.UnitZ, resultRotation)); + AssertVector(transformedUp, 0.0, 1.0, 0.0, 1e-4); + + Vector3 transformedForward = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, resultRotation)); + AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4); + } + + [TestMethod] + public void ShouldChooseHorizontalAxisFromActualPose_ForHoisting() + { + Quaternion actualRotation = Quaternion.Normalize(new Quaternion(0.70710677f, 0.0f, 0.0f, 0.70710677f)); + Vector3 hostUp = Vector3.UnitY; + Vector3 targetForward = Vector3.UnitX; + + bool ok = HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose( + actualRotation, + targetForward, + hostUp, + out Quaternion resultRotation, + out LocalAxisDirection selectedDirection, + out Vector3 selectedAxisLocal, + out Vector3 projectedForward); + + Assert.IsTrue(ok); + Assert.AreEqual(LocalAxisDirection.PositiveX, selectedDirection); + AssertVector(selectedAxisLocal, 1.0, 0.0, 0.0); + AssertVector(projectedForward, 1.0, 0.0, 0.0); + + Vector3 transformedUp = Vector3.Normalize(Vector3.Transform(-Vector3.UnitZ, resultRotation)); + AssertVector(transformedUp, 0.0, 1.0, 0.0, 1e-4); + + Vector3 transformedForward = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, resultRotation)); + AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4); + } + + private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6) + { + Assert.AreEqual(x, actual.X, tolerance); + Assert.AreEqual(y, actual.Y, tolerance); + Assert.AreEqual(z, actual.Z, tolerance); + } + } +} diff --git a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs index 0df379c..0faa079 100644 --- a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs +++ b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs @@ -118,6 +118,27 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostUpLocalAxis); } + [TestMethod] + public void YUp_WorldAxisMapping_ShouldRejectAmbiguousRawFragmentFrame() + { + var fragmentMatrices = new List + { + CreateMatrix( + axisX: Vector3.Normalize(new Vector3(0.80f, 0.60f, 0.0f)), + axisY: new Vector3(0.0f, 0.0f, -1.0f), + axisZ: Vector3.Normalize(new Vector3(-0.60f, 0.80f, 0.0f))) + }; + + bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices( + fragmentMatrices, + fragmentDefaultUpAxis: "Y", + hostUpAxis: "Y", + out RealObjectReferencePose pose); + + Assert.IsFalse(ok); + Assert.IsNull(pose); + } + private static double[] CreateMatrix(Vector3 axisX, Vector3 axisY, Vector3 axisZ) { return new double[] diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 3fbc733..8ac0584 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -4609,6 +4609,12 @@ namespace NavisworksTransport.Core.Animation { rotation = Rotation3D.Identity; + if (_route?.PathType == PathType.Hoisting && + TryCreateHoistingRealObjectRotationFromActualPose(hostForward, out rotation)) + { + return true; + } + if (!PathTargetFrameResolver.TryCreatePlanarHostFrame( hostForward, CoordinateSystemManager.Instance.CreateHostAdapter().HostType, @@ -4646,6 +4652,64 @@ namespace NavisworksTransport.Core.Animation return true; } + private bool TryCreateHoistingRealObjectRotationFromActualPose(Vector3 hostForward, out Rotation3D rotation) + { + rotation = Rotation3D.Identity; + + if (_animatedObject == null || + !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D)) + { + return false; + } + + var actualRotation = new Quaternion( + (float)actualRotation3D.A, + (float)actualRotation3D.B, + (float)actualRotation3D.C, + (float)actualRotation3D.D); + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ; + if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose( + actualRotation, + hostForward, + hostUp, + out var baselineQuaternion, + out var selectedAxisDirection, + out var selectedAxisLocal, + out var projectedForward)) + { + return false; + } + + rotation = adapter.FromHostQuaternionDirect( + adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection)); + + Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion); + Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion( + (float)rotation.A, + (float)rotation.B, + (float)rotation.C, + (float)rotation.D)); + + _realObjectPlanarSelectedForwardAxis = selectedAxisDirection; + _hasRealObjectPlanarSelectedForwardAxis = true; + + LogManager.Info( + $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。"); + LogManager.Info( + $"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " + + $"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " + + $"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " + + $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " + + $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " + + $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " + + $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " + + $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " + + $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})"); + return true; + } + private bool TryCreateRealObjectPlanarPoseSolution( Vector3 targetForward, Vector3 targetUp, diff --git a/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs b/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs new file mode 100644 index 0000000..bd23473 --- /dev/null +++ b/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs @@ -0,0 +1,135 @@ +using System; +using System.Numerics; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + public static class HoistingRealObjectPoseHelper + { + public static bool TryCreateRotationFromActualPose( + Quaternion actualRotation, + Vector3 targetForward, + Vector3 hostUp, + out Quaternion resultRotation, + out LocalAxisDirection selectedForwardAxisDirection, + out Vector3 selectedForwardAxisLocal, + out Vector3 projectedForward) + { + resultRotation = Quaternion.Identity; + selectedForwardAxisDirection = LocalAxisDirection.PositiveX; + selectedForwardAxisLocal = Vector3.UnitX; + projectedForward = Vector3.Zero; + + if (actualRotation.LengthSquared() < 1e-6f) + { + return false; + } + + Vector3 normalizedHostUp = NormalizeSafe(hostUp); + if (normalizedHostUp.LengthSquared() < 1e-6f) + { + return false; + } + + Vector3 normalizedTargetForward = NormalizeOnPlane(targetForward, normalizedHostUp); + if (normalizedTargetForward.LengthSquared() < 1e-6f) + { + return false; + } + + Quaternion normalizedActualRotation = Quaternion.Normalize(actualRotation); + Vector3[] localAxes = + { + Vector3.UnitX, + Vector3.UnitY, + Vector3.UnitZ + }; + LocalAxisDirection[] directions = + { + LocalAxisDirection.PositiveX, + LocalAxisDirection.PositiveY, + LocalAxisDirection.PositiveZ + }; + + float bestScore = float.NegativeInfinity; + Vector3 bestProjectedAxis = Vector3.Zero; + + for (int i = 0; i < localAxes.Length; i++) + { + Vector3 worldAxis = Vector3.Transform(localAxes[i], normalizedActualRotation); + float upAlignment = Math.Abs(Vector3.Dot(Vector3.Normalize(worldAxis), normalizedHostUp)); + if (upAlignment > 0.9f) + { + continue; + } + + Vector3 projectedAxis = NormalizeOnPlane(worldAxis, normalizedHostUp); + if (projectedAxis.LengthSquared() < 1e-6f) + { + continue; + } + + float score = Math.Abs(Vector3.Dot(projectedAxis, normalizedTargetForward)); + if (score > bestScore) + { + bestScore = score; + bestProjectedAxis = projectedAxis; + selectedForwardAxisDirection = directions[i]; + selectedForwardAxisLocal = localAxes[i]; + } + } + + if (bestScore == float.NegativeInfinity) + { + return false; + } + + Vector3 resolvedForward = Vector3.Dot(bestProjectedAxis, normalizedTargetForward) >= 0.0f + ? normalizedTargetForward + : -normalizedTargetForward; + + projectedForward = resolvedForward; + float signedAngle = SignedAngleAroundAxis(bestProjectedAxis, resolvedForward, normalizedHostUp); + Quaternion deltaRotation = Quaternion.CreateFromAxisAngle(normalizedHostUp, signedAngle); + resultRotation = Quaternion.Normalize(deltaRotation * normalizedActualRotation); + return true; + } + + private static Vector3 NormalizeOnPlane(Vector3 vector, Vector3 planeNormal) + { + Vector3 planar = vector - Vector3.Dot(vector, planeNormal) * planeNormal; + return NormalizeSafe(planar); + } + + private static Vector3 NormalizeSafe(Vector3 vector) + { + if (vector.LengthSquared() < 1e-6f) + { + return Vector3.Zero; + } + + return Vector3.Normalize(vector); + } + + private static float SignedAngleAroundAxis(Vector3 from, Vector3 to, Vector3 axis) + { + float dot = Vector3.Dot(from, to); + if (dot > 1.0f) + { + dot = 1.0f; + } + else if (dot < -1.0f) + { + dot = -1.0f; + } + + float angle = (float)Math.Acos(dot); + float sign = Math.Sign(Vector3.Dot(axis, Vector3.Cross(from, to))); + if (Math.Abs(sign) < 1e-6f) + { + sign = 1.0f; + } + + return angle * sign; + } + } +} diff --git a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs index b21dfa4..13ab441 100644 --- a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs +++ b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs @@ -17,6 +17,10 @@ namespace NavisworksTransport.Utils.CoordinateSystem /// public static class RealObjectReferencePoseResolver { + private const float WorldAxisMinimumAlignment = 0.98f; + private const float SemanticAxisMinimumAlignment = 0.985f; + private const float MinimumBestToSecondGap = 0.02f; + public static bool TryResolveFromModelItem( ModelItem sourceObject, Document doc, @@ -174,7 +178,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, - 0.5f, + WorldAxisMinimumAlignment, out hostWorldXAxisLocalAxis)) { return false; @@ -185,7 +189,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, - 0.5f, + WorldAxisMinimumAlignment, out hostWorldYAxisLocalAxis)) { return false; @@ -196,7 +200,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, - 0.5f, + WorldAxisMinimumAlignment, out hostWorldZAxisLocalAxis)) { return false; @@ -221,7 +225,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, - 0.95f, + SemanticAxisMinimumAlignment, out hostSemanticXAxisLocalAxis)) { return false; @@ -232,7 +236,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, - 0.95f, + SemanticAxisMinimumAlignment, out hostSemanticYAxisLocalAxis)) { return false; @@ -243,7 +247,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, - 0.95f, + SemanticAxisMinimumAlignment, out hostSemanticZAxisLocalAxis)) { return false; @@ -262,15 +266,17 @@ namespace NavisworksTransport.Utils.CoordinateSystem { axisDirection = LocalAxisDirection.PositiveX; float bestAlignment = float.NegativeInfinity; + float secondBestAlignment = float.NegativeInfinity; - TryUpdateBestMatch(targetAxis, rawAxisX, LocalAxisDirection.PositiveX, ref bestAlignment, ref axisDirection); - TryUpdateBestMatch(targetAxis, -rawAxisX, LocalAxisDirection.NegativeX, ref bestAlignment, ref axisDirection); - TryUpdateBestMatch(targetAxis, rawAxisY, LocalAxisDirection.PositiveY, ref bestAlignment, ref axisDirection); - TryUpdateBestMatch(targetAxis, -rawAxisY, LocalAxisDirection.NegativeY, ref bestAlignment, ref axisDirection); - TryUpdateBestMatch(targetAxis, rawAxisZ, LocalAxisDirection.PositiveZ, ref bestAlignment, ref axisDirection); - TryUpdateBestMatch(targetAxis, -rawAxisZ, LocalAxisDirection.NegativeZ, ref bestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, rawAxisX, LocalAxisDirection.PositiveX, ref bestAlignment, ref secondBestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, -rawAxisX, LocalAxisDirection.NegativeX, ref bestAlignment, ref secondBestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, rawAxisY, LocalAxisDirection.PositiveY, ref bestAlignment, ref secondBestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, -rawAxisY, LocalAxisDirection.NegativeY, ref bestAlignment, ref secondBestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, rawAxisZ, LocalAxisDirection.PositiveZ, ref bestAlignment, ref secondBestAlignment, ref axisDirection); + TryUpdateBestMatch(targetAxis, -rawAxisZ, LocalAxisDirection.NegativeZ, ref bestAlignment, ref secondBestAlignment, ref axisDirection); - return bestAlignment >= minimumAlignment; + return bestAlignment >= minimumAlignment + && bestAlignment - secondBestAlignment >= MinimumBestToSecondGap; } private static void TryUpdateBestMatch( @@ -278,14 +284,20 @@ namespace NavisworksTransport.Utils.CoordinateSystem Vector3 candidateAxis, LocalAxisDirection candidateDirection, ref float bestAlignment, + ref float secondBestAlignment, ref LocalAxisDirection bestDirection) { float alignment = Vector3.Dot(Vector3.Normalize(targetAxis), Vector3.Normalize(candidateAxis)); if (alignment > bestAlignment) { + secondBestAlignment = bestAlignment; bestAlignment = alignment; bestDirection = candidateDirection; } + else if (alignment > secondBestAlignment) + { + secondBestAlignment = alignment; + } } public static bool TryDetectDefaultUpAxis( From 1d71d36e5fb1195ada95add7d2777d787ad5eda4 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 30 Mar 2026 22:13:16 +0800 Subject: [PATCH 46/87] =?UTF-8?q?=E4=BF=AE=E5=A4=8Drail=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E7=9A=84=E7=BC=96=E8=BE=91=E7=8A=B6=E6=80=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 44 ++++++ src/UI/WPF/ViewModels/PathEditingViewModel.cs | 138 ++++++++++++++++-- src/UI/WPF/Views/PathEditingView.xaml | 4 +- src/Utils/ModelItemTransformHelper.cs | 4 +- 4 files changed, 173 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b58d67a..0bc6113 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -133,6 +133,50 @@ 禁止再使用“本地坐标系”这种含糊说法。 +### 4.1.2 坐标命名规则 + +以后凡是变量名、字段名、日志名里出现 `X/Y/Z`、正负轴、forward/up/side 等方向语义,必须在名字上直接带出所属坐标系,避免只看名字时无法判断语义层。 + +允许的前缀示例: + +- `Host...` + - 宿主坐标系 +- `Canonical...` + - 内部坐标系 +- `Asset...` + - 资产坐标系 +- `Local...` + - 仅当这里明确表示“对象自身局部轴”时才允许使用 + +正负轴命名示例: + +- `LocalPositiveX` +- `LocalNegativeY` +- `HostPositiveZ` +- `CanonicalPositiveX` +- `AssetPositiveY` + +方向/向量命名示例: + +- `hostForward` +- `hostUp` +- `canonicalForward` +- `assetUp` +- `localXAxis` +- `localZAxis` + +禁止继续使用这类脱离坐标系语义的名字: + +- `PositiveX` +- `NegativeZ` +- `xAxis` +- `upAxis` +- `forwardAxis` + +除非变量名里已经明确出现了所属坐标系前缀。 + +目标是做到:只看名字,就能知道这个方向、轴、向量到底属于宿主、内部、资产,还是对象自身局部轴。 + ### 4.1.1 Quaternion / Rotation3D 固定定义 这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口: diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 35213ad..a650edd 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -11,7 +11,6 @@ using System.IO; using System.Numerics; using Microsoft.Win32; using Autodesk.Navisworks.Api; -using Autodesk.Navisworks.Api.Plugins; using NavisApplication = Autodesk.Navisworks.Api.Application; using NavisworksTransport.Core; using NavisworksTransport.Core.Config; @@ -21,7 +20,6 @@ using NavisworksTransport.UI.WPF.Models; using NavisworksTransport.Utils; using NavisworksTransport.Utils.CoordinateSystem; using NavisworksTransport.Utils.GeometryAnalysis; -using NavisworksTransport; namespace NavisworksTransport.UI.WPF.ViewModels { @@ -145,6 +143,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels private bool _isEditingSelectedRailStartPoint; private bool _isSelectingAssemblyEndFacePoints; private bool _isSelectingAssemblyInstallationPoint; + private bool _isEditingSelectedRailInstallationPoint; + private bool _isEditingSelectedRail; private bool _hasAssemblyEndFaceAnalysis; private bool _hasAssemblyInstallationReference; private ModelItem _assemblyTerminalObject; @@ -218,6 +218,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels ClearAssemblyReferenceLineVisuals(); ClearAssemblyInstallationReferenceVisuals(); + // 重置 Rail 编辑状态 + _isEditingSelectedRail = false; + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); + // 🔧 修复:同步PathPlanningManager的CurrentRoute if (_pathPlanningManager != null && value != null) { @@ -1066,6 +1070,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand ExportPathCommand { get; private set; } public ICommand SaveAsPathCommand { get; private set; } public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; } + public ICommand ReCaptureAssemblyTerminalObjectCommand { get; private set; } public ICommand GenerateAssemblyReferenceRodCommand { get; private set; } public ICommand SelectAssemblyStartPointCommand { get; private set; } public ICommand RepositionRailStartPointCommand { get; private set; } @@ -1291,6 +1296,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels private bool HasSelectedRailInstallationReference() { + // 只有在点击"重选箱体"进入编辑状态后才显示"重选安装点" + if (!_isEditingSelectedRail) + { + return false; + } + var coreRoute = GetSelectedCoreRoute(); return coreRoute != null && coreRoute.PathType == PathType.Rail && @@ -1376,11 +1387,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels return; } - var coreRoute = GetSelectedCoreRoute(); - if (!TryBuildRailRouteReferenceLine(coreRoute, out AssemblyReferenceLine referenceLine)) + AssemblyReferenceLine referenceLine; + + if (_isEditingSelectedRail) { - ClearAssemblyReferenceLineVisuals(); - return; + // 编辑状态:使用默认杆长度构建参考线,方便选择更远的起点 + try + { + referenceLine = BuildAssemblyReferenceLine(); + } + catch + { + ClearAssemblyReferenceLineVisuals(); + return; + } + } + else + { + // 非编辑状态:使用路径实际长度 + var coreRoute = GetSelectedCoreRoute(); + if (!TryBuildRailRouteReferenceLine(coreRoute, out referenceLine)) + { + ClearAssemblyReferenceLineVisuals(); + return; + } } AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod( @@ -1422,11 +1452,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("当前 Rail 路径没有可更新的终点。"); } + // 获取当前起点索引 + int startPointIndex = GetRailRouteStartPointIndex(route); + if (startPointIndex < 0) + { + throw new InvalidOperationException("当前 Rail 路径没有可更新的起点。"); + } + + // 计算原路径长度(起点到终点的距离) + Point3D currentStartPoint = route.Points[startPointIndex].Position; + Point3D currentEndPoint = route.Points[installationPointIndex].Position; + double pathLength = GeometryHelper.CalculatePointDistance(currentStartPoint, currentEndPoint); + + // 更新终点(安装点) if (!_pathPlanningManager.UpdatePathPointWithConstraints(route, installationPointIndex, _assemblyInstallationAnchorPoint)) { throw new InvalidOperationException("更新 Rail 路径安装点失败。"); } + // 根据新终点和原路径长度重新计算起点位置 + Point3D newStartPoint = CalculateRailStartPointFromEndPoint(_assemblyInstallationAnchorPoint, pathLength); + if (!_pathPlanningManager.UpdatePathPointWithConstraints(route, startPointIndex, newStartPoint)) + { + throw new InvalidOperationException("更新 Rail 路径起点失败。"); + } + route.RailPreferredNormal = new Point3D( _assemblyInstallationPlaneNormal.X, _assemblyInstallationPlaneNormal.Y, @@ -1449,7 +1499,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels RefreshSelectedRailReferenceLineVisuals(); } - LogManager.Info($"[Rail构型] {route.Name}: {logMessage}"); + LogManager.Info($"[Rail构型] {route.Name}: {logMessage},起点已同步更新为 ({newStartPoint.X:F3}, {newStartPoint.Y:F3}, {newStartPoint.Z:F3})"); } private void RefreshSelectedRailInstallationReferenceVisuals() @@ -1622,6 +1672,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels ExportPathCommand = new RelayCommand(async () => await ExecuteExportPathAsync(), () => CanExecuteExportPath); SaveAsPathCommand = new RelayCommand(async () => await ExecuteSaveAsPathAsync(), () => CanExecuteSaveAsPath); CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync()); + ReCaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteReCaptureAssemblyTerminalObjectAsync()); GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod); SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint); RepositionRailStartPointCommand = new RelayCommand(async () => await ExecuteRepositionRailStartPointAsync(), () => CanRepositionRailStartPoint); @@ -1656,6 +1707,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("当前选择对象无效,请重新选择终点箱体"); } + // 注意:不在这里设置 _isEditingSelectedRail + // 新建路径时保持 false,只有点击"重选箱体"时才设置为 true + _assemblyTerminalObject = selectedItem; _assemblyStartPoint = Point3D.Origin; HasAssemblyTerminalObject = true; @@ -1672,12 +1726,24 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(CanRepositionRailStartPoint)); OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint)); OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); - RefreshSelectedRailReferenceLineVisuals($"已捕获终点箱体: {AssemblyTerminalObjectName}"); + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); + + // 新建路径时隐藏其他路径的辅助线 + ClearAssemblyReferenceLineVisuals(); + UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}"); LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}"); }, "捕获终点箱体"); } + private async Task ExecuteReCaptureAssemblyTerminalObjectAsync() + { + // 先进入编辑状态,再执行捕获 + _isEditingSelectedRail = true; + OnPropertyChanged(nameof(AssemblyInstallationPointButtonText)); + await ExecuteCaptureAssemblyTerminalObjectAsync(); + } + private void ClearNonGridPathVisualizations(string context) { if (PathPointRenderPlugin.Instance == null) @@ -1915,6 +1981,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels _pathPlanningManager.DisableMouseHandling(); _isSelectingAssemblyInstallationPoint = true; + + // 判断是编辑现有路径还是创建新路径 + var selectedRailRoute = GetSelectedCoreRoute(); + _isEditingSelectedRailInstallationPoint = selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail; + OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); OnPropertyChanged(nameof(CanRepositionRailStartPoint)); @@ -2048,12 +2119,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels BuildAndRenderAssemblyInstallationReference( _assemblyInstallationSeedPoints[0], _assemblyInstallationSeedPoints[1]); - var selectedRailRoute = GetSelectedCoreRoute(); - if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail) + + if (_isEditingSelectedRailInstallationPoint) { - PersistAssemblyInstallationReferenceToRailRoute( - selectedRailRoute, - "已重选安装点并同步更新 Rail 路径安装参数"); + // 编辑现有路径:更新安装点并同步起点 + var selectedRailRoute = GetSelectedCoreRoute(); + if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail) + { + PersistAssemblyInstallationReferenceToRailRoute( + selectedRailRoute, + "已重选安装点并同步更新 Rail 路径安装参数"); + } + } + else + { + // 创建新路径:生成参考线,等待用户选择起点 + LogManager.Info("[直线装配] 安装点已确定,请继续选择起点以生成新路径"); } CleanupAssemblyInstallationSelection(); }, "处理安装点拾取"); @@ -2224,6 +2305,36 @@ namespace NavisworksTransport.UI.WPF.ViewModels return adapter.FromCanonicalPoint(canonicalAnchorPoint); } + /// + /// 根据 Rail 路径终点和路径长度计算起点位置。 + /// 起点位于从终点沿光轴方向(远离球心方向)延伸路径长度的位置。 + /// + private Point3D CalculateRailStartPointFromEndPoint(Point3D endPoint, double pathLength) + { + HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); + + Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); + Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint); + Point3D canonicalOpticalAxisRef = adapter.ToCanonicalPoint(opticalAxisReferencePoint); + + // 计算从球心到光轴参考点的方向(作为参考线方向) + Vector3D direction = new Vector3D( + canonicalOpticalAxisRef.X - projectFrame.SphereCenterInCanonical.X, + canonicalOpticalAxisRef.Y - projectFrame.SphereCenterInCanonical.Y, + canonicalOpticalAxisRef.Z - projectFrame.SphereCenterInCanonical.Z); + + direction = direction.Normalize(); + + // 起点 = 终点 + 方向 * 路径长度 + Point3D canonicalStartPoint = new Point3D( + canonicalEndPoint.X + direction.X * pathLength, + canonicalEndPoint.Y + direction.Y * pathLength, + canonicalEndPoint.Z + direction.Z * pathLength); + + return adapter.FromCanonicalPoint(canonicalStartPoint); + } + private void UpdateAssemblyInstallationAnchorFromVerticalOffset() { if (!_hasAssemblyInstallationReference || @@ -2997,6 +3108,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels { PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked; _isSelectingAssemblyInstallationPoint = false; + _isEditingSelectedRailInstallationPoint = false; _pathPlanningManager?.EnableMouseHandling(); _pathPlanningManager?.StopClickTool(); ClearAssemblyInstallationReferenceVisuals(); diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 0249bdd..b17ac0d 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -560,10 +560,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 Orientation="Horizontal" VerticalAlignment="Center" ItemHeight="28"> - public class PathEditingViewModel : ViewModelBase, IDisposable { - private enum RailAssemblyWorkflowMode - { - None, - CreateNewRailPath, - EditSelectedRail - } - #region 私有字段 private PathPlanningManager _pathPlanningManager; @@ -135,38 +128,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const double DefaultAssemblySphereCenterX = 0.0; private const double DefaultAssemblySphereCenterY = 0.0; private const double DefaultAssemblySphereCenterZ = 0.0; - private string _assemblyTerminalObjectName = "未选择"; - private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体"; - private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters; - private double _assemblyReferenceRodDiameterInMeters = DefaultAssemblyReferenceRodDiameterInMeters; - private double _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; - private double _assemblySphereCenterX = DefaultAssemblySphereCenterX; - private double _assemblySphereCenterY = DefaultAssemblySphereCenterY; - private double _assemblySphereCenterZ = DefaultAssemblySphereCenterZ; - private string _assemblyStartPointText = "未选择"; - private RailMountMode _assemblyMountMode = RailMountMode.UnderRail; - private bool _hasAssemblyTerminalObject; - private bool _isSelectingAssemblyStartPoint; - private bool _isEditingSelectedRailStartPoint; - private bool _isSelectingAssemblyEndFacePoints; - private bool _isSelectingAssemblyInstallationPoint; - private bool _isEditingSelectedRailInstallationPoint; - private RailAssemblyWorkflowMode _railAssemblyWorkflowMode = RailAssemblyWorkflowMode.None; - private bool _hasAssemblyEndFaceAnalysis; - private bool _hasAssemblyInstallationReference; - private ModelItem _assemblyTerminalObject; - private Point3D _assemblyStartPoint; - private Point3D _assemblyEndFaceCenterPoint; - private Vector3 _assemblyEndFaceNormal; - private Point3D _assemblyInstallationPickPoint; - private Point3D _assemblyInstallationBaseAnchorPoint; - private Point3D _assemblyInstallationAnchorPoint; - private Vector3 _assemblyInstallationPlaneNormal; - private Vector3 _assemblyInstallationPlaneSpanDirection; - private double _assemblyInstallationOffsetDistanceInMeters; - private string _assemblyInstallationReferenceRouteId; - private readonly List _assemblyInstallationSeedPoints = new List(); - private readonly List _assemblyEndFaceSeedPoints = new List(); + private readonly RailAssemblyWorkflowContext _railAssemblyContext = new RailAssemblyWorkflowContext( + DefaultAssemblyReferenceRodLengthInMeters, + DefaultAssemblyReferenceRodDiameterInMeters, + DefaultAssemblyAnchorVerticalOffsetInMeters, + DefaultAssemblySphereCenterX, + DefaultAssemblySphereCenterY, + DefaultAssemblySphereCenterZ); private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line"; private const string AssemblyReferenceLinePathId = "assembly_reference_line"; @@ -181,6 +149,178 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const double AssemblyInstallationPlanePaddingInMeters = 0.5; private const double AssemblyInstallationLineLengthScale = 1.2; + private string _assemblyTerminalObjectName + { + get => _railAssemblyContext.TerminalObjectName; + set => _railAssemblyContext.TerminalObjectName = value; + } + + private string _assemblyTerminalObjectInfo + { + get => _railAssemblyContext.TerminalObjectInfo; + set => _railAssemblyContext.TerminalObjectInfo = value; + } + + private double _assemblyReferenceRodLengthInMeters + { + get => _railAssemblyContext.ReferenceRodLengthInMeters; + set => _railAssemblyContext.ReferenceRodLengthInMeters = value; + } + + private double _assemblyReferenceRodDiameterInMeters + { + get => _railAssemblyContext.ReferenceRodDiameterInMeters; + set => _railAssemblyContext.ReferenceRodDiameterInMeters = value; + } + + private double _assemblyAnchorVerticalOffsetInMeters + { + get => _railAssemblyContext.AnchorVerticalOffsetInMeters; + set => _railAssemblyContext.AnchorVerticalOffsetInMeters = value; + } + + private double _assemblySphereCenterX + { + get => _railAssemblyContext.SphereCenterX; + set => _railAssemblyContext.SphereCenterX = value; + } + + private double _assemblySphereCenterY + { + get => _railAssemblyContext.SphereCenterY; + set => _railAssemblyContext.SphereCenterY = value; + } + + private double _assemblySphereCenterZ + { + get => _railAssemblyContext.SphereCenterZ; + set => _railAssemblyContext.SphereCenterZ = value; + } + + private string _assemblyStartPointText + { + get => _railAssemblyContext.StartPointText; + set => _railAssemblyContext.StartPointText = value; + } + + private RailMountMode _assemblyMountMode + { + get => _railAssemblyContext.MountMode; + set => _railAssemblyContext.MountMode = value; + } + + private bool _hasAssemblyTerminalObject + { + get => _railAssemblyContext.HasTerminalObject; + set => _railAssemblyContext.HasTerminalObject = value; + } + + private bool _isSelectingAssemblyStartPoint + { + get => _railAssemblyContext.IsSelectingStartPoint; + set => _railAssemblyContext.IsSelectingStartPoint = value; + } + + private bool _isSelectingAssemblyEndFacePoints + { + get => _railAssemblyContext.IsSelectingEndFacePoints; + set => _railAssemblyContext.IsSelectingEndFacePoints = value; + } + + private bool _isSelectingAssemblyInstallationPoint + { + get => _railAssemblyContext.IsSelectingInstallationPoint; + set => _railAssemblyContext.IsSelectingInstallationPoint = value; + } + + private RailAssemblyWorkflowMode _railAssemblyWorkflowMode + { + get => _railAssemblyContext.WorkflowMode; + set => _railAssemblyContext.WorkflowMode = value; + } + + private bool _hasAssemblyEndFaceAnalysis + { + get => _railAssemblyContext.HasEndFaceAnalysis; + set => _railAssemblyContext.HasEndFaceAnalysis = value; + } + + private bool _hasAssemblyInstallationReference + { + get => _railAssemblyContext.HasInstallationReference; + set => _railAssemblyContext.HasInstallationReference = value; + } + + private ModelItem _assemblyTerminalObject + { + get => _railAssemblyContext.TerminalObject; + set => _railAssemblyContext.TerminalObject = value; + } + + private Point3D _assemblyStartPoint + { + get => _railAssemblyContext.StartPoint; + set => _railAssemblyContext.StartPoint = value; + } + + private Point3D _assemblyEndFaceCenterPoint + { + get => _railAssemblyContext.EndFaceCenterPoint; + set => _railAssemblyContext.EndFaceCenterPoint = value; + } + + private Vector3 _assemblyEndFaceNormal + { + get => _railAssemblyContext.EndFaceNormal; + set => _railAssemblyContext.EndFaceNormal = value; + } + + private Point3D _assemblyInstallationPickPoint + { + get => _railAssemblyContext.InstallationPickPoint; + set => _railAssemblyContext.InstallationPickPoint = value; + } + + private Point3D _assemblyInstallationBaseAnchorPoint + { + get => _railAssemblyContext.InstallationBaseAnchorPoint; + set => _railAssemblyContext.InstallationBaseAnchorPoint = value; + } + + private Point3D _assemblyInstallationAnchorPoint + { + get => _railAssemblyContext.InstallationAnchorPoint; + set => _railAssemblyContext.InstallationAnchorPoint = value; + } + + private Vector3 _assemblyInstallationPlaneNormal + { + get => _railAssemblyContext.InstallationPlaneNormal; + set => _railAssemblyContext.InstallationPlaneNormal = value; + } + + private Vector3 _assemblyInstallationPlaneSpanDirection + { + get => _railAssemblyContext.InstallationPlaneSpanDirection; + set => _railAssemblyContext.InstallationPlaneSpanDirection = value; + } + + private double _assemblyInstallationOffsetDistanceInMeters + { + get => _railAssemblyContext.InstallationOffsetDistanceInMeters; + set => _railAssemblyContext.InstallationOffsetDistanceInMeters = value; + } + + private string _assemblyInstallationReferenceRouteId + { + get => _railAssemblyContext.InstallationReferenceRouteId; + set => _railAssemblyContext.InstallationReferenceRouteId = value; + } + + private List _assemblyInstallationSeedPoints => _railAssemblyContext.InstallationSeedPoints; + + private List _assemblyEndFaceSeedPoints => _railAssemblyContext.EndFaceSeedPoints; + // 自动路径起点和终点的路径对象引用(用于正确的ID管理) private PathRoute _autoPathStartPointRoute = null; private PathRoute _autoPathEndPointRoute = null; @@ -250,8 +390,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels { try { - ViewpointHelper.AdjustViewpointToPathCenter(coreRoute); - LogManager.Info($"UI路径切换:已自动调整视角到路径中心: {value.Name}"); + ViewpointHelper.AdjustViewpointForPath(coreRoute); + LogManager.Info($"UI路径切换:已自动调整视角: {value.Name}, 类型={coreRoute.PathType}"); } catch (Exception ex) { @@ -417,13 +557,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels public string AssemblyTerminalObjectName { get => _assemblyTerminalObjectName; - set => SetProperty(ref _assemblyTerminalObjectName, value); + set + { + if (string.Equals(_assemblyTerminalObjectName, value, StringComparison.Ordinal)) + { + return; + } + + _assemblyTerminalObjectName = value; + OnPropertyChanged(nameof(AssemblyTerminalObjectName)); + } } public string AssemblyTerminalObjectInfo { get => _assemblyTerminalObjectInfo; - set => SetProperty(ref _assemblyTerminalObjectInfo, value); + set + { + if (string.Equals(_assemblyTerminalObjectInfo, value, StringComparison.Ordinal)) + { + return; + } + + _assemblyTerminalObjectInfo = value; + OnPropertyChanged(nameof(AssemblyTerminalObjectInfo)); + } } public double AssemblyReferenceRodLengthInMeters @@ -431,10 +589,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _assemblyReferenceRodLengthInMeters; set { - if (SetProperty(ref _assemblyReferenceRodLengthInMeters, value)) + if (Math.Abs(_assemblyReferenceRodLengthInMeters - value) < 1e-9) { - OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); + return; } + + _assemblyReferenceRodLengthInMeters = value; + OnPropertyChanged(nameof(AssemblyReferenceRodLengthInMeters)); } } @@ -443,10 +604,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _assemblyReferenceRodDiameterInMeters; set { - if (SetProperty(ref _assemblyReferenceRodDiameterInMeters, value)) + if (Math.Abs(_assemblyReferenceRodDiameterInMeters - value) < 1e-9) { - OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); + return; } + + _assemblyReferenceRodDiameterInMeters = value; + OnPropertyChanged(nameof(AssemblyReferenceRodDiameterInMeters)); } } @@ -455,7 +619,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _assemblyAnchorVerticalOffsetInMeters; set { - if (SetProperty(ref _assemblyAnchorVerticalOffsetInMeters, value) && HasAssemblyTerminalObject) + if (Math.Abs(_assemblyAnchorVerticalOffsetInMeters - value) < 1e-9) + { + return; + } + + _assemblyAnchorVerticalOffsetInMeters = value; + OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); + + if (HasAssemblyTerminalObject) { var coreRoute = GetSelectedCoreRoute(); if (_hasAssemblyInstallationReference) @@ -475,7 +647,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels } else { - RenderAssemblyAnchorMarker(); RefreshAssemblyReferenceRodIfNeeded(); } } @@ -487,7 +658,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _assemblySphereCenterX; set { - if (SetProperty(ref _assemblySphereCenterX, value) && HasAssemblyTerminalObject) + if (Math.Abs(_assemblySphereCenterX - value) < 1e-9) + { + return; + } + + _assemblySphereCenterX = value; + OnPropertyChanged(nameof(AssemblySphereCenterX)); + + if (HasAssemblyTerminalObject) { RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyReferenceRodIfNeeded(); @@ -500,7 +679,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _assemblySphereCenterY; set { - if (SetProperty(ref _assemblySphereCenterY, value) && HasAssemblyTerminalObject) + if (Math.Abs(_assemblySphereCenterY - value) < 1e-9) + { + return; + } + + _assemblySphereCenterY = value; + OnPropertyChanged(nameof(AssemblySphereCenterY)); + + if (HasAssemblyTerminalObject) { RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyReferenceRodIfNeeded(); @@ -513,7 +700,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _assemblySphereCenterZ; set { - if (SetProperty(ref _assemblySphereCenterZ, value) && HasAssemblyTerminalObject) + if (Math.Abs(_assemblySphereCenterZ - value) < 1e-9) + { + return; + } + + _assemblySphereCenterZ = value; + OnPropertyChanged(nameof(AssemblySphereCenterZ)); + + if (HasAssemblyTerminalObject) { RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyReferenceRodIfNeeded(); @@ -524,13 +719,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels public bool HasAssemblyTerminalObject { get => _hasAssemblyTerminalObject; - set => SetProperty(ref _hasAssemblyTerminalObject, value); + set + { + if (_hasAssemblyTerminalObject == value) + { + return; + } + + _hasAssemblyTerminalObject = value; + OnPropertyChanged(nameof(HasAssemblyTerminalObject)); + } } public string AssemblyStartPointText { get => _assemblyStartPointText; - set => SetProperty(ref _assemblyStartPointText, value); + set + { + if (string.Equals(_assemblyStartPointText, value, StringComparison.Ordinal)) + { + return; + } + + _assemblyStartPointText = value; + OnPropertyChanged(nameof(AssemblyStartPointText)); + } } public RailMountMode AssemblyMountMode @@ -538,7 +751,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels get => _assemblyMountMode; set { - if (SetProperty(ref _assemblyMountMode, value) && HasAssemblyTerminalObject) + if (_assemblyMountMode == value) + { + return; + } + + _assemblyMountMode = value; + OnPropertyChanged(nameof(AssemblyMountMode)); + + if (HasAssemblyTerminalObject) { RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyReferenceRodIfNeeded(); @@ -549,7 +770,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels public bool IsSelectingAssemblyStartPoint { get => _isSelectingAssemblyStartPoint; - set => SetProperty(ref _isSelectingAssemblyStartPoint, value); + set + { + if (_isSelectingAssemblyStartPoint == value) + { + return; + } + + _isSelectingAssemblyStartPoint = value; + OnPropertyChanged(nameof(IsSelectingAssemblyStartPoint)); + } } /// /// 更新路径可视化显示:清理其他路径,只显示当前选中的路径 @@ -1072,7 +1302,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand SaveAsPathCommand { get; private set; } public ICommand CaptureAssemblyTerminalObjectForCreateCommand { get; private set; } public ICommand ReCaptureAssemblyTerminalObjectForEditCommand { get; private set; } - public ICommand GenerateAssemblyReferenceRodCommand { get; private set; } public ICommand SelectAssemblyStartPointForCreateCommand { get; private set; } public ICommand RepositionRailStartPointCommand { get; private set; } public ICommand SelectAssemblyInstallationPointForCreateCommand { get; private set; } @@ -1119,10 +1348,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels public bool CanExecuteExportPath => _pathPlanningManager?.Routes?.Count > 0 || SelectedPathRoute != null; public bool CanExecuteSaveAsPath => SelectedPathRoute != null && SelectedPathRoute.Points.Count > 0; - public bool CanGenerateAssemblyReferenceRod => HasAssemblyTerminalObject && - _hasAssemblyInstallationReference && - AssemblyReferenceRodLengthInMeters > 0 && - AssemblyReferenceRodDiameterInMeters > 0; public bool CanSelectAssemblyStartPointForCreate => HasAssemblyTerminalObject && _pathPlanningManager != null && AssemblyReferencePathManager.Instance.HasReferenceLine && @@ -1182,6 +1407,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels private bool IsRailCreateWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.CreateNewRailPath; private bool IsRailEditingWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.EditSelectedRail; + private static bool IsRailAssemblyEditMode(RailAssemblyWorkflowMode workflowMode) + { + return workflowMode == RailAssemblyWorkflowMode.EditSelectedRail; + } + + private static string GetRailAssemblyWorkflowLabel(RailAssemblyWorkflowMode workflowMode) + { + return IsRailAssemblyEditMode(workflowMode) + ? "编辑现有Rail路径" + : "新建Rail路径"; + } + + private bool ShouldShowAssemblyReferenceLineVisuals() + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + return false; + } + + return IsRailEditingWorkflowActive || _hasAssemblyInstallationReference; + } + public bool CanExecuteModifyPoint => SelectedPathPoint != null && _pathPlanningManager?.PathEditState != PathEditState.EditingPoint; public bool CanExecuteCancelModifyPoint => _pathPlanningManager?.PathEditState == PathEditState.EditingPoint; @@ -1718,7 +1965,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels SaveAsPathCommand = new RelayCommand(async () => await ExecuteSaveAsPathAsync(), () => CanExecuteSaveAsPath); CaptureAssemblyTerminalObjectForCreateCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync(RailAssemblyWorkflowMode.CreateNewRailPath)); ReCaptureAssemblyTerminalObjectForEditCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync(RailAssemblyWorkflowMode.EditSelectedRail)); - GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod); SelectAssemblyStartPointForCreateCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPointForCreate); RepositionRailStartPointCommand = new RelayCommand(async () => await ExecuteRepositionRailStartPointAsync(), () => CanRepositionRailStartPoint); SelectAssemblyInstallationPointForCreateCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(RailAssemblyWorkflowMode.CreateNewRailPath), () => CanSelectAssemblyInstallationPointForCreate); @@ -1768,7 +2014,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels ClearAssemblyAnchorMarker(); ClearAssemblyEndFaceAnalysisVisuals(); ClearAssemblyInstallationReferenceVisuals(); - OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); NotifyRailAssemblyCommandStateChanged(); // 新建路径时隐藏其他路径的辅助线 @@ -1803,41 +2048,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } - private async Task ExecuteGenerateAssemblyReferenceRodAsync() - { - await SafeExecuteAsync(() => - { - if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) - { - throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体"); - } - - AssemblyReferenceLine referenceLine = BuildAssemblyReferenceLine(); - - AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod( - referenceLine.StartPoint, - referenceLine.EndPoint, - AssemblyReferenceRodDiameterInMeters); - - _assemblyStartPoint = Point3D.Origin; - AssemblyStartPointText = "未选择"; - RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint); - RenderAssemblyAnchorMarker(); - RenderAssemblyCenterGuideLine(); - RenderAssemblyReferenceLine(referenceLine); - FocusOnAssemblyReferenceArea(referenceLine.StartPoint, referenceLine.EndPoint); - - UpdateMainStatus("已生成终端安装辅助线,请在三维视图中确认终点锚点、方向、外端位置和辅助线贴合情况"); - NotifyRailAssemblyCommandStateChanged(); - LogManager.Info( - $"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " + - $"终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " + - $"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2}), " + - $"方向=({referenceLine.Direction.X:F3}, {referenceLine.Direction.Y:F3}, {referenceLine.Direction.Z:F3}), " + - $"资源={AssemblyReferencePathManager.Instance.ReferenceResourceName}"); - }, "生成终端安装辅助线"); - } - private async Task ExecuteSelectAssemblyStartPointAsync() { await SafeExecuteAsync(() => @@ -1854,27 +2064,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (!AssemblyReferencePathManager.Instance.HasReferenceLine) { - throw new InvalidOperationException("请先生成终端安装辅助线"); + throw new InvalidOperationException("请先选择安装点,系统生成终端安装辅助线后才能取起点"); } - CleanupAssemblyReferenceSelection(); - - _pathPlanningManager.DisableMouseHandling(); - IsSelectingAssemblyStartPoint = true; - NotifyRailAssemblyCommandStateChanged(); - - PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked; - PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked; - - if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) - { - CleanupAssemblyReferenceSelection(); - throw new InvalidOperationException("ToolPlugin 初始化失败,请重试"); - } - - _pathPlanningManager.StartClickTool(PathPointType.StartPoint); - UpdateMainStatus("请在辅助线附近点击一个起点,系统会自动吸附到辅助线上并生成直线路径"); - LogManager.Info("[直线装配] 已进入起点拾取模式"); + BeginAssemblyStartPointSelection( + statusMessage: "请在辅助线附近点击一个起点,系统会自动吸附到辅助线上并生成直线路径", + logMessage: "[直线装配] 已进入起点拾取模式", + toolPluginFailureMessage: "ToolPlugin 初始化失败,请重试"); }, "选择终端安装起点"); } @@ -1905,25 +2101,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("当前 Rail 路径无法生成辅助线,请先检查路径起点和终点。"); } - CleanupAssemblyReferenceSelection(); - - _pathPlanningManager.DisableMouseHandling(); - _isEditingSelectedRailStartPoint = true; - IsSelectingAssemblyStartPoint = true; - NotifyRailAssemblyCommandStateChanged(); - - PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked; - PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked; - - if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) - { - CleanupAssemblyReferenceSelection(); - throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); - } - - _pathPlanningManager.StartClickTool(PathPointType.StartPoint); - UpdateMainStatus("请在辅助线附近点击新的起点位置,系统会自动吸附到辅助线上并更新当前 Rail 路径。"); - LogManager.Info("[Rail构型] 已进入起点重选模式"); + BeginAssemblyStartPointSelection( + statusMessage: "请在辅助线附近点击新的起点位置,系统会自动吸附到辅助线上并更新当前 Rail 路径。", + logMessage: "[Rail构型] 已进入起点重选模式", + toolPluginFailureMessage: "ToolPlugin 初始化失败,请重试。"); }, "重选 Rail 路径起点"); } @@ -1957,45 +2138,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。"); } - bool isEditingExistingRailRoute = workflowMode == RailAssemblyWorkflowMode.EditSelectedRail; - var selectedRailRoute = isEditingExistingRailRoute ? GetSelectedCoreRoute() : null; - if (isEditingExistingRailRoute) - { - if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail) - { - throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。"); - } + PrepareRailAssemblyWorkflowForMode( + workflowMode, + editActionDescription: "请先选中一条需要编辑安装点的 Rail 路径。", + syncMountModeFromRoute: true); - AssemblyMountMode = selectedRailRoute.RailMountMode; - } - - SetRailAssemblyWorkflowMode(workflowMode); - - CleanupAssemblyReferenceSelection(); - CleanupAssemblyEndFaceSelection(clearVisuals: false); - CleanupAssemblyInstallationSelection(); - ClearAssemblyEndFaceAnalysisVisuals(); - ResetAssemblyEndFaceAnalysisState(); - ClearAssemblyInstallationReferenceVisuals(); - ResetAssemblyInstallationReferenceState(); - _assemblyEndFaceSeedPoints.Clear(); - - _pathPlanningManager.DisableMouseHandling(); - _isSelectingAssemblyEndFacePoints = true; - NotifyRailAssemblyCommandStateChanged(); - - PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked; - PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked; - - if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) - { - CleanupAssemblyEndFaceSelection(clearVisuals: false); - throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); - } - - UpdateMainStatus("请在同一个端面平面上连续点击 3 个点,系统将分析端面中心。"); - LogManager.Info( - $"[直线装配] 已进入端面三点分析模式,模式={(isEditingExistingRailRoute ? "编辑现有Rail路径" : "新建Rail路径")}"); + BeginAssemblyEndFaceSelection(workflowMode); }, "分析终端端面"); } @@ -2013,39 +2161,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。"); } - bool isEditingExistingRailRoute = workflowMode == RailAssemblyWorkflowMode.EditSelectedRail; - var selectedRailRoute = isEditingExistingRailRoute ? GetSelectedCoreRoute() : null; - if (isEditingExistingRailRoute && (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail)) - { - throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。"); - } + PrepareRailAssemblyWorkflowForMode( + workflowMode, + editActionDescription: "请先选中一条需要编辑安装点的 Rail 路径。", + syncMountModeFromRoute: false); - SetRailAssemblyWorkflowMode(workflowMode); - - CleanupAssemblyReferenceSelection(); - CleanupAssemblyEndFaceSelection(clearVisuals: false); - CleanupAssemblyInstallationSelection(); - ClearAssemblyInstallationReferenceVisuals(); - - _pathPlanningManager.DisableMouseHandling(); - _isSelectingAssemblyInstallationPoint = true; - - _isEditingSelectedRailInstallationPoint = isEditingExistingRailRoute; - - NotifyRailAssemblyCommandStateChanged(); - - PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked; - PathClickToolPlugin.MouseClicked += OnAssemblyInstallationMouseClicked; - - if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) - { - CleanupAssemblyInstallationSelection(); - throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); - } - - _assemblyInstallationSeedPoints.Clear(); - UpdateMainStatus("请在终点箱体表面连续点击两个安装面点,系统将按两点和光轴计算安装参考面与安装点。"); - LogManager.Info("[直线装配] 已进入安装点拾取模式"); + BeginAssemblyInstallationSelection(); }, "选择安装点"); } @@ -2067,16 +2188,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels await SafeExecuteAsync(() => { - if (_isEditingSelectedRailStartPoint) - { - UpdateSelectedRailRouteStartPoint(projectedStartPoint); - UpdateMainStatus("已根据辅助线更新当前 Rail 路径起点"); - } - else - { - CreateAssemblyLinearRoute(projectedStartPoint); - UpdateMainStatus("已根据辅助线起点生成终端安装路径"); - } + ApplyAssemblyStartPointForCurrentWorkflow(projectedStartPoint); CleanupAssemblyReferenceSelection(); }, "生成终端安装路径"); @@ -2163,22 +2275,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels _assemblyInstallationSeedPoints[0], _assemblyInstallationSeedPoints[1]); - if (_isEditingSelectedRailInstallationPoint) - { - // 编辑现有路径:更新安装点并同步起点 - var selectedRailRoute = GetSelectedCoreRoute(); - if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail) - { - PersistAssemblyInstallationReferenceToRailRoute( - selectedRailRoute, - "已重选安装点并同步更新 Rail 路径安装参数"); - } - } - else - { - // 创建新路径:生成参考线,等待用户选择起点 - LogManager.Info("[直线装配] 安装点已确定,请继续选择起点以生成新路径"); - } + ApplyAssemblyInstallationReferenceForCurrentWorkflow(); CleanupAssemblyInstallationSelection(clearVisuals: false); }, "处理安装点拾取"); } @@ -2190,6 +2287,138 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private void PrepareRailAssemblyWorkflowForMode( + RailAssemblyWorkflowMode workflowMode, + string editActionDescription, + bool syncMountModeFromRoute) + { + PathRoute selectedRailRoute = null; + if (IsRailAssemblyEditMode(workflowMode)) + { + selectedRailRoute = RequireSelectedRailRouteForAssemblyEdit(editActionDescription); + if (syncMountModeFromRoute) + { + AssemblyMountMode = selectedRailRoute.RailMountMode; + } + } + + SetRailAssemblyWorkflowMode(workflowMode); + } + + private PathRoute RequireSelectedRailRouteForAssemblyEdit(string errorMessage) + { + var selectedRailRoute = GetSelectedCoreRoute(); + if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail) + { + throw new InvalidOperationException(errorMessage); + } + + return selectedRailRoute; + } + + private void BeginAssemblyStartPointSelection(string statusMessage, string logMessage, string toolPluginFailureMessage) + { + CleanupAssemblyReferenceSelection(); + + _pathPlanningManager.DisableMouseHandling(); + IsSelectingAssemblyStartPoint = true; + NotifyRailAssemblyCommandStateChanged(); + + PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked; + PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked; + + if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) + { + CleanupAssemblyReferenceSelection(); + throw new InvalidOperationException(toolPluginFailureMessage); + } + + _pathPlanningManager.StartClickTool(PathPointType.StartPoint); + UpdateMainStatus(statusMessage); + LogManager.Info(logMessage); + } + + private void BeginAssemblyEndFaceSelection(RailAssemblyWorkflowMode workflowMode) + { + CleanupAssemblyReferenceSelection(); + CleanupAssemblyEndFaceSelection(clearVisuals: false); + CleanupAssemblyInstallationSelection(); + ClearAssemblyEndFaceAnalysisVisuals(); + ResetAssemblyEndFaceAnalysisState(); + ClearAssemblyInstallationReferenceVisuals(); + ResetAssemblyInstallationReferenceState(); + _assemblyEndFaceSeedPoints.Clear(); + + _pathPlanningManager.DisableMouseHandling(); + _isSelectingAssemblyEndFacePoints = true; + NotifyRailAssemblyCommandStateChanged(); + + PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked; + PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked; + + if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) + { + CleanupAssemblyEndFaceSelection(clearVisuals: false); + throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); + } + + UpdateMainStatus("请在同一个端面平面上连续点击 3 个点,系统将分析端面中心。"); + LogManager.Info($"[直线装配] 已进入端面三点分析模式,模式={GetRailAssemblyWorkflowLabel(workflowMode)}"); + } + + private void BeginAssemblyInstallationSelection() + { + CleanupAssemblyReferenceSelection(); + CleanupAssemblyEndFaceSelection(clearVisuals: false); + CleanupAssemblyInstallationSelection(); + ClearAssemblyInstallationReferenceVisuals(); + + _pathPlanningManager.DisableMouseHandling(); + _isSelectingAssemblyInstallationPoint = true; + NotifyRailAssemblyCommandStateChanged(); + + PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked; + PathClickToolPlugin.MouseClicked += OnAssemblyInstallationMouseClicked; + + if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) + { + CleanupAssemblyInstallationSelection(); + throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); + } + + _assemblyInstallationSeedPoints.Clear(); + UpdateMainStatus("请在终点箱体表面连续点击两个安装面点,系统将按两点和光轴计算安装参考面与安装点。"); + LogManager.Info($"[直线装配] 已进入安装点拾取模式,模式={GetRailAssemblyWorkflowLabel(_railAssemblyWorkflowMode)}"); + } + + private void ApplyAssemblyStartPointForCurrentWorkflow(Point3D projectedStartPoint) + { + if (IsRailEditingWorkflowActive) + { + UpdateSelectedRailRouteStartPoint(projectedStartPoint); + ClearAssemblyReferenceLineVisuals(); + UpdateMainStatus("已根据辅助线更新当前 Rail 路径起点"); + return; + } + + CreateAssemblyLinearRoute(projectedStartPoint); + UpdateMainStatus("已根据辅助线起点生成终端安装路径"); + } + + private void ApplyAssemblyInstallationReferenceForCurrentWorkflow() + { + if (IsRailEditingWorkflowActive) + { + var selectedRailRoute = RequireSelectedRailRouteForAssemblyEdit("当前没有可更新安装点的 Rail 路径。"); + PersistAssemblyInstallationReferenceToRailRoute( + selectedRailRoute, + "已重选安装点并同步更新 Rail 路径安装参数"); + return; + } + + LogManager.Info("[直线装配] 安装点已确定,请继续选择起点以生成新路径"); + } + private void CreateAssemblyLinearRoute(Point3D startPoint) { Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd; @@ -2324,16 +2553,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels ClearAssemblyInstallationReferenceVisuals(); ResetAssemblyEndFaceAnalysisState(); ResetAssemblyInstallationReferenceState(); - - _assemblyTerminalObject = null; - _assemblyStartPoint = Point3D.Origin; + _railAssemblyContext.ResetSession(); HasAssemblyTerminalObject = false; - AssemblyTerminalObjectName = "未选择"; - AssemblyTerminalObjectInfo = "请选择终点处已安装箱体"; - AssemblyStartPointText = "未选择"; + AssemblyTerminalObjectName = _assemblyTerminalObjectName; + AssemblyTerminalObjectInfo = _assemblyTerminalObjectInfo; + AssemblyStartPointText = _assemblyStartPointText; SetRailAssemblyWorkflowMode(RailAssemblyWorkflowMode.None); - OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); NotifyRailAssemblyCommandStateChanged(); if (announce && !string.IsNullOrWhiteSpace(statusMessage)) @@ -2595,7 +2821,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels private void RefreshAssemblyReferenceRodIfNeeded() { - if (!AssemblyReferencePathManager.Instance.HasReferenceLine || _assemblyTerminalObject == null) + if (!ShouldShowAssemblyReferenceLineVisuals()) + { + ClearAssemblyReferenceLineVisuals(); + return; + } + + if (_assemblyTerminalObject == null) { return; } @@ -2672,61 +2904,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels return standardRadiusInMeters * 0.8; } - private void FocusOnAssemblyReferenceArea(Point3D startPoint, Point3D endPoint) - { - try - { - double minX = Math.Min(startPoint.X, endPoint.X); - double minY = Math.Min(startPoint.Y, endPoint.Y); - double minZ = Math.Min(startPoint.Z, endPoint.Z); - double maxX = Math.Max(startPoint.X, endPoint.X); - double maxY = Math.Max(startPoint.Y, endPoint.Y); - double maxZ = Math.Max(startPoint.Z, endPoint.Z); - - if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) - { - var terminalBounds = _assemblyTerminalObject.BoundingBox(); - minX = Math.Min(minX, terminalBounds.Min.X); - minY = Math.Min(minY, terminalBounds.Min.Y); - minZ = Math.Min(minZ, terminalBounds.Min.Z); - maxX = Math.Max(maxX, terminalBounds.Max.X); - maxY = Math.Max(maxY, terminalBounds.Max.Y); - maxZ = Math.Max(maxZ, terminalBounds.Max.Z); - } - - Point3D center = new Point3D( - (minX + maxX) / 2.0, - (minY + maxY) / 2.0, - (minZ + maxZ) / 2.0); - - double targetSize = Math.Max( - maxX - minX, - Math.Max(maxY - minY, maxZ - minZ)); - - targetSize = Math.Max(targetSize, UnitsConverter.ConvertFromMeters(1.0)); - - ViewpointHelper.FocusOnPosition(center, targetSize, 45.0, 0.35); - - var selection = new List(); - if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) - { - selection.Add(_assemblyTerminalObject); - } - - var referenceRod = AssemblyReferencePathManager.Instance.CurrentReferenceRod; - if (referenceRod != null) - { - selection.Add(referenceRod); - } - - NavisworksSelectionHelper.SetModelSelection(selection); - } - catch (Exception ex) - { - LogManager.Warning($"[直线装配] 聚焦参考杆区域失败: {ex.Message}"); - } - } - private void RenderAssemblyAnchorMarker() { if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) @@ -2930,13 +3107,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyReferenceRodIfNeeded(); UpdateMainStatus( - $"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m"); + $"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m,可继续取起点"); LogManager.Info( $"[直线装配] 安装参考已计算: 安装面点1=({firstPickPoint.X:F3}, {firstPickPoint.Y:F3}, {firstPickPoint.Z:F3}), " + $"安装面点2=({secondPickPoint.X:F3}, {secondPickPoint.Y:F3}, {secondPickPoint.Z:F3}), " + $"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " + $"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})"); - OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); NotifyRailAssemblyCommandStateChanged(); } @@ -3139,9 +3315,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private void ResetAssemblyEndFaceAnalysisState() { - _hasAssemblyEndFaceAnalysis = false; - _assemblyEndFaceCenterPoint = null; - _assemblyEndFaceNormal = default(Vector3); + _railAssemblyContext.ResetEndFaceAnalysis(); } private void ClearAssemblyInstallationReferenceVisuals() @@ -3161,18 +3335,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels private void ResetAssemblyInstallationReferenceState() { - _hasAssemblyInstallationReference = false; - _assemblyInstallationPickPoint = null; - _assemblyInstallationBaseAnchorPoint = null; - _assemblyInstallationAnchorPoint = null; - _assemblyInstallationPlaneNormal = default(Vector3); - _assemblyInstallationPlaneSpanDirection = default(Vector3); - _assemblyInstallationOffsetDistanceInMeters = 0.0; - _assemblyInstallationReferenceRouteId = null; - _assemblyInstallationSeedPoints.Clear(); - _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; + _railAssemblyContext.ResetInstallationReference(); OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters)); - OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); NotifyRailAssemblyCommandStateChanged(); } @@ -3182,7 +3346,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels { PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked; IsSelectingAssemblyStartPoint = false; - _isEditingSelectedRailStartPoint = false; _pathPlanningManager?.EnableMouseHandling(); _pathPlanningManager?.StopClickTool(); NotifyRailAssemblyCommandStateChanged(); @@ -3199,7 +3362,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels { PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked; _isSelectingAssemblyInstallationPoint = false; - _isEditingSelectedRailInstallationPoint = false; _pathPlanningManager?.EnableMouseHandling(); _pathPlanningManager?.StopClickTool(); if (clearVisuals) diff --git a/src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs b/src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs new file mode 100644 index 0000000..f5c3262 --- /dev/null +++ b/src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.Numerics; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.UI.WPF.ViewModels +{ + internal enum RailAssemblyWorkflowMode + { + None, + CreateNewRailPath, + EditSelectedRail + } + + internal sealed class RailAssemblyWorkflowContext + { + private const string DefaultTerminalObjectName = "未选择"; + private const string DefaultTerminalObjectInfo = "请选择终点处已安装箱体"; + private const string DefaultAssemblyStartPointText = "未选择"; + + private readonly double _defaultReferenceRodLengthInMeters; + private readonly double _defaultReferenceRodDiameterInMeters; + private readonly double _defaultAnchorVerticalOffsetInMeters; + private readonly double _defaultSphereCenterX; + private readonly double _defaultSphereCenterY; + private readonly double _defaultSphereCenterZ; + + public RailAssemblyWorkflowContext( + double defaultReferenceRodLengthInMeters, + double defaultReferenceRodDiameterInMeters, + double defaultAnchorVerticalOffsetInMeters, + double defaultSphereCenterX, + double defaultSphereCenterY, + double defaultSphereCenterZ) + { + _defaultReferenceRodLengthInMeters = defaultReferenceRodLengthInMeters; + _defaultReferenceRodDiameterInMeters = defaultReferenceRodDiameterInMeters; + _defaultAnchorVerticalOffsetInMeters = defaultAnchorVerticalOffsetInMeters; + _defaultSphereCenterX = defaultSphereCenterX; + _defaultSphereCenterY = defaultSphereCenterY; + _defaultSphereCenterZ = defaultSphereCenterZ; + + TerminalObjectName = DefaultTerminalObjectName; + TerminalObjectInfo = DefaultTerminalObjectInfo; + ReferenceRodLengthInMeters = _defaultReferenceRodLengthInMeters; + ReferenceRodDiameterInMeters = _defaultReferenceRodDiameterInMeters; + AnchorVerticalOffsetInMeters = _defaultAnchorVerticalOffsetInMeters; + SphereCenterX = _defaultSphereCenterX; + SphereCenterY = _defaultSphereCenterY; + SphereCenterZ = _defaultSphereCenterZ; + StartPointText = DefaultAssemblyStartPointText; + MountMode = RailMountMode.UnderRail; + InstallationSeedPoints = new List(); + EndFaceSeedPoints = new List(); + } + + public RailAssemblyWorkflowMode WorkflowMode { get; set; } + + public string TerminalObjectName { get; set; } + + public string TerminalObjectInfo { get; set; } + + public double ReferenceRodLengthInMeters { get; set; } + + public double ReferenceRodDiameterInMeters { get; set; } + + public double AnchorVerticalOffsetInMeters { get; set; } + + public double SphereCenterX { get; set; } + + public double SphereCenterY { get; set; } + + public double SphereCenterZ { get; set; } + + public string StartPointText { get; set; } + + public RailMountMode MountMode { get; set; } + + public bool HasTerminalObject { get; set; } + + public bool IsSelectingStartPoint { get; set; } + + public bool IsSelectingEndFacePoints { get; set; } + + public bool IsSelectingInstallationPoint { get; set; } + + public bool HasEndFaceAnalysis { get; set; } + + public bool HasInstallationReference { get; set; } + + public ModelItem TerminalObject { get; set; } + + public Point3D StartPoint { get; set; } + + public Point3D EndFaceCenterPoint { get; set; } + + public Vector3 EndFaceNormal { get; set; } + + public Point3D InstallationPickPoint { get; set; } + + public Point3D InstallationBaseAnchorPoint { get; set; } + + public Point3D InstallationAnchorPoint { get; set; } + + public Vector3 InstallationPlaneNormal { get; set; } + + public Vector3 InstallationPlaneSpanDirection { get; set; } + + public double InstallationOffsetDistanceInMeters { get; set; } + + public string InstallationReferenceRouteId { get; set; } + + public List InstallationSeedPoints { get; } + + public List EndFaceSeedPoints { get; } + + public void ResetEndFaceAnalysis() + { + HasEndFaceAnalysis = false; + EndFaceCenterPoint = null; + EndFaceNormal = default(Vector3); + EndFaceSeedPoints.Clear(); + } + + public void ResetInstallationReference() + { + HasInstallationReference = false; + InstallationPickPoint = null; + InstallationBaseAnchorPoint = null; + InstallationAnchorPoint = null; + InstallationPlaneNormal = default(Vector3); + InstallationPlaneSpanDirection = default(Vector3); + InstallationOffsetDistanceInMeters = 0.0; + InstallationReferenceRouteId = null; + InstallationSeedPoints.Clear(); + AnchorVerticalOffsetInMeters = _defaultAnchorVerticalOffsetInMeters; + } + + public void ResetSession() + { + WorkflowMode = RailAssemblyWorkflowMode.None; + HasTerminalObject = false; + IsSelectingStartPoint = false; + IsSelectingEndFacePoints = false; + IsSelectingInstallationPoint = false; + TerminalObject = null; + StartPoint = Point3D.Origin; + TerminalObjectName = DefaultTerminalObjectName; + TerminalObjectInfo = DefaultTerminalObjectInfo; + StartPointText = DefaultAssemblyStartPointText; + ResetEndFaceAnalysis(); + ResetInstallationReference(); + } + } +} diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 4ea9e67..73e7b6d 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -488,10 +488,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 Command="{Binding SelectAssemblyInstallationPointForCreateCommand}" Style="{StaticResource SecondaryButtonStyle}" IsEnabled="{Binding CanSelectAssemblyInstallationPointForCreate}"/> - - - + + @@ -155,7 +155,7 @@ - + @@ -165,7 +165,7 @@ - + @@ -175,7 +175,7 @@ - + @@ -183,7 +183,7 @@ x:Name="CopyTopButton" Style="{StaticResource IconButtonStyle}" Click="OnCopyTopCenterClick" - ToolTip="复制顶面中心点坐标" + ToolTip="复制宿主包围盒顶面中心点坐标" Margin="3,0,0,0"> - - - + + + @@ -208,7 +208,7 @@ - + @@ -218,7 +218,7 @@ - + @@ -228,7 +228,7 @@ - + @@ -236,13 +236,111 @@ x:Name="CopyBottomButton" Style="{StaticResource IconButtonStyle}" Click="OnCopyBottomCenterClick" - ToolTip="复制底面中心点坐标" + ToolTip="复制宿主包围盒底面中心点坐标" Margin="3,0,0,0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs index f6591a6..b833bb8 100644 --- a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs +++ b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs @@ -4,6 +4,8 @@ using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; using Autodesk.Navisworks.Api; +using NavisworksTransport.Utils; +using NavisworksTransport.Utils.CoordinateSystem; namespace NavisworksTransport.UI.WPF.Views { @@ -35,23 +37,41 @@ namespace NavisworksTransport.UI.WPF.Views /// 中心点Z坐标 public double CenterZ { get; set; } - /// 顶面中心点X坐标 - public double TopCenterX { get; set; } + /// 宿主包围盒顶面中心点X坐标 + public double HostTopCenterX { get; set; } - /// 顶面中心点Y坐标 - public double TopCenterY { get; set; } + /// 宿主包围盒顶面中心点Y坐标 + public double HostTopCenterY { get; set; } - /// 顶面中心点Z坐标 - public double TopCenterZ { get; set; } + /// 宿主包围盒顶面中心点Z坐标 + public double HostTopCenterZ { get; set; } - /// 底面中心点X坐标 - public double BottomCenterX { get; set; } + /// 宿主包围盒底面中心点X坐标 + public double HostBottomCenterX { get; set; } - /// 底面中心点Y坐标 - public double BottomCenterY { get; set; } + /// 宿主包围盒底面中心点Y坐标 + public double HostBottomCenterY { get; set; } - /// 底面中心点Z坐标 - public double BottomCenterZ { get; set; } + /// 宿主包围盒底面中心点Z坐标 + public double HostBottomCenterZ { get; set; } + + /// 对象姿态语义顶面中心点X坐标 + public double OrientedTopCenterX { get; set; } + + /// 对象姿态语义顶面中心点Y坐标 + public double OrientedTopCenterY { get; set; } + + /// 对象姿态语义顶面中心点Z坐标 + public double OrientedTopCenterZ { get; set; } + + /// 对象姿态语义底面中心点X坐标 + public double OrientedBottomCenterX { get; set; } + + /// 对象姿态语义底面中心点Y坐标 + public double OrientedBottomCenterY { get; set; } + + /// 对象姿态语义底面中心点Z坐标 + public double OrientedBottomCenterZ { get; set; } #endregion @@ -127,15 +147,36 @@ namespace NavisworksTransport.UI.WPF.Views CenterY = (bounds.Min.Y + bounds.Max.Y) / 2.0; CenterZ = (bounds.Min.Z + bounds.Max.Z) / 2.0; - // 计算顶面中心点(Z最大) - TopCenterX = CenterX; - TopCenterY = CenterY; - TopCenterZ = bounds.Max.Z; + HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); - // 计算底面中心点(Z最小) - BottomCenterX = CenterX; - BottomCenterY = CenterY; - BottomCenterZ = bounds.Min.Z; + Point3D hostBottomCenter = ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter); + double hostHeight = ModelItemTransformHelper.GetHostHeight(bounds, adapter); + Point3D hostTopCenter = new Point3D( + hostBottomCenter.X + adapter.HostUpVector.X * hostHeight, + hostBottomCenter.Y + adapter.HostUpVector.Y * hostHeight, + hostBottomCenter.Z + adapter.HostUpVector.Z * hostHeight); + + HostBottomCenterX = hostBottomCenter.X; + HostBottomCenterY = hostBottomCenter.Y; + HostBottomCenterZ = hostBottomCenter.Z; + HostTopCenterX = hostTopCenter.X; + HostTopCenterY = hostTopCenter.Y; + HostTopCenterZ = hostTopCenter.Z; + + Point3D orientedBottomCenter = ModelItemTransformHelper.GetStableBottomAnchorPoint(bounds, item.Transform); + Vector3D objectUp = ModelItemTransformHelper.GetUpDirectionFromTransform(item.Transform); + Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, item.Transform); + Point3D orientedTopCenter = new Point3D( + orientedBottomCenter.X + objectUp.X * localSize.Z, + orientedBottomCenter.Y + objectUp.Y * localSize.Z, + orientedBottomCenter.Z + objectUp.Z * localSize.Z); + + OrientedBottomCenterX = orientedBottomCenter.X; + OrientedBottomCenterY = orientedBottomCenter.Y; + OrientedBottomCenterZ = orientedBottomCenter.Z; + OrientedTopCenterX = orientedTopCenter.X; + OrientedTopCenterY = orientedTopCenter.Y; + OrientedTopCenterZ = orientedTopCenter.Z; } catch (Exception ex) { @@ -157,8 +198,10 @@ namespace NavisworksTransport.UI.WPF.Views { SizeX = SizeY = SizeZ = 0; CenterX = CenterY = CenterZ = 0; - TopCenterX = TopCenterY = TopCenterZ = 0; - BottomCenterX = BottomCenterY = BottomCenterZ = 0; + HostTopCenterX = HostTopCenterY = HostTopCenterZ = 0; + HostBottomCenterX = HostBottomCenterY = HostBottomCenterZ = 0; + OrientedTopCenterX = OrientedTopCenterY = OrientedTopCenterZ = 0; + OrientedBottomCenterX = OrientedBottomCenterY = OrientedBottomCenterZ = 0; } /// @@ -202,7 +245,7 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyTopCenterClick(object sender, RoutedEventArgs e) { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ); + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", HostTopCenterX, HostTopCenterY, HostTopCenterZ); bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); ShowButtonFeedback(CopyTopButton, success); } @@ -212,11 +255,25 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyBottomCenterClick(object sender, RoutedEventArgs e) { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ); + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", HostBottomCenterX, HostBottomCenterY, HostBottomCenterZ); bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); ShowButtonFeedback(CopyBottomButton, success); } + private void OnCopyOrientedTopCenterClick(object sender, RoutedEventArgs e) + { + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", OrientedTopCenterX, OrientedTopCenterY, OrientedTopCenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyOrientedTopButton, success); + } + + private void OnCopyOrientedBottomCenterClick(object sender, RoutedEventArgs e) + { + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", OrientedBottomCenterX, OrientedBottomCenterY, OrientedBottomCenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyOrientedBottomButton, success); + } + /// /// 显示按钮点击反馈 /// From 7ff510daf036438df8753e45186bf54251593eeb Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 2 Apr 2026 00:15:58 +0800 Subject: [PATCH 54/87] Use actual hoisting pose without fragment fallback --- .../RailPreservedPoseTests.cs | 54 ++++ src/Core/Animation/PathAnimationManager.cs | 232 +++++++++++++----- 2 files changed, 227 insertions(+), 59 deletions(-) diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs index afac4e6..72f9614 100644 --- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs +++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs @@ -87,5 +87,59 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.IsFalse(shouldPreserve); } + + [TestMethod] + public void ShouldAllowFragmentPlanarFallback_ShouldDisableHoisting() + { + Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Hoisting)); + Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Ground)); + Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Rail)); + } + + [TestMethod] + public void ResolveCurrentRotationBaseline_ShouldPreferActualGeometryForHoisting() + { + Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.0, 1.0); + Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678); + + Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline( + PathType.Hoisting, + isRealObjectMode: true, + hasTrackedRotation: true, + trackedRotation: trackedRotation, + hasReferenceRotation: false, + referenceRotation: Rotation3D.Identity, + transformRotation: Rotation3D.Identity, + hasActualGeometryRotation: true, + actualGeometryRotation: actualGeometryRotation); + + Assert.AreEqual(actualGeometryRotation.A, resolved.A, 1e-9); + Assert.AreEqual(actualGeometryRotation.B, resolved.B, 1e-9); + Assert.AreEqual(actualGeometryRotation.C, resolved.C, 1e-9); + Assert.AreEqual(actualGeometryRotation.D, resolved.D, 1e-9); + } + + [TestMethod] + public void ResolveCurrentRotationBaseline_ShouldKeepTrackedRotationForGround() + { + Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.38268343, 0.92387953); + Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678); + + Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline( + PathType.Ground, + isRealObjectMode: true, + hasTrackedRotation: true, + trackedRotation: trackedRotation, + hasReferenceRotation: false, + referenceRotation: Rotation3D.Identity, + transformRotation: Rotation3D.Identity, + hasActualGeometryRotation: true, + actualGeometryRotation: actualGeometryRotation); + + Assert.AreEqual(trackedRotation.A, resolved.A, 1e-9); + Assert.AreEqual(trackedRotation.B, resolved.B, 1e-9); + Assert.AreEqual(trackedRotation.C, resolved.C, 1e-9); + Assert.AreEqual(trackedRotation.D, resolved.D, 1e-9); + } } } diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index ad1507f..6cdfe7d 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -2774,23 +2774,25 @@ namespace NavisworksTransport.Core.Animation Rotation3D currentRotation; Rotation3D currentRotationForTransform; Rotation3D actualGeometryRotation = Rotation3D.Identity; + bool hasActualGeometryRotation = false; Rotation3D appliedTargetRotation = newRotation; Rotation3D targetRotationForTransform = newRotation; Point3D appliedTargetPosition = newPosition; - if (_hasTrackedRotation) - { - currentRotation = _trackedRotation; - } - else if (IsRealObjectMode && _hasRealObjectReferenceRotation) - { - currentRotation = CoordinateSystemManager.Instance - .CreateHostAdapter() - .FromHostQuaternionDirect(_realObjectReferenceRotation); - } - else - { - currentRotation = controlledObject.Transform.Factor().Rotation; - } + Rotation3D referenceRotation = IsRealObjectMode && _hasRealObjectReferenceRotation + ? CoordinateSystemManager.Instance.CreateHostAdapter().FromHostQuaternionDirect(_realObjectReferenceRotation) + : Rotation3D.Identity; + Rotation3D transformRotation = controlledObject.Transform.Factor().Rotation; + PathType currentPathType = _route?.PathType ?? PathType.Ground; + currentRotation = ResolveCurrentRotationBaseline( + currentPathType, + IsRealObjectMode, + _hasTrackedRotation, + _trackedRotation, + _hasRealObjectReferenceRotation, + referenceRotation, + transformRotation, + hasActualGeometryRotation: false, + actualGeometryRotation: Rotation3D.Identity); try { @@ -2805,6 +2807,17 @@ namespace NavisworksTransport.Core.Animation _route?.PathType == PathType.Rail) && ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out actualGeometryRotation)) { + hasActualGeometryRotation = true; + currentRotation = ResolveCurrentRotationBaseline( + currentPathType, + IsRealObjectMode, + _hasTrackedRotation, + _trackedRotation, + _hasRealObjectReferenceRotation, + referenceRotation, + transformRotation, + hasActualGeometryRotation, + actualGeometryRotation); var trackedLinear = new Transform3D(currentRotation).Linear; var actualLinear = new Transform3D(actualGeometryRotation).Linear; string poseTag = _route?.PathType == PathType.Rail @@ -3987,6 +4000,42 @@ namespace NavisworksTransport.Core.Animation return pathType == PathType.Rail || pathType == PathType.Hoisting; } + internal static bool ShouldAllowFragmentPlanarFallback(PathType pathType) + { + return pathType != PathType.Hoisting; + } + + internal static Rotation3D ResolveCurrentRotationBaseline( + PathType pathType, + bool isRealObjectMode, + bool hasTrackedRotation, + Rotation3D trackedRotation, + bool hasReferenceRotation, + Rotation3D referenceRotation, + Rotation3D transformRotation, + bool hasActualGeometryRotation, + Rotation3D actualGeometryRotation) + { + if (isRealObjectMode && + pathType == PathType.Hoisting && + hasActualGeometryRotation) + { + return actualGeometryRotation; + } + + if (hasTrackedRotation) + { + return trackedRotation; + } + + if (isRealObjectMode && hasReferenceRotation) + { + return referenceRotation; + } + + return transformRotation; + } + private void SyncTrackedRotationToDisplayedPose(ModelItem sourceObject) { if (sourceObject == null) @@ -4159,6 +4208,71 @@ namespace NavisworksTransport.Core.Animation return true; } + private bool TryResolveHoistingActualPose( + Vector3 hostForward, + out Rotation3D rotation, + out Quaternion baselineQuaternion, + out LocalAxisDirection selectedAxisDirection, + out Vector3 selectedAxisLocal, + out Vector3 projectedForward) + { + rotation = Rotation3D.Identity; + baselineQuaternion = Quaternion.Identity; + selectedAxisDirection = LocalAxisDirection.PositiveX; + selectedAxisLocal = Vector3.UnitX; + projectedForward = Vector3.Zero; + + if (_animatedObject == null || + !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D)) + { + return false; + } + + var actualRotation = new Quaternion( + (float)actualRotation3D.A, + (float)actualRotation3D.B, + (float)actualRotation3D.C, + (float)actualRotation3D.D); + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ; + if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose( + actualRotation, + hostForward, + hostUp, + out baselineQuaternion, + out selectedAxisDirection, + out selectedAxisLocal, + out projectedForward)) + { + return false; + } + + rotation = adapter.FromHostQuaternionDirect( + adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection)); + + Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion); + Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion( + (float)rotation.A, + (float)rotation.B, + (float)rotation.C, + (float)rotation.D)); + + LogManager.Debug( + $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。"); + LogManager.Debug( + $"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " + + $"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " + + $"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " + + $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " + + $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " + + $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " + + $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " + + $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " + + $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})"); + return true; + } + private static Quaternion Rotation3DToHostQuaternion(Rotation3D rotation) { var linear = new Transform3D(rotation).Linear; @@ -4357,8 +4471,9 @@ namespace NavisworksTransport.Core.Animation convention = null; extents = (0.0, 0.0, 0.0); + bool requiresReferenceRotation = _route?.PathType != PathType.Hoisting; if (!IsRealObjectMode || - !_hasRealObjectReferenceRotation || + (requiresReferenceRotation && !_hasRealObjectReferenceRotation) || _realObjectLength <= 0.0 || _realObjectWidth <= 0.0 || _realObjectHeight <= 0.0) @@ -4386,8 +4501,9 @@ namespace NavisworksTransport.Core.Animation convention = null; extents = (0.0, 0.0, 0.0); + bool requiresReferenceRotation = _route?.PathType != PathType.Hoisting; if (!IsRealObjectMode || - !_hasRealObjectReferenceRotation || + (requiresReferenceRotation && !_hasRealObjectReferenceRotation) || _realObjectLength <= 0.0 || _realObjectWidth <= 0.0 || _realObjectHeight <= 0.0) @@ -4432,6 +4548,38 @@ namespace NavisworksTransport.Core.Animation return true; } + if (_route?.PathType == PathType.Hoisting) + { + if (!TryResolveHoistingActualPose( + hostForward, + out var hoistingRotation, + out var baselineQuaternion, + out var selectedAxisDirection, + out _, + out _)) + { + return false; + } + + LocalAxisDirection hoistingSemanticUpAxis = + adapter.HostType == CoordinateSystemType.YUp + ? LocalAxisDirection.PositiveY + : LocalAxisDirection.PositiveZ; + convention = new ModelAxisConvention(selectedAxisDirection, hoistingSemanticUpAxis); + + Quaternion hoistingFinalHostQuaternion = Rotation3DToHostQuaternion(hoistingRotation); + extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents( + convention, + _realObjectLength, + _realObjectWidth, + _realObjectHeight, + baselineQuaternion, + hoistingFinalHostQuaternion, + targetFrame.Forward, + targetFrame.Up); + return true; + } + if (!TryCreateRealObjectPlanarPoseSolution( targetFrame.Forward, targetFrame.Up, @@ -4761,6 +4909,12 @@ namespace NavisworksTransport.Core.Animation return true; } + if (_route?.PathType == PathType.Hoisting) + { + LogManager.Error("[吊装真实物体姿态] 无法基于实际几何姿态生成起点姿态,已禁止回退到 fragment 参考姿态。"); + return false; + } + if (!PathTargetFrameResolver.TryCreatePlanarHostFrame( hostForward, CoordinateSystemManager.Instance.CreateHostAdapter().HostType, @@ -4800,27 +4954,10 @@ namespace NavisworksTransport.Core.Animation private bool TryCreateHoistingRealObjectRotationFromActualPose(Vector3 hostForward, out Rotation3D rotation) { - rotation = Rotation3D.Identity; - - if (_animatedObject == null || - !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D)) - { - return false; - } - - var actualRotation = new Quaternion( - (float)actualRotation3D.A, - (float)actualRotation3D.B, - (float)actualRotation3D.C, - (float)actualRotation3D.D); - - var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); - Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ; - if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose( - actualRotation, + if (!TryResolveHoistingActualPose( hostForward, - hostUp, - out var baselineQuaternion, + out rotation, + out _, out var selectedAxisDirection, out var selectedAxisLocal, out var projectedForward)) @@ -4828,31 +4965,8 @@ namespace NavisworksTransport.Core.Animation return false; } - rotation = adapter.FromHostQuaternionDirect( - adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection)); - - Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion); - Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion( - (float)rotation.A, - (float)rotation.B, - (float)rotation.C, - (float)rotation.D)); - _realObjectPlanarSelectedForwardAxis = selectedAxisDirection; _hasRealObjectPlanarSelectedForwardAxis = true; - - LogManager.Debug( - $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。"); - LogManager.Debug( - $"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " + - $"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " + - $"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " + - $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " + - $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " + - $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " + - $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " + - $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " + - $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})"); return true; } From 9e8fea324116ec980c25ca8b9597a322d95162eb Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 2 Apr 2026 00:19:36 +0800 Subject: [PATCH 55/87] Unify real-object override rotation application --- .../CoordinateSystem/RailPreservedPoseTests.cs | 7 +++++++ src/Core/Animation/PathAnimationManager.cs | 17 +++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs index 72f9614..9d3785d 100644 --- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs +++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs @@ -141,5 +141,12 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.AreEqual(trackedRotation.C, resolved.C, 1e-9); Assert.AreEqual(trackedRotation.D, resolved.D, 1e-9); } + + [TestMethod] + public void ShouldUseRealObjectOverrideRotation_ShouldEnableForRealObjects() + { + Assert.IsTrue(PathAnimationManager.ShouldUseRealObjectOverrideRotation(true)); + Assert.IsFalse(PathAnimationManager.ShouldUseRealObjectOverrideRotation(false)); + } } } diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 6cdfe7d..7e448da 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -2748,7 +2748,7 @@ namespace NavisworksTransport.Core.Animation try { ModelItem controlledObject = CurrentControlledObject; - bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && controlledObject != null; + bool usesRealObjectOverrideRotation = ShouldUseRealObjectOverrideRotation(IsRealObjectMode) && controlledObject != null; if (controlledObject == null) { return; @@ -2841,7 +2841,7 @@ namespace NavisworksTransport.Core.Animation } currentRotationForTransform = currentRotation; - if (isRailRealObject) + if (usesRealObjectOverrideRotation) { if (ModelItemTransformHelper.TryGetCurrentOverrideRotation(controlledObject, out var currentOverrideRotation)) { @@ -2896,11 +2896,11 @@ namespace NavisworksTransport.Core.Animation $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); - if (isRailRealObject) + if (usesRealObjectOverrideRotation) { var overrideLinear = new Transform3D(targetRotationForTransform).Linear; LogManager.Debug( - $"[Rail覆盖姿态] {controlledObject.DisplayName} Override目标: " + + $"[真实物体覆盖姿态] {controlledObject.DisplayName} Override目标: " + $"X=({overrideLinear.Get(0, 0):F4},{overrideLinear.Get(1, 0):F4},{overrideLinear.Get(2, 0):F4}), " + $"Y=({overrideLinear.Get(0, 1):F4},{overrideLinear.Get(1, 1):F4},{overrideLinear.Get(2, 1):F4}), " + $"Z=({overrideLinear.Get(0, 2):F4},{overrideLinear.Get(1, 2):F4},{overrideLinear.Get(2, 2):F4})"); @@ -2918,11 +2918,11 @@ namespace NavisworksTransport.Core.Animation _hasTrackedRotation = true; _currentYaw = ModelItemTransformHelper.GetYawFromRotation(appliedTargetRotation); - if (isRailRealObject) + if (usesRealObjectOverrideRotation) { Point3D hostActualAfter = GetTrackedObjectPosition(controlledObject); LogManager.Debug( - $"[Rail姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " + + $"[真实物体姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " + $"目标跟踪中心=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " + $"偏差=({hostActualAfter.X - newPosition.X:F3},{hostActualAfter.Y - newPosition.Y:F3},{hostActualAfter.Z - newPosition.Z:F3})"); } @@ -4036,6 +4036,11 @@ namespace NavisworksTransport.Core.Animation return transformRotation; } + internal static bool ShouldUseRealObjectOverrideRotation(bool isRealObjectMode) + { + return isRealObjectMode; + } + private void SyncTrackedRotationToDisplayedPose(ModelItem sourceObject) { if (sourceObject == null) From 24951d4205f585af364f6166b12cace8f9319ee8 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 2 Apr 2026 01:32:31 +0800 Subject: [PATCH 56/87] Stabilize hoisting start-pose logging and caching --- .../RailPreservedPoseTests.cs | 7 + src/Core/Animation/PathAnimationManager.cs | 305 +++++++++++------- .../ViewModels/AnimationControlViewModel.cs | 5 +- 3 files changed, 199 insertions(+), 118 deletions(-) diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs index 9d3785d..1e8326a 100644 --- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs +++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs @@ -148,5 +148,12 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.IsTrue(PathAnimationManager.ShouldUseRealObjectOverrideRotation(true)); Assert.IsFalse(PathAnimationManager.ShouldUseRealObjectOverrideRotation(false)); } + + [TestMethod] + public void ResolveHoistingPoseSourceLabel_ShouldDescribeCachedBasePose() + { + Assert.AreEqual("缓存基姿态", PathAnimationManager.ResolveHoistingPoseSourceLabel(true)); + Assert.AreEqual("实际几何姿态基线", PathAnimationManager.ResolveHoistingPoseSourceLabel(false)); + } } } diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 7e448da..76eb2d2 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -198,6 +198,8 @@ namespace NavisworksTransport.Core.Animation private Rotation3D _hoistingRealObjectBaseRotation = Rotation3D.Identity; private double _hoistingRealObjectBaseYaw = 0.0; private bool _hasHoistingRealObjectBasePose = false; + private Rotation3D _lastResolvedHoistingStartBaselineRotation = Rotation3D.Identity; + private bool _hasLastResolvedHoistingStartBaselineRotation = false; private Vector3D _groundRealObjectStartCompensation = new Vector3D(0, 0, 0); private bool _hasGroundRealObjectStartCompensation = false; private bool _suppressGroundRealObjectCompensation = false; @@ -856,7 +858,15 @@ namespace NavisworksTransport.Core.Animation } else if (IsRealObjectMode && _route.PathType == PathType.Hoisting) { - _hoistingRealObjectBaseRotation = planarRotation; + if (_hasLastResolvedHoistingStartBaselineRotation) + { + _hoistingRealObjectBaseRotation = _lastResolvedHoistingStartBaselineRotation; + } + else if (!TryCreateHoistingPlanarBaseRotationAtStart(out _hoistingRealObjectBaseRotation)) + { + _hoistingRealObjectBaseRotation = planarRotation; + } + _hasHoistingRealObjectBasePose = TryResolvePlanarRealObjectBaseYaw(PathType.Hoisting, out _hoistingRealObjectBaseYaw); LogManager.Debug( $"[Hoisting真实物体基姿态] {(animatedObject ?? _animatedObject)?.DisplayName} BaseYaw={_hoistingRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " + @@ -2823,8 +2833,11 @@ namespace NavisworksTransport.Core.Animation string poseTag = _route?.PathType == PathType.Rail ? "[Rail姿态基线诊断]" : "[平面姿态基线诊断]"; + string baselineLabel = usesRealObjectOverrideRotation + ? "当前姿态基线" + : "当前跟踪姿态"; LogManager.Debug( - $"{poseTag} {controlledObject.DisplayName} 跟踪旋转: " + + $"{poseTag} {controlledObject.DisplayName} {baselineLabel}: " + $"X=({trackedLinear.Get(0, 0):F4},{trackedLinear.Get(1, 0):F4},{trackedLinear.Get(2, 0):F4}), " + $"Y=({trackedLinear.Get(0, 1):F4},{trackedLinear.Get(1, 1):F4},{trackedLinear.Get(2, 1):F4}), " + $"Z=({trackedLinear.Get(0, 2):F4},{trackedLinear.Get(1, 2):F4},{trackedLinear.Get(2, 2):F4})"); @@ -2880,30 +2893,33 @@ namespace NavisworksTransport.Core.Animation $"应用目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})"); } - var currentLinear = new Transform3D(currentRotationForTransform).Linear; - var targetLinear = new Transform3D(appliedTargetRotation).Linear; + var hostCurrentPoseLocalAxesLinear = new Transform3D(currentRotationForTransform).Linear; + var hostTargetDisplayPoseLocalAxesLinear = new Transform3D(appliedTargetRotation).Linear; LogManager.Debug( $"[动画姿态入口] {controlledObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " + $"目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})"); + string currentRotationLabel = usesRealObjectOverrideRotation + ? "宿主覆盖姿态基线局部轴" + : "宿主当前跟踪姿态局部轴"; LogManager.Debug( - $"[动画姿态入口] {controlledObject.DisplayName} 跟踪姿态: " + - $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " + - $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " + - $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})"); + $"[动画姿态入口] {controlledObject.DisplayName} {currentRotationLabel}: " + + $"hostCurrentLocalXAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 0):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 0):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 0):F4}), " + + $"hostCurrentLocalYAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 1):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 1):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 1):F4}), " + + $"hostCurrentLocalZAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 2):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 2):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 2):F4})"); LogManager.Debug( - $"[动画姿态入口] {controlledObject.DisplayName} 目标姿态: " + - $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + - $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + - $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + $"[动画姿态入口] {controlledObject.DisplayName} 宿主目标显示姿态局部轴: " + + $"hostTargetDisplayLocalXAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 0):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 0):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 0):F4}), " + + $"hostTargetDisplayLocalYAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 1):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 1):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 1):F4}), " + + $"hostTargetDisplayLocalZAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 2):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 2):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 2):F4})"); if (usesRealObjectOverrideRotation) { - var overrideLinear = new Transform3D(targetRotationForTransform).Linear; + var hostTargetOverridePoseLocalAxesLinear = new Transform3D(targetRotationForTransform).Linear; LogManager.Debug( - $"[真实物体覆盖姿态] {controlledObject.DisplayName} Override目标: " + - $"X=({overrideLinear.Get(0, 0):F4},{overrideLinear.Get(1, 0):F4},{overrideLinear.Get(2, 0):F4}), " + - $"Y=({overrideLinear.Get(0, 1):F4},{overrideLinear.Get(1, 1):F4},{overrideLinear.Get(2, 1):F4}), " + - $"Z=({overrideLinear.Get(0, 2):F4},{overrideLinear.Get(1, 2):F4},{overrideLinear.Get(2, 2):F4})"); + $"[真实物体覆盖姿态] {controlledObject.DisplayName} 宿主目标覆盖姿态局部轴: " + + $"hostTargetOverrideLocalXAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 0):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 0):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 0):F4}), " + + $"hostTargetOverrideLocalYAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 1):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 1):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 1):F4}), " + + $"hostTargetOverrideLocalZAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 2):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 2):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 2):F4})"); } ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation( @@ -4174,45 +4190,6 @@ namespace NavisworksTransport.Core.Animation return true; } - private bool TryCreateHoistingRealObjectConstrainedRotationFromHostForward( - Vector3 hostForward, - out Rotation3D rotation) - { - rotation = Rotation3D.Identity; - if (!IsRealObjectMode || - _route?.PathType != PathType.Hoisting || - !_hasHoistingRealObjectBasePose) - { - return false; - } - - if (!PathTargetFrameResolver.TryCreatePlanarHostFrame( - hostForward, - CoordinateSystemManager.Instance.ResolvedType, - out var currentFrame)) - { - return false; - } - - if (!PathTargetFrameResolver.TryResolvePlanarHostYaw( - currentFrame.Forward, - CoordinateSystemManager.Instance.ResolvedType, - out double targetYawRadians)) - { - return false; - } - - var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); - Quaternion baseQuaternion = Rotation3DToHostQuaternion(_hoistingRealObjectBaseRotation); - Quaternion targetQuaternion = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose( - baseQuaternion, - _hoistingRealObjectBaseYaw, - targetYawRadians, - Vector3.Normalize(adapter.HostUpVector3)); - rotation = adapter.FromHostQuaternionDirect(targetQuaternion); - return true; - } - private bool TryResolveHoistingActualPose( Vector3 hostForward, out Rotation3D rotation, @@ -4226,58 +4203,127 @@ namespace NavisworksTransport.Core.Animation selectedAxisDirection = LocalAxisDirection.PositiveX; selectedAxisLocal = Vector3.UnitX; projectedForward = Vector3.Zero; - - if (_animatedObject == null || - !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D)) - { - return false; - } - - var actualRotation = new Quaternion( - (float)actualRotation3D.A, - (float)actualRotation3D.B, - (float)actualRotation3D.C, - (float)actualRotation3D.D); - - var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); - Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ; - if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose( - actualRotation, + string sourceLabel; + if (!TryResolveHoistingStartBaselinePose( hostForward, - hostUp, out baselineQuaternion, out selectedAxisDirection, out selectedAxisLocal, - out projectedForward)) + out projectedForward, + out sourceLabel)) { + rotation = Rotation3D.Identity; return false; } - rotation = adapter.FromHostQuaternionDirect( - adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection)); + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + Quaternion finalHostQuaternion = adapter.ComposeHostQuaternion( + baselineQuaternion, + _objectRotationCorrection); + _lastResolvedHoistingStartBaselineRotation = adapter.FromHostQuaternionDirect(baselineQuaternion); + _hasLastResolvedHoistingStartBaselineRotation = true; + rotation = adapter.FromHostQuaternionDirect(finalHostQuaternion); - Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion); - Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion( - (float)rotation.A, - (float)rotation.B, - (float)rotation.C, - (float)rotation.D)); + Matrix4x4 hostBaselineLocalAxesLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion); + Matrix4x4 hostComposedLocalAxesLinear = Matrix4x4.CreateFromQuaternion(finalHostQuaternion); LogManager.Debug( - $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。"); + $"[真实物体平面前进轴] Hoisting 已改用{sourceLabel},选中对象轴={selectedAxisDirection}。"); LogManager.Debug( $"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " + $"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " + $"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " + - $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " + - $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " + - $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " + - $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " + - $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " + - $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})"); + $"hostBaselineLocalXAxis=({hostBaselineLocalAxesLinear.M11:F4},{hostBaselineLocalAxesLinear.M21:F4},{hostBaselineLocalAxesLinear.M31:F4}), " + + $"hostBaselineLocalYAxis=({hostBaselineLocalAxesLinear.M12:F4},{hostBaselineLocalAxesLinear.M22:F4},{hostBaselineLocalAxesLinear.M32:F4}), " + + $"hostBaselineLocalZAxis=({hostBaselineLocalAxesLinear.M13:F4},{hostBaselineLocalAxesLinear.M23:F4},{hostBaselineLocalAxesLinear.M33:F4}), " + + $"hostComposedLocalXAxis=({hostComposedLocalAxesLinear.M11:F4},{hostComposedLocalAxesLinear.M21:F4},{hostComposedLocalAxesLinear.M31:F4}), " + + $"hostComposedLocalYAxis=({hostComposedLocalAxesLinear.M12:F4},{hostComposedLocalAxesLinear.M22:F4},{hostComposedLocalAxesLinear.M32:F4}), " + + $"hostComposedLocalZAxis=({hostComposedLocalAxesLinear.M13:F4},{hostComposedLocalAxesLinear.M23:F4},{hostComposedLocalAxesLinear.M33:F4})"); return true; } + private bool TryResolveHoistingStartBaselinePose( + Vector3 hostForward, + out Quaternion baselineQuaternion, + out LocalAxisDirection selectedAxisDirection, + out Vector3 selectedAxisLocal, + out Vector3 projectedForward, + out string sourceLabel) + { + baselineQuaternion = Quaternion.Identity; + selectedAxisDirection = _hasRealObjectPlanarSelectedForwardAxis + ? _realObjectPlanarSelectedForwardAxis + : LocalAxisDirection.PositiveX; + selectedAxisLocal = Vector3.UnitX; + projectedForward = Vector3.Zero; + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ; + Vector3 normalizedHostForward = hostForward.LengthSquared() > 1e-6f + ? Vector3.Normalize(hostForward) + : Vector3.UnitX; + Vector3 planarProjectedForward = normalizedHostForward - Vector3.Dot(normalizedHostForward, hostUp) * hostUp; + projectedForward = planarProjectedForward.LengthSquared() > 1e-6f + ? Vector3.Normalize(planarProjectedForward) + : Vector3.UnitX; + + if (_hasHoistingRealObjectBasePose) + { + sourceLabel = ResolveHoistingPoseSourceLabel(true); + Quaternion cachedBaseQuaternion = Rotation3DToHostQuaternion(_hoistingRealObjectBaseRotation); + if (!PathTargetFrameResolver.TryCreatePlanarHostFrame( + projectedForward, + CoordinateSystemManager.Instance.ResolvedType, + out var currentFrame)) + { + return false; + } + + if (!PathTargetFrameResolver.TryResolvePlanarHostYaw( + currentFrame.Forward, + CoordinateSystemManager.Instance.ResolvedType, + out double targetYawRadians)) + { + return false; + } + + baselineQuaternion = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose( + cachedBaseQuaternion, + _hoistingRealObjectBaseYaw, + targetYawRadians, + Vector3.Normalize(adapter.HostUpVector3)); + selectedAxisLocal = ResolveReferenceWorldAxisForLocalDirection( + selectedAxisDirection, + _realObjectReferenceAxisX, + _realObjectReferenceAxisY, + _realObjectReferenceAxisZ); + return true; + } + + sourceLabel = ResolveHoistingPoseSourceLabel(false); + Rotation3D sourceRotation; + if (_animatedObject == null || + !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out sourceRotation)) + { + return false; + } + + Quaternion sourceQuaternion = Rotation3DToHostQuaternion(sourceRotation); + return HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose( + sourceQuaternion, + projectedForward, + hostUp, + out baselineQuaternion, + out selectedAxisDirection, + out selectedAxisLocal, + out projectedForward); + } + + internal static string ResolveHoistingPoseSourceLabel(bool hasHoistingBasePose) + { + return hasHoistingBasePose ? "缓存基姿态" : "实际几何姿态基线"; + } + private static Quaternion Rotation3DToHostQuaternion(Rotation3D rotation) { var linear = new Transform3D(rotation).Linear; @@ -4726,16 +4772,16 @@ namespace NavisworksTransport.Core.Animation localCorrection); rotation = adapter.FromHostQuaternionDirect(composedHostQuaternion); - Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion); - Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion); + Matrix4x4 hostBaselineLocalAxesLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion); + Matrix4x4 hostComposedLocalAxesLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion); LogManager.Debug( - $"[Rail真实物体角度修正] BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " + - $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " + - $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " + + $"[Rail真实物体角度修正] hostBaselineLocalXAxis=({hostBaselineLocalAxesLinear.M11:F4},{hostBaselineLocalAxesLinear.M21:F4},{hostBaselineLocalAxesLinear.M31:F4}), " + + $"hostBaselineLocalYAxis=({hostBaselineLocalAxesLinear.M12:F4},{hostBaselineLocalAxesLinear.M22:F4},{hostBaselineLocalAxesLinear.M32:F4}), " + + $"hostBaselineLocalZAxis=({hostBaselineLocalAxesLinear.M13:F4},{hostBaselineLocalAxesLinear.M23:F4},{hostBaselineLocalAxesLinear.M33:F4}), " + $"宿主修正={_objectRotationCorrection}, 本地修正={localCorrection}, " + - $"HostComposeX=({composedLinear.M11:F4},{composedLinear.M21:F4},{composedLinear.M31:F4}), " + - $"HostComposeY=({composedLinear.M12:F4},{composedLinear.M22:F4},{composedLinear.M32:F4}), " + - $"HostComposeZ=({composedLinear.M13:F4},{composedLinear.M23:F4},{composedLinear.M33:F4})"); + $"hostComposedLocalXAxis=({hostComposedLocalAxesLinear.M11:F4},{hostComposedLocalAxesLinear.M21:F4},{hostComposedLocalAxesLinear.M31:F4}), " + + $"hostComposedLocalYAxis=({hostComposedLocalAxesLinear.M12:F4},{hostComposedLocalAxesLinear.M22:F4},{hostComposedLocalAxesLinear.M32:F4}), " + + $"hostComposedLocalZAxis=({hostComposedLocalAxesLinear.M13:F4},{hostComposedLocalAxesLinear.M23:F4},{hostComposedLocalAxesLinear.M33:F4})"); return true; } @@ -4782,6 +4828,43 @@ namespace NavisworksTransport.Core.Animation return false; } + private bool TryCreateHoistingPlanarBaseRotationAtStart(out Rotation3D rotation) + { + rotation = Rotation3D.Identity; + + if (!IsRealObjectMode || + _route?.PathType != PathType.Hoisting || + _pathPoints == null || + _pathPoints.Count < 2) + { + return false; + } + + if (!PathTargetFrameResolver.TryResolvePlanarStartHostForward( + PathType.Hoisting, + _pathPoints, + out var hostForward)) + { + return false; + } + + if (!TryResolveHoistingStartBaselinePose( + hostForward, + out var baselineQuaternion, + out _, + out _, + out _, + out _)) + { + return false; + } + + rotation = CoordinateSystemManager.Instance + .CreateHostAdapter() + .FromHostQuaternionDirect(baselineQuaternion); + return true; + } + private bool TryCreatePlanarPathRotationForFrame(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation) { rotation = Rotation3D.Identity; @@ -4901,13 +4984,6 @@ namespace NavisworksTransport.Core.Animation { rotation = Rotation3D.Identity; - if (_route?.PathType == PathType.Hoisting && - _hasHoistingRealObjectBasePose && - TryCreateHoistingRealObjectConstrainedRotationFromHostForward(hostForward, out rotation)) - { - return true; - } - if (_route?.PathType == PathType.Hoisting && TryCreateHoistingRealObjectRotationFromActualPose(hostForward, out rotation)) { @@ -4941,19 +5017,19 @@ namespace NavisworksTransport.Core.Animation Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(solution.BaselineRotation, localCorrection); rotation = adapter.FromHostQuaternionDirect(hostComposedQuaternion); - Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(solution.BaselineRotation); - Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(hostComposedQuaternion); + Matrix4x4 hostBaselineLocalAxesLinear = Matrix4x4.CreateFromQuaternion(solution.BaselineRotation); + Matrix4x4 hostComposedLocalAxesLinear = Matrix4x4.CreateFromQuaternion(hostComposedQuaternion); LogManager.Debug( $"[真实物体起点姿态] 选中参考轴=({solution.SelectedReferenceAxisLocal.X:F3},{solution.SelectedReferenceAxisLocal.Y:F3},{solution.SelectedReferenceAxisLocal.Z:F3}), " + $"投影前进=({solution.ProjectedForward.X:F3},{solution.ProjectedForward.Y:F3},{solution.ProjectedForward.Z:F3}), " + $"宿主修正={_objectRotationCorrection}, 本地修正={localCorrection}, " + - $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " + - $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " + - $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " + - $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " + - $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " + - $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})"); + $"hostBaselineLocalXAxis=({hostBaselineLocalAxesLinear.M11:F4},{hostBaselineLocalAxesLinear.M21:F4},{hostBaselineLocalAxesLinear.M31:F4}), " + + $"hostBaselineLocalYAxis=({hostBaselineLocalAxesLinear.M12:F4},{hostBaselineLocalAxesLinear.M22:F4},{hostBaselineLocalAxesLinear.M32:F4}), " + + $"hostBaselineLocalZAxis=({hostBaselineLocalAxesLinear.M13:F4},{hostBaselineLocalAxesLinear.M23:F4},{hostBaselineLocalAxesLinear.M33:F4}), " + + $"hostComposedLocalXAxis=({hostComposedLocalAxesLinear.M11:F4},{hostComposedLocalAxesLinear.M21:F4},{hostComposedLocalAxesLinear.M31:F4}), " + + $"hostComposedLocalYAxis=({hostComposedLocalAxesLinear.M12:F4},{hostComposedLocalAxesLinear.M22:F4},{hostComposedLocalAxesLinear.M32:F4}), " + + $"hostComposedLocalZAxis=({hostComposedLocalAxesLinear.M13:F4},{hostComposedLocalAxesLinear.M23:F4},{hostComposedLocalAxesLinear.M33:F4})"); return true; } @@ -5298,6 +5374,8 @@ namespace NavisworksTransport.Core.Animation _hoistingRealObjectBaseRotation = Rotation3D.Identity; _hoistingRealObjectBaseYaw = 0.0; _hasHoistingRealObjectBasePose = false; + _lastResolvedHoistingStartBaselineRotation = Rotation3D.Identity; + _hasLastResolvedHoistingStartBaselineRotation = false; } private LocalEulerRotationCorrection ResolveRealObjectLocalRotationCorrection() @@ -5317,7 +5395,6 @@ namespace NavisworksTransport.Core.Animation _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose; _hasPathPreservedPoseRotation = false; _objectRotationCorrection = rotationCorrection; - ResetPlanarRealObjectBasePoseCache(); // 如果动画已创建,更新物体到起点的朝向 if (_animatedObject != null && _pathPoints != null && _pathPoints.Count > 0) diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 4689bb8..9d7e22e 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -2173,10 +2173,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels ObjectRotationCorrection = placementRequest.RotationCorrection; LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}"); UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}"); - - // 应用角度修正到物体 - UpdateObjectRotation(); - + // 更新通行空间可视化(考虑旋转后的尺寸) UpdatePassageSpaceVisualization(); } From 4ec4cf77eed7639a6398241e61bdec5992be69db Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 2 Apr 2026 22:14:26 +0800 Subject: [PATCH 57/87] Refine path viewpoint behavior and config editing --- .../CoordinateSystem/ViewpointHelperTests.cs | 35 ++++---- resources/default_config.toml | 13 +++ src/Core/Config/ConfigManager.cs | 4 + src/Core/Config/SystemConfig.cs | 30 +++++++ .../ViewModels/LogisticsControlViewModel.cs | 8 +- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 9 +- .../ViewModels/SystemManagementViewModel.cs | 7 +- src/UI/WPF/Views/ConfigEditorDialog.xaml.cs | 54 +++++++++--- src/Utils/ViewpointHelper.cs | 82 ++++++++++++------- 9 files changed, 180 insertions(+), 62 deletions(-) diff --git a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs index 90dad79..168e72a 100644 --- a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs +++ b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NavisworksTransport.Utils; using System.Numerics; +using NavisworksTransport.Core.Config; namespace NavisworksTransport.UnitTests.CoordinateSystem { @@ -10,32 +11,35 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem [TestMethod] public void ResolvePathViewpointProfile_ShouldReturnExpectedDefaults() { + var config = ConfigManager.Instance.Current.PathEditing; var ground = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathGroundSelection); var hoisting = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection); var rail = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection); - Assert.AreEqual(1.0, ground.DistanceScale, 1e-9); - Assert.AreEqual(12.0, ground.MinDistanceMeters, 1e-9); + Assert.AreEqual(12.0, ground.CameraDistanceMeters, 1e-9); + Assert.AreEqual(90.0, ground.ElevationDegrees, 1e-9); - Assert.AreEqual(1.2, hoisting.DistanceScale, 1e-9); - Assert.AreEqual(8.0, hoisting.MinDistanceMeters, 1e-9); - Assert.AreEqual(22.0, hoisting.ElevationDegrees, 1e-9); + Assert.AreEqual(config.HoistingViewDistanceMeters, hoisting.CameraDistanceMeters, 1e-9); + Assert.AreEqual(config.HoistingViewElevationDegrees, hoisting.ElevationDegrees, 1e-9); + Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, hoisting.HorizontalMode); - Assert.AreEqual(0.75, rail.DistanceScale, 1e-9); - Assert.AreEqual(4.5, rail.MinDistanceMeters, 1e-9); - Assert.AreEqual(12.0, rail.ElevationDegrees, 1e-9); + Assert.AreEqual(config.RailViewDistanceMeters, rail.CameraDistanceMeters, 1e-9); + Assert.AreEqual(config.RailViewElevationDegrees, rail.ElevationDegrees, 1e-9); + Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, rail.HorizontalMode); } [TestMethod] - public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromBehindAndAbove() + public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromSideAndSlightlyAbove() { Vector3 hostUp = Vector3.UnitZ; Vector3 forward = Vector3.UnitX; + var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection); - Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Hoisting, hostUp, forward); + Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward); - Assert.IsTrue(offset.X < -0.8f); - Assert.IsTrue(offset.Z > 0.3f); + Assert.AreEqual(0.0f, offset.X, 1e-5f); + Assert.IsTrue(offset.Y > 0.0f); + Assert.IsTrue(offset.Z > 0.0f); } [TestMethod] @@ -43,12 +47,13 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem { Vector3 hostUp = Vector3.UnitZ; Vector3 forward = Vector3.UnitX; + var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection); - Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Rail, hostUp, forward); + Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward); Assert.AreEqual(0.0f, offset.X, 1e-5f); - Assert.IsTrue(offset.Y > 0.9f); - Assert.IsTrue(offset.Z > 0.15f); + Assert.IsTrue(offset.Y > 0.0f); + Assert.IsTrue(offset.Z > 0.0f); } [TestMethod] diff --git a/resources/default_config.toml b/resources/default_config.toml index da3e2b4..8825899 100644 --- a/resources/default_config.toml +++ b/resources/default_config.toml @@ -26,6 +26,19 @@ default_path_turn_radius = 2.5 # 圆弧采样步长(米)- 推荐值:0.02-0.1 arc_sampling_step = 0.05 +# 吊装路径自动视角距离(米) +# 默认为近距离低侧视 +hoisting_view_distance_meters = 6.0 + +# 吊装路径自动视角仰角(度) +hoisting_view_elevation_degrees = 30.0 + +# Rail 路径自动视角距离(米) +rail_view_distance_meters = 6.0 + +# Rail 路径自动视角仰角(度) +rail_view_elevation_degrees = 30.0 + [visualization] # 地图边距比例(0-1之间) margin_ratio = 0.1 diff --git a/src/Core/Config/ConfigManager.cs b/src/Core/Config/ConfigManager.cs index a89a216..5b04d96 100644 --- a/src/Core/Config/ConfigManager.cs +++ b/src/Core/Config/ConfigManager.cs @@ -394,6 +394,10 @@ namespace NavisworksTransport.Core.Config config.PathEditing.SafetyMarginMeters = GetDoubleValueWithDefault(pathEdit, "safety_margin_meters", 0.1, missingItems); config.PathEditing.DefaultPathTurnRadiusMeters = GetDoubleValueWithDefault(pathEdit, "default_path_turn_radius", 2.5, missingItems); config.PathEditing.ArcSamplingStepMeters = GetDoubleValueWithDefault(pathEdit, "arc_sampling_step", 0.05, missingItems); + config.PathEditing.HoistingViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "hoisting_view_distance_meters", 6.0, missingItems); + config.PathEditing.HoistingViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "hoisting_view_elevation_degrees", 30.0, missingItems); + config.PathEditing.RailViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "rail_view_distance_meters", 6.0, missingItems); + config.PathEditing.RailViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "rail_view_elevation_degrees", 30.0, missingItems); } } diff --git a/src/Core/Config/SystemConfig.cs b/src/Core/Config/SystemConfig.cs index 024d340..1c424f7 100644 --- a/src/Core/Config/SystemConfig.cs +++ b/src/Core/Config/SystemConfig.cs @@ -119,6 +119,26 @@ namespace NavisworksTransport.Core.Config /// public double ArcSamplingStepMeters { get; set; } + /// + /// 吊装路径自动视角距离(米) + /// + public double HoistingViewDistanceMeters { get; set; } + + /// + /// 吊装路径自动视角仰角(度) + /// + public double HoistingViewElevationDegrees { get; set; } + + /// + /// Rail 路径自动视角距离(米) + /// + public double RailViewDistanceMeters { get; set; } + + /// + /// Rail 路径自动视角仰角(度) + /// + public double RailViewElevationDegrees { get; set; } + // === 模型单位接口(用于内部计算) === /// @@ -160,6 +180,16 @@ namespace NavisworksTransport.Core.Config /// 圆弧采样步长(模型单位) /// public double ArcSamplingStep => ArcSamplingStepMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor(); + + /// + /// 吊装路径自动视角距离(模型单位) + /// + public double HoistingViewDistance => HoistingViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor(); + + /// + /// Rail 路径自动视角距离(模型单位) + /// + public double RailViewDistance => RailViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor(); } /// diff --git a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs index bfe3c2e..17b7089 100644 --- a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs +++ b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs @@ -240,6 +240,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels { if (IsClipBoxEnabled) { + if (_pathPlanningManager.PathEditState == PathEditState.Creating) + { + LogManager.Info("[剖面盒] 当前处于新建路径流程,跳过自动跟随"); + return; + } + LogManager.Info("[剖面盒] 路径已切换,重新设置剖面盒聚焦到新路径"); EnableClipBoxForCurrentRoute(); } @@ -418,4 +424,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels #endregion } -} \ No newline at end of file +} diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 6e01e40..67fb46d 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; @@ -385,8 +385,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels _pathPlanningManager.SetCurrentRoute(coreRoute); LogManager.Info($"UI路径切换:已同步PathPlanningManager的CurrentRoute到 {value.Name}"); + // 新建路径流程里禁止自动改视角,避免打断建路径操作。 + if (_pathPlanningManager.PathEditState == PathEditState.Creating) + { + LogManager.Info($"UI路径切换:当前处于新建路径流程,跳过自动视角调整: {value.Name}"); + } // 自动调整视角到路径中心(只有路径有点时才调整) - if (coreRoute.Points.Count > 0) + else if (coreRoute.Points.Count > 0) { try { diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs index 4468645..4048f76 100644 --- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs @@ -832,14 +832,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels { try { - // 使用单例模式显示配置编辑器(非模态,避免独占焦点) - // 不传递 owner,让 ShowEditor 自己通过 DialogHelper 处理 + // 使用模态配置编辑器,确保键盘输入不会被 Navisworks 主窗口截获 var dialog = NavisworksTransport.UI.WPF.Views.ConfigEditorDialog.ShowEditor(); if (dialog != null) { - UpdateMainStatus("配置编辑器已打开"); - LogManager.Info("配置编辑器:已打开(非模态)"); + UpdateMainStatus("配置编辑器已关闭"); + LogManager.Info("配置编辑器:已打开并关闭(模态)"); } else { diff --git a/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs b/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs index f8b3638..73e6d56 100644 --- a/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs +++ b/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs @@ -2,13 +2,15 @@ using System; using System.IO; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; using System.Diagnostics; using NavisworksTransport.Core.Config; namespace NavisworksTransport.UI.WPF.Views { /// - /// 配置编辑器对话框 - 非模态窗口,避免独占焦点 + /// 配置编辑器对话框 /// public partial class ConfigEditorDialog : Window { @@ -297,7 +299,7 @@ namespace NavisworksTransport.UI.WPF.Views } /// - /// 窗口加载事件 - 处理激活逻辑避免独占焦点 + /// 窗口初始化后,确保获得稳定的键盘焦点 /// protected override void OnSourceInitialized(EventArgs e) { @@ -305,11 +307,10 @@ namespace NavisworksTransport.UI.WPF.Views try { - // 激活并前置窗口,但不独占焦点 - // 先置顶再取消置顶,让窗口前置但不系统级置顶 this.Activate(); this.Topmost = true; this.Topmost = false; + Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input); LogManager.Debug("配置编辑器对话框已激活并前置显示"); } @@ -319,6 +320,15 @@ namespace NavisworksTransport.UI.WPF.Views } } + /// + /// 窗口内容渲染完成后再次请求焦点,避免宿主窗口抢占键盘 + /// + protected override void OnContentRendered(EventArgs e) + { + base.OnContentRendered(e); + Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input); + } + /// /// 激活并显示已存在的窗口 /// @@ -332,10 +342,10 @@ namespace NavisworksTransport.UI.WPF.Views this.WindowState = WindowState.Normal; } - // 激活并前置窗口(先置顶再取消置顶,避免独占焦点) this.Activate(); this.Topmost = true; this.Topmost = false; + FocusEditorTextBox(); LogManager.Info("配置编辑器对话框已激活并前置显示"); } @@ -345,10 +355,29 @@ namespace NavisworksTransport.UI.WPF.Views } } + private void FocusEditorTextBox() + { + try + { + if (ConfigContentTextBox == null) + { + return; + } + + ConfigContentTextBox.Focus(); + Keyboard.Focus(ConfigContentTextBox); + ConfigContentTextBox.CaretIndex = ConfigContentTextBox.Text?.Length ?? 0; + } + catch (Exception ex) + { + LogManager.Debug($"配置编辑器焦点设置失败: {ex.Message}"); + } + } + #region 静态工厂方法 /// - /// 创建并显示配置编辑器对话框(单例模式,非模态) + /// 创建并显示配置编辑器对话框(单例模式,模态) /// /// 对话框实例 public static ConfigEditorDialog ShowEditor() @@ -385,16 +414,19 @@ namespace NavisworksTransport.UI.WPF.Views // 创建新的对话框实例 var dialog = new ConfigEditorDialog(); - // 使用 DialogHelper 安全地设置 owner - NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog); + // 优先设置 WPF Owner,失败时回退到 Navisworks 主窗口句柄 + if (!NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog)) + { + NavisworksTransport.Utils.DialogHelper.SetWin32Owner(dialog); + } // 设置为当前实例 _currentInstance = dialog; - // 使用 Show() 非模态显示,避免独占焦点 - dialog.Show(); + // 使用 ShowDialog() 确保键盘输入不会被宿主窗口吃掉 + dialog.ShowDialog(); - LogManager.Info("新的配置编辑器对话框已显示(非模态)"); + LogManager.Info("新的配置编辑器对话框已显示(模态)"); return dialog; } } diff --git a/src/Utils/ViewpointHelper.cs b/src/Utils/ViewpointHelper.cs index 08aea47..6b4c0b0 100644 --- a/src/Utils/ViewpointHelper.cs +++ b/src/Utils/ViewpointHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Autodesk.Navisworks.Api; +using NavisworksTransport.Core.Config; using NavisworksTransport.Utils.CoordinateSystem; namespace NavisworksTransport.Utils @@ -22,18 +23,25 @@ namespace NavisworksTransport.Utils CollisionCloseUp } + internal enum PathCameraHorizontalMode + { + Top, + Side, + Rear + } + internal struct PathViewpointProfile { - public PathViewpointProfile(double distanceScale, double minDistanceMeters, double elevationDegrees) + public PathViewpointProfile(double cameraDistanceMeters, double elevationDegrees, PathCameraHorizontalMode horizontalMode) { - DistanceScale = distanceScale; - MinDistanceMeters = minDistanceMeters; + CameraDistanceMeters = cameraDistanceMeters; ElevationDegrees = elevationDegrees; + HorizontalMode = horizontalMode; } - public double DistanceScale { get; } - public double MinDistanceMeters { get; } + public double CameraDistanceMeters { get; } public double ElevationDegrees { get; } + public PathCameraHorizontalMode HorizontalMode { get; } } internal struct FocusViewpointProfile @@ -146,8 +154,7 @@ namespace NavisworksTransport.Utils Point3D startPoint = path.Points[0].Position; Point3D endPoint = path.Points[path.Points.Count - 1].Position; - BoundingBox3D focusBoundingBox = CreateBoundingBoxFromPoints(startPoint, endPoint); - BoundingBox3D viewBoundingBox = CalculatePathBoundingBox(path); + BoundingBox3D focusBoundingBox = ResolveDirectionalFocusBoundingBox(path); Point3D focusCenter = GetBoundingBoxCenter(focusBoundingBox); var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); @@ -158,31 +165,40 @@ namespace NavisworksTransport.Utils (float)(endPoint.Z - startPoint.Z)); Vector3 fallbackForward = ToVector3(ProjectVectorOntoPlane(doc.FrontRightTopViewVector, adapter.HostUpVector)); Vector3 horizontalForward = ResolveHorizontalPathForward(rawPathDirection, hostUp, fallbackForward); - Vector3 cameraOffsetDirection = ResolveCameraOffsetDirection(path.PathType, hostUp, horizontalForward); - - double maxDimensionModel = GetMaxDimension(viewBoundingBox); - double minDistanceModel = UnitsConverter.ConvertFromMeters(profile.MinDistanceMeters); - double cameraDistance = Math.Max(maxDimensionModel * profile.DistanceScale, minDistanceModel); + Vector3 cameraOffsetDirection = ResolveCameraOffsetDirection(profile, hostUp, horizontalForward); + double cameraDistance = UnitsConverter.ConvertFromMeters(profile.CameraDistanceMeters); Point3D cameraPosition = new Point3D( focusCenter.X + cameraOffsetDirection.X * (float)cameraDistance, focusCenter.Y + cameraOffsetDirection.Y * (float)cameraDistance, focusCenter.Z + cameraOffsetDirection.Z * (float)cameraDistance); - double expansionMargin = Math.Max( - maxDimensionModel * (VIEW_BOUNDING_BOX_EXPANSION_FACTOR - 1.0) / 2.0, - UnitsConverter.ConvertFromMeters(2.0)); - - ApplyViewpointWithZoomBox( + ApplyViewpoint( cameraPosition, focusCenter, new Vector3D(hostUp.X, hostUp.Y, hostUp.Z), - viewBoundingBox, - expansionMargin); + useAlignDirection: true); LogManager.Info( $"视角已调整完成: 类型={path.PathType}, 焦点=({focusCenter.X:F2},{focusCenter.Y:F2},{focusCenter.Z:F2}), " + $"相机=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}), " + - $"距离={UnitsConverter.ConvertToMeters(cameraDistance):F2}米, elevation={profile.ElevationDegrees:F1}°"); + $"距离={UnitsConverter.ConvertToMeters(cameraDistance):F2}米, elevation={profile.ElevationDegrees:F1}°, 模式={profile.HorizontalMode}"); + } + + internal static BoundingBox3D ResolveDirectionalFocusBoundingBox(PathRoute path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (path.Points == null || path.Points.Count == 0) + { + throw new ArgumentException("路径为空或没有路径点", nameof(path)); + } + + // 侧视路径应聚焦整条路径包络,而不是只看首尾点。 + // 对吊装路径尤其重要:首尾点常在地面,若只取首尾中心,高处主路径会被挤到画面上半部。 + return CalculatePathBoundingBox(path); } /// @@ -296,15 +312,25 @@ namespace NavisworksTransport.Utils internal static PathViewpointProfile ResolvePathViewpointProfile(ViewpointStrategy strategy) { + PathEditingConfig config = ConfigManager.Instance.Current?.PathEditing ?? new PathEditingConfig(); switch (strategy) { case ViewpointStrategy.PathHoistingSelection: - return new PathViewpointProfile(distanceScale: 1.2, minDistanceMeters: 8.0, elevationDegrees: 22.0); + return new PathViewpointProfile( + cameraDistanceMeters: config.HoistingViewDistanceMeters > 0 ? config.HoistingViewDistanceMeters : 6.0, + elevationDegrees: config.HoistingViewElevationDegrees > 0 ? config.HoistingViewElevationDegrees : 30.0, + horizontalMode: PathCameraHorizontalMode.Side); case ViewpointStrategy.PathRailSelection: - return new PathViewpointProfile(distanceScale: 0.75, minDistanceMeters: 4.5, elevationDegrees: 12.0); + return new PathViewpointProfile( + cameraDistanceMeters: config.RailViewDistanceMeters > 0 ? config.RailViewDistanceMeters : 6.0, + elevationDegrees: config.RailViewElevationDegrees > 0 ? config.RailViewElevationDegrees : 30.0, + horizontalMode: PathCameraHorizontalMode.Side); case ViewpointStrategy.PathGroundSelection: default: - return new PathViewpointProfile(distanceScale: 1.0, minDistanceMeters: 12.0, elevationDegrees: 90.0); + return new PathViewpointProfile( + cameraDistanceMeters: 12.0, + elevationDegrees: 90.0, + horizontalMode: PathCameraHorizontalMode.Top); } } @@ -340,27 +366,25 @@ namespace NavisworksTransport.Utils return Vector3.Normalize(ProjectOntoPlane(canonicalFallback, hostUp)); } - internal static Vector3 ResolveCameraOffsetDirection(PathType pathType, Vector3 hostUp, Vector3 horizontalForward) + internal static Vector3 ResolveCameraOffsetDirection(PathViewpointProfile profile, Vector3 hostUp, Vector3 horizontalForward) { - PathViewpointProfile profile = ResolvePathViewpointProfile(pathType); float elevationRadians = (float)(profile.ElevationDegrees * Math.PI / 180.0); float horizontalWeight = (float)Math.Cos(elevationRadians); float verticalWeight = (float)Math.Sin(elevationRadians); Vector3 horizontalDirection; - switch (pathType) + switch (profile.HorizontalMode) { - case PathType.Hoisting: + case PathCameraHorizontalMode.Rear: horizontalDirection = -horizontalForward; break; - case PathType.Rail: + case PathCameraHorizontalMode.Side: horizontalDirection = Vector3.Cross(hostUp, horizontalForward); if (horizontalDirection.LengthSquared() <= 1e-8f) { horizontalDirection = -horizontalForward; } break; - case PathType.Ground: default: horizontalDirection = hostUp; break; From 9be5d250e8df0064fdf6ff11c99eea1bd66e6d94 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 2 Apr 2026 22:35:58 +0800 Subject: [PATCH 58/87] Restore tool focus by picking context --- src/Core/PathInputMonitor.cs | 2 +- src/Core/PathPlanningManager.cs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Core/PathInputMonitor.cs b/src/Core/PathInputMonitor.cs index 57a5525..04e119e 100644 --- a/src/Core/PathInputMonitor.cs +++ b/src/Core/PathInputMonitor.cs @@ -49,7 +49,7 @@ namespace NavisworksTransport if (currentTool != Tool.CustomToolPlugin) { LogManager.Info($"[InputMonitor] 用户按空格键,当前工具为{currentTool},强制恢复ToolPlugin焦点"); - bool restored = pathManager.ForceReinitializeToolPlugin(subscribeToEvents: false); + bool restored = pathManager.RestoreToolPluginFocusForCurrentContext(); if (restored) { return true; diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index 94b5455..620187b 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -4562,6 +4562,33 @@ namespace NavisworksTransport } } + /// + /// 根据当前业务上下文恢复 ToolPlugin 焦点。 + /// 普通手动建/编辑路径应恢复 PathPlanningManager 主链; + /// 自动路径、装配拾取、多层吊装等外部自管拾取模式则保持外部事件处理。 + /// + public bool RestoreToolPluginFocusForCurrentContext() + { + try + { + bool shouldSubscribeToManagerEvents = !IsInAutoPathMode; + string contextLabel = shouldSubscribeToManagerEvents + ? "普通手动取点" + : "外部自管拾取"; + + LogManager.Info( + $"[工具插件恢复] 按当前上下文恢复焦点: 模式={contextLabel}, " + + $"PathEditState={PathEditState}, 订阅PathPlanningManager事件={shouldSubscribeToManagerEvents}"); + + return ForceReinitializeToolPlugin(shouldSubscribeToManagerEvents); + } + catch (Exception ex) + { + LogManager.Error($"[工具插件恢复] 按当前上下文恢复焦点失败: {ex.Message}", ex); + return false; + } + } + #endregion #region 路径计算和保存 From 7058e5fd23655b4a05b7129a3d2e3dde0c7baf90 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 2 Apr 2026 22:37:16 +0800 Subject: [PATCH 59/87] Add hoisting layer height editing --- src/UI/WPF/Models/HoistingLevelItem.cs | 44 ++++ src/UI/WPF/ViewModels/PathEditingViewModel.cs | 232 +++++++++++++++++- src/UI/WPF/Views/PathEditingView.xaml | 53 ++++ 3 files changed, 324 insertions(+), 5 deletions(-) diff --git a/src/UI/WPF/Models/HoistingLevelItem.cs b/src/UI/WPF/Models/HoistingLevelItem.cs index c92e7ce..56d356b 100644 --- a/src/UI/WPF/Models/HoistingLevelItem.cs +++ b/src/UI/WPF/Models/HoistingLevelItem.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Runtime.CompilerServices; using Autodesk.Navisworks.Api; +using System.Collections.Generic; namespace NavisworksTransport.UI.WPF.Models { @@ -53,4 +54,47 @@ namespace NavisworksTransport.UI.WPF.Models PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } + + /// + /// 吊装路径层高编辑项 + /// + public class HoistingLayerEditItem : INotifyPropertyChanged + { + private int _displayIndex; + private double _heightInMeters; + private string _pointSummary; + + public int DisplayIndex + { + get => _displayIndex; + set { _displayIndex = value; OnPropertyChanged(nameof(DisplayIndex)); } + } + + /// + /// 相对起吊点的绝对高度(米) + /// + public double HeightInMeters + { + get => _heightInMeters; + set { _heightInMeters = value; OnPropertyChanged(nameof(HeightInMeters)); } + } + + public string PointSummary + { + get => _pointSummary; + set { _pointSummary = value; OnPropertyChanged(nameof(PointSummary)); } + } + + /// + /// 当前层关联的路径点 Id 列表 + /// + public List PointIds { get; } = new List(); + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } } diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 67fb46d..c533ce7 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; @@ -332,6 +332,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 多层吊装模式 private bool _isMultiLevelHoistingMode = false; private ObservableCollection _multiLevelItems = new ObservableCollection(); + private ObservableCollection _hoistingLayerEditItems = new ObservableCollection(); private Point3D _multiLevelStartPoint; private Point3D _multiLevelEndPoint; private bool _hasMultiLevelStartPoint = false; @@ -339,6 +340,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private string _multiLevelStartPointText = "未选择"; private string _multiLevelEndPointText = "未选择"; private string _multiLevelStatsText = "层级数: 0 | 预估路径点数: 0"; + private string _hoistingLayerEditHintText = "选择一条吊装路径后,可在这里按层编辑相对起点的绝对高度。"; private bool _isSelectingMultiLevelStartPoint = false; private bool _isSelectingMultiLevelEndPoint = false; private HoistingLevelItem _currentSelectingLevelTarget; @@ -431,9 +433,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 🔥 路径类型改变时,检查是否可以使用路径线 OnPropertyChanged(nameof(CanUsePathLines)); OnPropertyChanged(nameof(IsRailRouteSelected)); + OnPropertyChanged(nameof(IsHoistingRouteSelected)); OnPropertyChanged(nameof(SelectedRailMountMode)); OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); OnPropertyChanged(nameof(CanRepositionRailStartPoint)); + OnPropertyChanged(nameof(CanApplyHoistingLayerHeights)); NotifyRailAssemblyCommandStateChanged(); if (!CanUsePathLines && ShowPathLines) { @@ -442,6 +446,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Info("[路径可视化] 切换到吊装/空轨路径,自动关闭路径线"); } + RefreshHoistingLayerEditItemsFromSelectedRoute(); + // 实现路径选择时的可视化切换 UpdatePathVisualization(); } @@ -493,6 +499,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels } public bool IsRailRouteSelected => SelectedPathRoute?.PathType == PathType.Rail; + public bool IsHoistingRouteSelected => SelectedPathRoute?.PathType == PathType.Hoisting; public ObservableCollection> RailMountModeOptions { @@ -906,6 +913,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels set => SetProperty(ref _multiLevelItems, value); } + public ObservableCollection HoistingLayerEditItems + { + get => _hoistingLayerEditItems; + set => SetProperty(ref _hoistingLayerEditItems, value); + } + + public string HoistingLayerEditHintText + { + get => _hoistingLayerEditHintText; + set => SetProperty(ref _hoistingLayerEditHintText, value); + } + public string MultiLevelStartPointText { get => _multiLevelStartPointText; @@ -925,6 +944,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels } public bool CanCreateMultiLevelPath => _hasMultiLevelStartPoint && _hasMultiLevelEndPoint && _multiLevelItems.Count > 0; + public bool CanApplyHoistingLayerHeights => IsHoistingRouteSelected && + SelectedPathRoute != null && + HoistingLayerEditItems.Count > 0; #endregion @@ -1326,6 +1348,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand PickMultiLevelTargetCommand { get; private set; } public ICommand CreateMultiLevelPathCommand { get; private set; } public ICommand CancelMultiLevelHoistingCommand { get; private set; } + public ICommand ApplyHoistingLayerHeightsCommand { get; private set; } #endregion @@ -1977,6 +2000,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels AnalyzeAssemblyTerminalFaceForEditCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(RailAssemblyWorkflowMode.EditSelectedRail), () => CanAnalyzeAssemblyTerminalFaceForEdit); DecreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(-RailNormalOffsetNudgeStepInMeters)); IncreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(RailNormalOffsetNudgeStepInMeters)); + ApplyHoistingLayerHeightsCommand = new RelayCommand(async () => await ExecuteApplyHoistingLayerHeightsAsync(), () => CanApplyHoistingLayerHeights); } #endregion @@ -2010,22 +2034,58 @@ namespace NavisworksTransport.UI.WPF.ViewModels HasAssemblyTerminalObject = true; AssemblyStartPointText = "未选择"; AssemblyTerminalObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(selectedItem); + bool isEditMode = IsRailAssemblyEditMode(mode); + ResetAssemblyEndFaceAnalysisState(); - ResetAssemblyInstallationReferenceState(); + if (!isEditMode) + { + ResetAssemblyInstallationReferenceState(); + } RefreshAssemblyTerminalObjectInfo(); ClearAssemblyAnchorMarker(); ClearAssemblyEndFaceAnalysisVisuals(); - ClearAssemblyInstallationReferenceVisuals(); + if (!isEditMode) + { + ClearAssemblyInstallationReferenceVisuals(); + } NotifyRailAssemblyCommandStateChanged(); - // 新建路径时隐藏其他路径的辅助线 - ClearAssemblyReferenceLineVisuals(); + if (isEditMode) + { + // 编辑态重选箱体后,只恢复可由当前 Rail 路径和终点箱体稳定重建的可视化。 + // 安装参考面依赖安装拾取点、安装面 span 等会话态数据,这些数据当前未持久化到 PathRoute, + // 因此这里不尝试恢复安装参考面,避免后续误以为“少了一次刷新调用”是 bug。 + RefreshSelectedPathVisualizationForAssemblyEdit(); + RefreshAssemblyReferenceRodIfNeeded(); + } + else + { + // 新建路径时隐藏其他路径的辅助线 + ClearAssemblyReferenceLineVisuals(); + } UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}"); LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}"); }, "捕获终点箱体"); } + private void RefreshSelectedPathVisualizationForAssemblyEdit() + { + if (_selectedPathRoute == null || _selectedPathRoute.Points.Count == 0 || _pathPlanningManager == null) + { + return; + } + + var coreRoute = _pathPlanningManager.Routes?.FirstOrDefault(route => route.Id == _selectedPathRoute.Id) + ?? _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == _selectedPathRoute.Name); + if (coreRoute == null) + { + return; + } + + _pathPlanningManager.DrawRouteVisualization(coreRoute, isAutoPath: false); + } + private void ClearNonGridPathVisualizations(string context) { if (PathPointRenderPlugin.Instance == null) @@ -4572,6 +4632,163 @@ namespace NavisworksTransport.UI.WPF.ViewModels return true; } + private void RefreshHoistingLayerEditItemsFromSelectedRoute() + { + HoistingLayerEditItems.Clear(); + + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Hoisting || coreRoute.Points == null || coreRoute.Points.Count < 2) + { + HoistingLayerEditHintText = "选择一条吊装路径后,可在这里按层编辑相对起点的绝对高度。"; + OnPropertyChanged(nameof(CanApplyHoistingLayerHeights)); + return; + } + + HostCoordinateAdapter hostAdapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + PathPoint hostStartPoint = coreRoute.Points.FirstOrDefault(point => point.Type == PathPointType.StartPoint) ?? coreRoute.Points.First(); + double hostStartElevation = HoistingCoordinateHelper.GetElevation(hostStartPoint.Position, hostAdapter); + const double relativeHeightToleranceInMeters = 0.001; + + var layerItems = new List(); + foreach (var point in coreRoute.Points) + { + if (point == null || point.Type == PathPointType.StartPoint || point.Type == PathPointType.EndPoint) + { + continue; + } + + double hostPointElevation = HoistingCoordinateHelper.GetElevation(point.Position, hostAdapter); + double relativeHeightInMeters = UnitsConverter.ConvertToMeters(hostPointElevation - hostStartElevation); + if (relativeHeightInMeters <= relativeHeightToleranceInMeters) + { + continue; + } + + HoistingLayerEditItem existingLayer = layerItems.FirstOrDefault(item => + Math.Abs(item.HeightInMeters - relativeHeightInMeters) <= relativeHeightToleranceInMeters); + if (existingLayer == null) + { + existingLayer = new HoistingLayerEditItem + { + HeightInMeters = Math.Round(relativeHeightInMeters, 3) + }; + layerItems.Add(existingLayer); + } + + if (!string.IsNullOrWhiteSpace(point.Id) && !existingLayer.PointIds.Contains(point.Id)) + { + existingLayer.PointIds.Add(point.Id); + } + } + + List orderedLayers = layerItems + .OrderByDescending(item => item.HeightInMeters) + .ToList(); + + for (int i = 0; i < orderedLayers.Count; i++) + { + orderedLayers[i].DisplayIndex = i + 1; + orderedLayers[i].PointSummary = $"关联点 {orderedLayers[i].PointIds.Count} 个"; + HoistingLayerEditItems.Add(orderedLayers[i]); + } + + HoistingLayerEditHintText = HoistingLayerEditItems.Count > 0 + ? "层高按高到低显示;每层高度相对起吊点,且必须严格低于上一层、高于下一层。" + : "当前吊装路径未解析出可编辑的空中层。"; + OnPropertyChanged(nameof(CanApplyHoistingLayerHeights)); + } + + private async Task ExecuteApplyHoistingLayerHeightsAsync() + { + await SafeExecuteAsync(() => + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Hoisting) + { + return; + } + + if (HoistingLayerEditItems.Count == 0) + { + UpdateMainStatus("当前吊装路径没有可编辑的空中层。"); + return; + } + + for (int i = 0; i < HoistingLayerEditItems.Count; i++) + { + HoistingLayerEditItem currentLayer = HoistingLayerEditItems[i]; + if (currentLayer.HeightInMeters <= 0) + { + HandleInvalidHoistingLayerHeightInput($"第{currentLayer.DisplayIndex}层高度必须大于0。"); + return; + } + + if (i > 0 && currentLayer.HeightInMeters >= HoistingLayerEditItems[i - 1].HeightInMeters) + { + HandleInvalidHoistingLayerHeightInput($"第{currentLayer.DisplayIndex}层高度必须严格低于上一层。"); + return; + } + + if (i < HoistingLayerEditItems.Count - 1 && currentLayer.HeightInMeters <= HoistingLayerEditItems[i + 1].HeightInMeters) + { + HandleInvalidHoistingLayerHeightInput($"第{currentLayer.DisplayIndex}层高度必须严格高于下一层。"); + return; + } + } + + HostCoordinateAdapter hostAdapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + PathPoint hostStartPoint = coreRoute.Points.FirstOrDefault(point => point.Type == PathPointType.StartPoint) ?? coreRoute.Points.First(); + double hostStartElevation = HoistingCoordinateHelper.GetElevation(hostStartPoint.Position, hostAdapter); + + foreach (HoistingLayerEditItem layerItem in HoistingLayerEditItems) + { + double hostTargetElevation = hostStartElevation + UnitsConverter.ConvertFromMeters(layerItem.HeightInMeters); + foreach (string pointId in layerItem.PointIds) + { + PathPoint targetPoint = coreRoute.Points.FirstOrDefault(point => string.Equals(point.Id, pointId, StringComparison.Ordinal)); + if (targetPoint != null) + { + targetPoint.Position = HoistingCoordinateHelper.SetElevation(targetPoint.Position, hostTargetElevation, hostAdapter); + } + } + } + + PathPoint firstAerialPoint = coreRoute.Points.FirstOrDefault(point => + point != null && + point.Type != PathPointType.StartPoint && + point.Type != PathPointType.EndPoint && + HoistingCoordinateHelper.GetElevation(point.Position, hostAdapter) > hostStartElevation + UnitsConverter.ConvertFromMeters(0.001)); + coreRoute.LiftHeight = firstAerialPoint != null + ? HoistingCoordinateHelper.GetElevation(firstAerialPoint.Position, hostAdapter) - hostStartElevation + : 0.0; + coreRoute.LastModified = DateTime.Now; + coreRoute.RecalculateRoute("更新吊装层高"); + + SyncObjectParametersToRenderPlugin(); + _pathPlanningManager.DrawRouteVisualization(coreRoute, isAutoPath: false); + _pathPlanningManager.SavePathToDatabase(coreRoute); + _pathPlanningManager.RaiseVisualizationStateChanged(); + + if (SelectedPathRoute != null && SelectedPathRoute.Id == coreRoute.Id) + { + SyncPathViewModelFromCoreRoute(SelectedPathRoute, coreRoute, preserveSelection: true); + } + + RefreshHoistingLayerEditItemsFromSelectedRoute(); + UpdateMainStatus($"已应用 {HoistingLayerEditItems.Count} 个吊装层高。"); + LogManager.Info($"[吊装层高] {coreRoute.Name}: 已应用 {HoistingLayerEditItems.Count} 个层高编辑项"); + }, "应用吊装层高"); + } + + private void HandleInvalidHoistingLayerHeightInput(string message) + { + const string validationMessage = "每层高度必须低于上一层、高于下一层,也不能相等。"; + LogManager.Warning($"[吊装层高] 用户输入无效: {message}"); + RefreshHoistingLayerEditItemsFromSelectedRoute(); + UpdateMainStatus(validationMessage); + MessageBox.Show(validationMessage, "层高输入无效", MessageBoxButton.OK, MessageBoxImage.Warning); + } + /// /// 更新吊装路径的关联点(UI层) /// @@ -6579,6 +6796,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Info($"已更新路径点列表,当前点数: {pathViewModel.Points.Count}"); + if (SelectedPathRoute == pathViewModel) + { + RefreshHoistingLayerEditItemsFromSelectedRoute(); + } + // 更新真实路径可视化 UpdatePathVisualization(); } diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 73e7b6d..dc7680a 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -371,6 +371,59 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +