Refine rail assembly workflow and viewpoint strategies

This commit is contained in:
tian 2026-04-01 10:47:34 +08:00
parent c3b103c159
commit 2e0b26e032
13 changed files with 1243 additions and 331 deletions

View File

@ -177,6 +177,56 @@
目标是做到:只看名字,就能知道这个方向、轴、向量到底属于宿主、内部、资产,还是对象自身局部轴。 目标是做到:只看名字,就能知道这个方向、轴、向量到底属于宿主、内部、资产,还是对象自身局部轴。
### 4.1.3 对象局部轴业务映射
以后关于 `Local...`、前进轴、up 轴、side 轴,统一优先使用这个说法:
- **对象局部轴业务映射**
- 它不是第四套全局坐标系
- 也不是几何天然自带的“真理”
- 它表示:在当前业务链路里,我们准备把对象自身哪根局部轴解释成:
- `forward`
- `up`
- `side`
这个概念的边界必须非常明确:
- 它是**业务解释层**
- 不是宿主坐标系
- 不是内部 `Canonical Space`
- 不是插件资源资产坐标系
- 它不能脱离上下文单独存在
- `up` 往往要结合宿主 `up` 语义来解释
- `forward` 往往取决于:
- fragment 代表姿态解释
- 资源默认轴约定
- 用户选择
- 当前路径类型
为什么必须保留这层概念:
- 路径姿态求解必须回答:
- 对象哪根局部轴去对齐路径 `forward`
- 对象哪根局部轴去对齐目标 `up`
- 如果没有这层映射,下面这些都无法稳定定义:
- `Ground / Hoisting / Rail` 起点姿态
- 逐帧姿态
- `Rail` 角度修正
- 通行空间尺寸投影
- 终点原始姿态保持
- `Rail` 平移模式的终点原位搬运
当前项目里的硬约束:
- 不要把“对象局部轴业务映射”写成“对象自身天然就有 forward/up 定义”
- 对真实物体:
- 不能简单把 `ModelItem.Transform` 当成可靠的局部轴真值
- 应优先通过 fragment 代表姿态 + `Fragment默认Up` 去解释
- 对虚拟物体:
- 对象局部轴业务映射通常来自明确的资产轴约定
- 如果只是讨论对象自身局部轴语义,允许继续使用 `Local...` 命名
- 但文档、日志、设计讨论里优先说“对象局部轴业务映射”
### 4.1.1 Quaternion / Rotation3D 固定定义 ### 4.1.1 Quaternion / Rotation3D 固定定义
这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口: 这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口:

View File

@ -76,6 +76,8 @@
<Compile Include="UnitTests\CoordinateSystem\PathTargetFrameResolverTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\PathTargetFrameResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyInstallationReferenceBuilderTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\AssemblyInstallationReferenceBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RailAssemblyWorkflowContextTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ViewpointHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\PathPointVisualizationTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\PathPointVisualizationTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\FragmentDefaultUpContextTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\FragmentDefaultUpContextTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\VirtualGroundPoseCharacterizationTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\VirtualGroundPoseCharacterizationTests.cs" />

View File

@ -284,6 +284,7 @@
<Compile Include="src\UI\WPF\ViewModels\ModelSettingsViewModel.cs" /> <Compile Include="src\UI\WPF\ViewModels\ModelSettingsViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\AnimationControlViewModel.cs" /> <Compile Include="src\UI\WPF\ViewModels\AnimationControlViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\PathEditingViewModel.cs" /> <Compile Include="src\UI\WPF\ViewModels\PathEditingViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\RailAssemblyWorkflowContext.cs" />
<Compile Include="src\UI\WPF\ViewModels\SystemManagementViewModel.cs" /> <Compile Include="src\UI\WPF\ViewModels\SystemManagementViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\CollisionReportViewModel.cs" /> <Compile Include="src\UI\WPF\ViewModels\CollisionReportViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\PathAnalysisViewModel.cs" /> <Compile Include="src\UI\WPF\ViewModels\PathAnalysisViewModel.cs" />

View File

@ -0,0 +1,118 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.UI.WPF.ViewModels;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RailAssemblyWorkflowContextTests
{
[TestMethod]
public void ResetEndFaceAnalysis_ClearsAnalysisStateAndSeedPoints()
{
var context = CreateContext();
context.HasEndFaceAnalysis = true;
context.EndFaceCenterPoint = new Point3D(1.0, 2.0, 3.0);
context.EndFaceNormal = new Vector3(1f, 0f, 0f);
context.EndFaceSeedPoints.Add(new Point3D(4.0, 5.0, 6.0));
context.ResetEndFaceAnalysis();
Assert.IsFalse(context.HasEndFaceAnalysis);
Assert.IsNull(context.EndFaceCenterPoint);
Assert.AreEqual(default(Vector3), context.EndFaceNormal);
Assert.AreEqual(0, context.EndFaceSeedPoints.Count);
}
[TestMethod]
public void ResetInstallationReference_RestoresDefaultOffsetAndClearsReferenceState()
{
var context = CreateContext();
context.HasInstallationReference = true;
context.InstallationPickPoint = new Point3D(1.0, 2.0, 3.0);
context.InstallationBaseAnchorPoint = new Point3D(4.0, 5.0, 6.0);
context.InstallationAnchorPoint = new Point3D(7.0, 8.0, 9.0);
context.InstallationPlaneNormal = new Vector3(0f, 1f, 0f);
context.InstallationPlaneSpanDirection = new Vector3(1f, 0f, 0f);
context.InstallationOffsetDistanceInMeters = 2.5;
context.InstallationReferenceRouteId = "route-1";
context.InstallationSeedPoints.Add(new Point3D(9.0, 8.0, 7.0));
context.AnchorVerticalOffsetInMeters = 3.5;
context.ResetInstallationReference();
Assert.IsFalse(context.HasInstallationReference);
Assert.IsNull(context.InstallationPickPoint);
Assert.IsNull(context.InstallationBaseAnchorPoint);
Assert.IsNull(context.InstallationAnchorPoint);
Assert.AreEqual(default(Vector3), context.InstallationPlaneNormal);
Assert.AreEqual(default(Vector3), context.InstallationPlaneSpanDirection);
Assert.AreEqual(0.0, context.InstallationOffsetDistanceInMeters, 1e-9);
Assert.IsNull(context.InstallationReferenceRouteId);
Assert.AreEqual(0, context.InstallationSeedPoints.Count);
Assert.AreEqual(0.25, context.AnchorVerticalOffsetInMeters, 1e-9);
}
[TestMethod]
public void ResetSession_ClearsTransientStateButPreservesConfiguration()
{
var context = CreateContext();
context.WorkflowMode = RailAssemblyWorkflowMode.EditSelectedRail;
context.TerminalObjectName = "箱体A";
context.TerminalObjectInfo = "info";
context.ReferenceRodLengthInMeters = 12.0;
context.ReferenceRodDiameterInMeters = 0.4;
context.SphereCenterX = 10.0;
context.SphereCenterY = 11.0;
context.SphereCenterZ = 12.0;
context.MountMode = RailMountMode.OverRail;
context.HasTerminalObject = true;
context.IsSelectingStartPoint = true;
context.IsSelectingEndFacePoints = true;
context.IsSelectingInstallationPoint = true;
context.StartPoint = new Point3D(1.0, 2.0, 3.0);
context.StartPointText = "(1,2,3)";
context.HasEndFaceAnalysis = true;
context.EndFaceCenterPoint = new Point3D(4.0, 5.0, 6.0);
context.HasInstallationReference = true;
context.InstallationReferenceRouteId = "route-2";
context.AnchorVerticalOffsetInMeters = 1.5;
context.EndFaceSeedPoints.Add(new Point3D(1.0, 0.0, 0.0));
context.InstallationSeedPoints.Add(new Point3D(0.0, 1.0, 0.0));
context.ResetSession();
Assert.AreEqual(RailAssemblyWorkflowMode.None, context.WorkflowMode);
Assert.IsFalse(context.HasTerminalObject);
Assert.IsFalse(context.IsSelectingStartPoint);
Assert.IsFalse(context.IsSelectingEndFacePoints);
Assert.IsFalse(context.IsSelectingInstallationPoint);
Assert.IsNull(context.TerminalObject);
Assert.IsNotNull(context.StartPoint);
Assert.AreEqual("未选择", context.TerminalObjectName);
Assert.AreEqual("请选择终点处已安装箱体", context.TerminalObjectInfo);
Assert.AreEqual("未选择", context.StartPointText);
Assert.IsFalse(context.HasEndFaceAnalysis);
Assert.IsFalse(context.HasInstallationReference);
Assert.AreEqual(0.25, context.AnchorVerticalOffsetInMeters, 1e-9);
Assert.AreEqual(12.0, context.ReferenceRodLengthInMeters, 1e-9);
Assert.AreEqual(0.4, context.ReferenceRodDiameterInMeters, 1e-9);
Assert.AreEqual(10.0, context.SphereCenterX, 1e-9);
Assert.AreEqual(11.0, context.SphereCenterY, 1e-9);
Assert.AreEqual(12.0, context.SphereCenterZ, 1e-9);
Assert.AreEqual(RailMountMode.OverRail, context.MountMode);
}
private static RailAssemblyWorkflowContext CreateContext()
{
return new RailAssemblyWorkflowContext(
defaultReferenceRodLengthInMeters: 20.0,
defaultReferenceRodDiameterInMeters: 0.3,
defaultAnchorVerticalOffsetInMeters: 0.25,
defaultSphereCenterX: 0.0,
defaultSphereCenterY: 0.0,
defaultSphereCenterZ: 0.0);
}
}
}

View File

@ -0,0 +1,80 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ViewpointHelperTests
{
[TestMethod]
public void ResolvePathViewpointProfile_ShouldReturnExpectedDefaults()
{
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(1.2, hoisting.DistanceScale, 1e-9);
Assert.AreEqual(8.0, hoisting.MinDistanceMeters, 1e-9);
Assert.AreEqual(22.0, hoisting.ElevationDegrees, 1e-9);
Assert.AreEqual(0.9, rail.DistanceScale, 1e-9);
Assert.AreEqual(6.0, rail.MinDistanceMeters, 1e-9);
Assert.AreEqual(12.0, rail.ElevationDegrees, 1e-9);
}
[TestMethod]
public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromBehindAndAbove()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Hoisting, hostUp, forward);
Assert.IsTrue(offset.X < -0.8f);
Assert.IsTrue(offset.Z > 0.3f);
}
[TestMethod]
public void ResolveCameraOffsetDirection_Rail_ShouldLookFromSideAndSlightlyAbove()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Rail, hostUp, forward);
Assert.AreEqual(0.0f, offset.X, 1e-5f);
Assert.IsTrue(offset.Y > 0.9f);
Assert.IsTrue(offset.Z > 0.15f);
}
[TestMethod]
public void ResolveHorizontalPathForward_ShouldFallbackWhenPathIsVertical()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 rawVertical = Vector3.UnitZ;
Vector3 fallback = Vector3.UnitY;
Vector3 resolved = ViewpointHelper.ResolveHorizontalPathForward(rawVertical, hostUp, fallback);
Assert.AreEqual(0.0f, resolved.Z, 1e-5f);
Assert.AreEqual(1.0f, resolved.Y, 1e-5f);
}
[TestMethod]
public void ResolveFocusViewpointProfile_ShouldCentralizeNamedFocusStrategies()
{
var modelFocus = ViewpointHelper.ResolveFocusViewpointProfile(ViewpointHelper.ViewpointStrategy.ModelFocus);
var collision = ViewpointHelper.ResolveFocusViewpointProfile(ViewpointHelper.ViewpointStrategy.CollisionCloseUp);
Assert.AreEqual(60.0, modelFocus.ViewAngleDegrees, 1e-9);
Assert.AreEqual(0.25, modelFocus.TargetViewRatio, 1e-9);
Assert.AreEqual(60.0, collision.ViewAngleDegrees, 1e-9);
Assert.AreEqual(0.25, collision.TargetViewRatio, 1e-9);
}
}
}

View File

@ -1,12 +1,18 @@
# 当前工程状态 # 当前工程状态
更新时间2026-03-25 更新时间2026-04-01
## 1. 当前稳定状态 ## 1. 当前稳定状态
- `Rail` 路径主链路已稳定: - `Rail` 路径主链路已稳定:
- `ZUp` 模型下,轨上/轨下路径的真实物体与虚拟物体起点、动画、终点贴合正确。 - `ZUp` 模型下,轨上/轨下路径的真实物体与虚拟物体起点、动画、终点贴合正确。
- `YUp` 模型下,终端安装仿真、`Rail` 姿态、真实物体与虚拟物体通行空间、起点与动画主链路已基本跑通。 - `YUp` 模型下,终端安装仿真、`Rail` 姿态、真实物体与虚拟物体通行空间、起点与动画主链路已基本跑通。
- `Rail` “调整角度”窗口中的“平移”模式当前已重新收稳:
- 以终点箱体当前原始位姿为真值;
- 记录终点原位相对路径终点参考点的固定残差;
- 把这份残差整体搬运到路径起点;
- 动画全程保持终点原始姿态;
- 动画结束时应精确回到终点箱体原位。
- 地面路径在 `YUp` 模型下: - 地面路径在 `YUp` 模型下:
- 虚拟物体起点姿态、动画姿态、转弯姿态已恢复正常。 - 虚拟物体起点姿态、动画姿态、转弯姿态已恢复正常。
- 真实物体地面路径当前已恢复到可用状态: - 真实物体地面路径当前已恢复到可用状态:
@ -20,16 +26,24 @@
- 对象级前进轴统一按 `PositiveX` 解释; - 对象级前进轴统一按 `PositiveX` 解释;
- 不再因为路径初始方向更偏 `Z` 就把对象前进轴自动切换成 `PositiveZ` - 不再因为路径初始方向更偏 `Z` 就把对象前进轴自动切换成 `PositiveZ`
- 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。 - 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。
- 吊装路径真实物体的角度调整链路已重新稳定:
- 起点“调整角度”后,预览、生成动画、播放前重置都复用同一份真实起点基姿态;
- 清除并重新选择物体时,旧角度修正必须被清空,不能继承上一次调整状态。
- 碰撞检测/恢复主链路已稳定: - 碰撞检测/恢复主链路已稳定:
- `ClashDetective` 三维恢复不能再先 `ResetPermanentTransform` - `ClashDetective` 三维恢复不能再先 `ResetPermanentTransform`
- 碰撞恢复、自动报告、自动截图已重新对齐到动画主链路。 - 碰撞恢复、自动报告、自动截图已重新对齐到动画主链路。
- 虚拟物体资源问题已确认并修复: - 虚拟物体资源问题已确认并修复:
- 旧 `unit_cube.nwc` 局部几何中心不在原点,会导致虚拟物体中心偏差。 - 旧 `unit_cube.nwc` 局部几何中心不在原点,会导致虚拟物体中心偏差。
- 新 `unit_cube.nwc` 已替换为几何中心在原点的版本。 - 新 `unit_cube.nwc` 已替换为几何中心在原点的版本。
- 旋转适配入口当前稳定分流: - 旋转适配入口当前稳定分流:
- 虚拟物体继续走 `HostCoordinateAdapter``Legacy` 入口; - 虚拟物体继续走 `HostCoordinateAdapter``Legacy` 入口;
- 真实物体走 `Direct` 入口; - 真实物体走 `Direct` 入口;
- 两者不能再强行共用同一条旋转转换链。 - 两者不能再强行共用同一条旋转转换链。
- `Rail` 终端安装仿真创建/编辑区当前已完成第一轮工作流拆分:
- 新建 Rail 与编辑选中 Rail 不再共用同一套按钮命令入口;
- 新建区和编辑区有独立的 `找端面 / 选安装点 / 取起点` 可用条件;
- 两个区域都已有明确的“取消装配”退出入口;
- 上方“路径编辑 -> 结束”在 Rail 装配工作流活跃时,也必须能真正退出 Rail 装配流程。
## 2. 当前坐标系架构 ## 2. 当前坐标系架构
@ -212,6 +226,19 @@
2. 再验证修正后 `forward` 是否仍沿 rail 切向 2. 再验证修正后 `forward` 是否仍沿 rail 切向
3. 最后再看 Navisworks 应用层是否正确按三步增量姿态落位 3. 最后再看 Navisworks 应用层是否正确按三步增量姿态落位
### 4.7.1 Rail 平移模式
- `Rail` 的“平移”不是重新按轨上/轨下求一套新的安装姿态。
- 当前稳定语义:
- 终点箱体当前原始位姿是唯一真值;
- 先计算终点箱体相对“路径终点参考点”的固定残差;
- 再把这份残差搬运到路径起点;
- 动画全程保持这套终点原始姿态和固定残差。
- 当前规则:
- 不能把“平移模式”误实现成重新按 `RailMountMode` 解目标 pose
- 不能锁到“参考姿态”而丢掉终点箱体当前真实几何姿态;
- 终点保位误差如果异常,优先检查“残差是相对终点参考点采样,还是误相对起点参考点采样”。
### 4.8 Rail 真实物体必须复用统一姿态解释层 ### 4.8 Rail 真实物体必须复用统一姿态解释层
- `Rail` 不应再单独退回默认 `PositiveX / PositiveY` 轴约定。 - `Rail` 不应再单独退回默认 `PositiveX / PositiveY` 轴约定。
@ -243,6 +270,40 @@
- `AnimationControlViewModel.UpdatePassageSpaceVisualization()` 中,真实物体的 `Rail``Hoisting` 必须分开消费 - `AnimationControlViewModel.UpdatePassageSpaceVisualization()` 中,真实物体的 `Rail``Hoisting` 必须分开消费
- `Rail` 真实物体不能再复用 `Hoisting` 的法向/分段参数语义 - `Rail` 真实物体不能再复用 `Hoisting` 的法向/分段参数语义
### 4.10 Rail 终端安装仿真工作流
- 当前真实问题已经确认:
- “创建 Rail 路径”区和“Rail 参数”编辑区之前共用同一套装配状态、命令入口和按钮可用条件;
- 导致新建流程会误吃编辑态校验,或在切换路径后残留旧的装配状态。
- 当前第一轮稳定修复:
- 新建 Rail 与编辑 Rail 已拆成独立命令入口;
- “找端面”在新建 Rail 时不再要求先选中一条现有 Rail 路径;
- 新建区的“选安装点”不再受 `IsRailRouteSelected` 编辑态条件限制;
- 安装点拾取完成后,拾取点、安装点、安装参考面、中心线必须保留可视化,不能在收尾时被一并清掉;
- “隐藏辅助线”必须同时清理:
- 参考杆与锚点
- 球心到光轴参考点基准线
- 端面分析可视化
- 安装点、安装中心线、安装参考面
- 新建区和编辑区都必须有明确的“取消装配”;
- “路径编辑 -> 结束”在 Rail 装配流程活跃时,也必须能触发同一条取消/清理链路;
- 切换路径时必须自动清空旧的 Rail 装配上下文,不能把旧状态带到下一条路径。
- 当前仍要记住:
- 这块只是“第一轮工作流拆分”,还不是彻底的数据结构重构;
- 如果后续继续扩 Rail 终端安装仿真,优先方向应是把“创建态上下文”和“编辑态上下文”进一步拆成独立 context而不是继续在 ViewModel 顶层堆共享字段和布尔分支。
### 4.11 吊装路径角度调整
- 当前真实根因已经确认:
- 吊装真实物体在“起点调整角度后生成动画”时,如果基姿态缓存不跟着角度修正一起重建,就会出现:
- 动画里没有沿用调整后的角度;
- 甚至被错误立起或只抬高一点的异常现象。
- 当前稳定规则:
- 吊装路径必须缓存“真实起点基姿态 + 起始 yaw”
- 起点预览、动画生成、播放前重置,都要复用同一份基线;
- 只要角度修正变化,就必须清掉旧缓存并重新 `MoveObjectToPathStart()`
- 清除物体或重新选择物体时,角度修正状态必须归零,不允许继承上一轮对象的角度状态。
## 5. 当前保留的日志策略 ## 5. 当前保留的日志策略
- 保留: - 保留:
@ -257,8 +318,16 @@
- 已降级或删除: - 已降级或删除:
- 大量重复逐帧宿主姿态轴日志 - 大量重复逐帧宿主姿态轴日志
- 虚拟物体 `Transform` 即时读回日志(容易误导) - 虚拟物体 `Transform` 即时读回日志(容易误导)
- `Hoisting` 真实物体起点姿态与平面前进轴的刷屏 `Info` 日志,已降为 `Debug`
## 6. 当前 Navisworks 变换 API 结论 ## 6. 最近关键提交
- `c3b103c` `Add explicit exit for rail assembly workflow`
- `c974469` `Separate rail creation and edit assembly flows`
- `d4c49fc` `Stabilize hoisting pose adjustment flow`
- `cb56737` `Preserve terminal pose when translating rail objects to path start`
## 7. 当前 Navisworks 变换 API 结论
- `ModelItem.Transform` - `ModelItem.Transform`
- 只表示原始设计文件变换; - 只表示原始设计文件变换;
@ -279,13 +348,13 @@
- 清掉的是永久增量层; - 清掉的是永久增量层;
- 不会修改原始设计文件变换。 - 不会修改原始设计文件变换。
## 6. 当前还值得继续观察的点 ## 8. 当前还值得继续观察的点
- 空轨路径里仍有一个旧 warning - 空轨路径里仍有一个旧 warning
- `[空轨] 双轨几何中心线提取失败,退回 OBB 主轴中线` - `[空轨] 双轨几何中心线提取失败,退回 OBB 主轴中线`
- 这与当前 `YUp` 地面路径问题无关,但后续可以单独处理。 - 这与当前 `YUp` 地面路径问题无关,但后续可以单独处理。
## 7. 下一步建议 ## 9. 下一步建议
- 继续按“先框架、先测试、后接业务”的方式推进。 - 继续按“先框架、先测试、后接业务”的方式推进。
- 新问题优先: - 新问题优先:
@ -293,8 +362,12 @@
2. 若日志不足,先补日志 2. 若日志不足,先补日志
3. 再补针对性的回归测试 3. 再补针对性的回归测试
4. 最后再改业务代码 4. 最后再改业务代码
- `Rail` 终端安装仿真的下一轮优化优先级:
1. 把创建态/编辑态从 ViewModel 顶层共享字段继续拆成独立 context
2. 为“取消装配 / 切换路径 / 中途退出后重新开始”补单元测试或更轻量的状态回归验证
3. 再考虑是否需要把创建区与编辑区在 UI 上做更明确的视觉分隔
## 8. 当前阶段的工作边界 ## 10. 当前阶段的工作边界
- 不要回到“直接补旧 `yaw` 链路”的方式。 - 不要回到“直接补旧 `yaw` 链路”的方式。
- 不要再增加隐藏错误的 fallback。 - 不要再增加隐藏错误的 fallback。

View File

@ -2458,7 +2458,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ModelHighlightHelper.HighlightItems(ManualTargetsHighlightCategory, items); ModelHighlightHelper.HighlightItems(ManualTargetsHighlightCategory, items);
// 聚焦到对象斜上方45度视角 // 聚焦到对象斜上方45度视角
ViewpointHelper.FocusOnModelItem(target.ModelItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25); ViewpointHelper.FocusOnModelItem(target.ModelItem, ViewpointHelper.ViewpointStrategy.ModelFocus);
UpdateMainStatus($"已聚焦到: {target.DisplayName}"); UpdateMainStatus($"已聚焦到: {target.DisplayName}");
LogManager.Info($"聚焦到手工指定对象: {target.DisplayName}"); LogManager.Info($"聚焦到手工指定对象: {target.DisplayName}");
@ -2484,7 +2484,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, items); ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, items);
// 聚焦到对象斜上方45度视角 // 聚焦到对象斜上方45度视角
ViewpointHelper.FocusOnModelItem(excludedObject.ModelItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25); ViewpointHelper.FocusOnModelItem(excludedObject.ModelItem, ViewpointHelper.ViewpointStrategy.ModelFocus);
UpdateMainStatus($"已聚焦到排除对象: {excludedObject.DisplayName}"); UpdateMainStatus($"已聚焦到排除对象: {excludedObject.DisplayName}");
LogManager.Info($"聚焦到排除对象: {excludedObject.DisplayName}"); LogManager.Info($"聚焦到排除对象: {excludedObject.DisplayName}");

View File

@ -1597,7 +1597,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
try try
{ {
// 聚焦到模型斜上方60度视角 // 聚焦到模型斜上方60度视角
ViewpointHelper.FocusOnModelItem(logisticsModel.NavisworksItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25); ViewpointHelper.FocusOnModelItem(logisticsModel.NavisworksItem, ViewpointHelper.ViewpointStrategy.ModelFocus);
UpdateMainStatus($"已聚焦到物流模型: {logisticsModel.Name}"); UpdateMainStatus($"已聚焦到物流模型: {logisticsModel.Name}");
LogManager.Info($"聚焦到物流模型: {logisticsModel.Name}, 类别: {logisticsModel.Category}"); LogManager.Info($"聚焦到物流模型: {logisticsModel.Name}, 类别: {logisticsModel.Category}");

File diff suppressed because it is too large Load Diff

View File

@ -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<Point3D>();
EndFaceSeedPoints = new List<Point3D>();
}
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<Point3D> InstallationSeedPoints { get; }
public List<Point3D> 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();
}
}
}

View File

@ -488,10 +488,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding SelectAssemblyInstallationPointForCreateCommand}" Command="{Binding SelectAssemblyInstallationPointForCreateCommand}"
Style="{StaticResource SecondaryButtonStyle}" Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyInstallationPointForCreate}"/> IsEnabled="{Binding CanSelectAssemblyInstallationPointForCreate}"/>
<Button Content="生成辅助线"
Command="{Binding GenerateAssemblyReferenceRodCommand}"
Style="{StaticResource ActionButtonStyle}"
IsEnabled="{Binding CanGenerateAssemblyReferenceRod}"/>
<Button Content="取起点" <Button Content="取起点"
Command="{Binding SelectAssemblyStartPointForCreateCommand}" Command="{Binding SelectAssemblyStartPointForCreateCommand}"
Style="{StaticResource ActionButtonStyle}" Style="{StaticResource ActionButtonStyle}"

View File

@ -50,7 +50,7 @@ namespace NavisworksTransport.Utils
ModelHighlightHelper.HighlightItems(ModelHighlightHelper.ClashDetectiveResultsCategory, new[] { hitObject }); ModelHighlightHelper.HighlightItems(ModelHighlightHelper.ClashDetectiveResultsCategory, new[] { hitObject });
// 聚焦到被撞对象 // 聚焦到被撞对象
ViewpointHelper.FocusOnModelItem(hitObject, viewAngleDegrees: 60.0, targetViewRatio: 0.25); ViewpointHelper.FocusOnModelItem(hitObject, ViewpointHelper.ViewpointStrategy.CollisionCloseUp);
LogManager.Info(string.Format("聚焦到被撞对象: {0}", hitObject.DisplayName)); LogManager.Info(string.Format("聚焦到被撞对象: {0}", hitObject.DisplayName));
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics;
using Autodesk.Navisworks.Api; using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils.CoordinateSystem; using NavisworksTransport.Utils.CoordinateSystem;
@ -12,6 +13,41 @@ namespace NavisworksTransport.Utils
/// </summary> /// </summary>
public static class ViewpointHelper public static class ViewpointHelper
{ {
public enum ViewpointStrategy
{
PathGroundSelection,
PathHoistingSelection,
PathRailSelection,
ModelFocus,
CollisionCloseUp
}
internal struct PathViewpointProfile
{
public PathViewpointProfile(double distanceScale, double minDistanceMeters, double elevationDegrees)
{
DistanceScale = distanceScale;
MinDistanceMeters = minDistanceMeters;
ElevationDegrees = elevationDegrees;
}
public double DistanceScale { get; }
public double MinDistanceMeters { get; }
public double ElevationDegrees { get; }
}
internal struct FocusViewpointProfile
{
public FocusViewpointProfile(double viewAngleDegrees, double targetViewRatio)
{
ViewAngleDegrees = viewAngleDegrees;
TargetViewRatio = targetViewRatio;
}
public double ViewAngleDegrees { get; }
public double TargetViewRatio { get; }
}
/// <summary> /// <summary>
/// 包围盒扩展系数(用于 ZoomBox 调整视野范围) /// 包围盒扩展系数(用于 ZoomBox 调整视野范围)
/// </summary> /// </summary>
@ -33,6 +69,34 @@ namespace NavisworksTransport.Utils
AdjustViewpointToPathCenter(path); AdjustViewpointToPathCenter(path);
} }
/// <summary>
/// 按路径类型调整视角。
/// Ground 使用俯视;
/// Hoisting 使用后上方视角;
/// Rail 使用侧面轻俯视角。
/// </summary>
public static void AdjustViewpointForPath(PathRoute path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
switch (path.PathType)
{
case PathType.Hoisting:
AdjustDirectionalViewpointToPath(path, ResolvePathViewpointProfile(ViewpointStrategy.PathHoistingSelection));
return;
case PathType.Rail:
AdjustDirectionalViewpointToPath(path, ResolvePathViewpointProfile(ViewpointStrategy.PathRailSelection));
return;
case PathType.Ground:
default:
AdjustViewpointToPathCenter(path);
return;
}
}
/// <summary> /// <summary>
/// 调整视角到路径中心,确保整个路径都在视野内 /// 调整视角到路径中心,确保整个路径都在视野内
/// </summary> /// </summary>
@ -67,6 +131,60 @@ namespace NavisworksTransport.Utils
AdjustViewpointToBoundingBox(startEndBoundingBox, pathBoundingBox, baseDimension); AdjustViewpointToBoundingBox(startEndBoundingBox, pathBoundingBox, baseDimension);
} }
private static void AdjustDirectionalViewpointToPath(PathRoute path, PathViewpointProfile profile)
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
if (path?.Points == null || path.Points.Count == 0)
{
throw new ArgumentException("路径为空或没有路径点", nameof(path));
}
Point3D startPoint = path.Points[0].Position;
Point3D endPoint = path.Points[path.Points.Count - 1].Position;
BoundingBox3D focusBoundingBox = CreateBoundingBoxFromPoints(startPoint, endPoint);
BoundingBox3D viewBoundingBox = CalculatePathBoundingBox(path);
Point3D focusCenter = GetBoundingBoxCenter(focusBoundingBox);
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
Vector3 hostUp = ToVector3(adapter.HostUpVector);
Vector3 rawPathDirection = new Vector3(
(float)(endPoint.X - startPoint.X),
(float)(endPoint.Y - startPoint.Y),
(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);
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(
cameraPosition,
focusCenter,
new Vector3D(hostUp.X, hostUp.Y, hostUp.Z),
viewBoundingBox,
expansionMargin);
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}°");
}
/// <summary> /// <summary>
/// 调整视角到指定的包围盒(通用方法) /// 调整视角到指定的包围盒(通用方法)
/// 设置俯视位置,确保包围盒完整显示在视野中 /// 设置俯视位置,确保包围盒完整显示在视野中
@ -162,6 +280,146 @@ namespace NavisworksTransport.Utils
return bounds; return bounds;
} }
internal static PathViewpointProfile ResolvePathViewpointProfile(PathType pathType)
{
switch (pathType)
{
case PathType.Hoisting:
return ResolvePathViewpointProfile(ViewpointStrategy.PathHoistingSelection);
case PathType.Rail:
return ResolvePathViewpointProfile(ViewpointStrategy.PathRailSelection);
case PathType.Ground:
default:
return ResolvePathViewpointProfile(ViewpointStrategy.PathGroundSelection);
}
}
internal static PathViewpointProfile ResolvePathViewpointProfile(ViewpointStrategy strategy)
{
switch (strategy)
{
case ViewpointStrategy.PathHoistingSelection:
return new PathViewpointProfile(distanceScale: 1.2, minDistanceMeters: 8.0, elevationDegrees: 22.0);
case ViewpointStrategy.PathRailSelection:
return new PathViewpointProfile(distanceScale: 0.9, minDistanceMeters: 6.0, elevationDegrees: 12.0);
case ViewpointStrategy.PathGroundSelection:
default:
return new PathViewpointProfile(distanceScale: 1.0, minDistanceMeters: 12.0, elevationDegrees: 90.0);
}
}
internal static FocusViewpointProfile ResolveFocusViewpointProfile(ViewpointStrategy strategy)
{
switch (strategy)
{
case ViewpointStrategy.CollisionCloseUp:
return new FocusViewpointProfile(viewAngleDegrees: 60.0, targetViewRatio: 0.25);
case ViewpointStrategy.ModelFocus:
default:
return new FocusViewpointProfile(viewAngleDegrees: 60.0, targetViewRatio: 0.25);
}
}
internal static Vector3 ResolveHorizontalPathForward(Vector3 rawPathDirection, Vector3 hostUp, Vector3 fallbackForward)
{
Vector3 projected = ProjectOntoPlane(rawPathDirection, hostUp);
if (projected.LengthSquared() > 1e-8f)
{
return Vector3.Normalize(projected);
}
Vector3 fallbackProjected = ProjectOntoPlane(fallbackForward, hostUp);
if (fallbackProjected.LengthSquared() > 1e-8f)
{
return Vector3.Normalize(fallbackProjected);
}
Vector3 canonicalFallback = Math.Abs(Vector3.Dot(Vector3.UnitX, hostUp)) > 0.9f
? Vector3.UnitY
: Vector3.UnitX;
return Vector3.Normalize(ProjectOntoPlane(canonicalFallback, hostUp));
}
internal static Vector3 ResolveCameraOffsetDirection(PathType pathType, 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)
{
case PathType.Hoisting:
horizontalDirection = -horizontalForward;
break;
case PathType.Rail:
horizontalDirection = Vector3.Cross(hostUp, horizontalForward);
if (horizontalDirection.LengthSquared() <= 1e-8f)
{
horizontalDirection = -horizontalForward;
}
break;
case PathType.Ground:
default:
horizontalDirection = hostUp;
break;
}
Vector3 offset = horizontalDirection * horizontalWeight + hostUp * verticalWeight;
return Vector3.Normalize(offset);
}
private static BoundingBox3D CreateBoundingBoxFromPoints(Point3D pointA, Point3D pointB)
{
return new BoundingBox3D(
new Point3D(
Math.Min(pointA.X, pointB.X),
Math.Min(pointA.Y, pointB.Y),
Math.Min(pointA.Z, pointB.Z)),
new Point3D(
Math.Max(pointA.X, pointB.X),
Math.Max(pointA.Y, pointB.Y),
Math.Max(pointA.Z, pointB.Z)));
}
private static Point3D GetBoundingBoxCenter(BoundingBox3D boundingBox)
{
return new Point3D(
(boundingBox.Min.X + boundingBox.Max.X) / 2.0,
(boundingBox.Min.Y + boundingBox.Max.Y) / 2.0,
(boundingBox.Min.Z + boundingBox.Max.Z) / 2.0);
}
private static double GetMaxDimension(BoundingBox3D boundingBox)
{
return Math.Max(
boundingBox.Max.X - boundingBox.Min.X,
Math.Max(
boundingBox.Max.Y - boundingBox.Min.Y,
boundingBox.Max.Z - boundingBox.Min.Z));
}
private static Vector3D ProjectVectorOntoPlane(Vector3D vector, Vector3D planeNormal)
{
double dot = vector.X * planeNormal.X + vector.Y * planeNormal.Y + vector.Z * planeNormal.Z;
return new Vector3D(
vector.X - planeNormal.X * dot,
vector.Y - planeNormal.Y * dot,
vector.Z - planeNormal.Z * dot);
}
private static Vector3 ProjectOntoPlane(Vector3 vector, Vector3 planeNormal)
{
float dot = Vector3.Dot(vector, planeNormal);
return vector - planeNormal * dot;
}
private static Vector3 ToVector3(Vector3D vector)
{
return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z);
}
/// <summary> /// <summary>
/// 计算所有碰撞位置的总包围盒 /// 计算所有碰撞位置的总包围盒
/// </summary> /// </summary>
@ -401,6 +659,12 @@ namespace NavisworksTransport.Utils
} }
} }
public static void FocusOnModelItem(ModelItem modelItem, ViewpointStrategy strategy)
{
FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
FocusOnModelItem(modelItem, profile.ViewAngleDegrees, profile.TargetViewRatio);
}
/// <summary> /// <summary>
/// 聚焦到多个模型元素(如碰撞中的两个对象) /// 聚焦到多个模型元素(如碰撞中的两个对象)
/// </summary> /// </summary>
@ -468,6 +732,12 @@ namespace NavisworksTransport.Utils
} }
} }
public static void FocusOnCollision(ModelItem item1, ModelItem item2, ViewpointStrategy strategy)
{
FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
FocusOnCollision(item1, item2, profile.ViewAngleDegrees, profile.TargetViewRatio);
}
/// <summary> /// <summary>
/// 聚焦到指定位置和尺寸 /// 聚焦到指定位置和尺寸
/// </summary> /// </summary>
@ -499,6 +769,12 @@ namespace NavisworksTransport.Utils
} }
} }
public static void FocusOnPosition(Point3D center, double targetSize, ViewpointStrategy strategy)
{
FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
FocusOnPosition(center, targetSize, profile.ViewAngleDegrees, profile.TargetViewRatio);
}
/// <summary> /// <summary>
/// 计算相机位置(使用模型标准前右上视角) /// 计算相机位置(使用模型标准前右上视角)
/// 基于 Navisworks 的标准 FrontRightTopViewVector 确定相机方向 /// 基于 Navisworks 的标准 FrontRightTopViewVector 确定相机方向