Refine rail assembly workflow and viewpoint strategies
This commit is contained in:
parent
c3b103c159
commit
2e0b26e032
50
AGENTS.md
50
AGENTS.md
@ -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 固定定义
|
||||||
|
|
||||||
这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口:
|
这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口:
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
118
UnitTests/CoordinateSystem/RailAssemblyWorkflowContextTests.cs
Normal file
118
UnitTests/CoordinateSystem/RailAssemblyWorkflowContextTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
UnitTests/CoordinateSystem/ViewpointHelperTests.cs
Normal file
80
UnitTests/CoordinateSystem/ViewpointHelperTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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。
|
||||||
|
|||||||
@ -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}");
|
||||||
|
|||||||
@ -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
154
src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs
Normal file
154
src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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}"
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 确定相机方向
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user