# 坐标系统一架构设计方案

## 1. 背景

当前项目运行在 Navisworks 宿主环境中，程序直接读取和写入的都是 Navisworks 世界坐标：

- `Point3D`
- `Vector3D`
- `BoundingBox3D`
- `Transform3D`
- 鼠标点击拾取点
- `Document.UpVector`

这些坐标对程序来说属于**外部坐标**。

项目目前的问题不是“完全没有坐标系抽象”，而是：

- 有一部分代码已经开始抽象 `Y-up / Z-up`
- 另一部分代码仍然直接写死世界 `Z`
- 还有一部分代码把世界原点直接当作业务球心

结果是三种语义混在一起：

1. Navisworks 外部坐标语义
2. 程序内部几何计算语义
3. 工程业务基准语义（球心、安装基准、轨道参考面）

这会导致：

- `Y-up` / `Z-up` 项目切换后功能不一致
- 终端安装仿真把世界原点误当成球心
- Rail 姿态和渲染层偷用世界 `Z`
- 不同模块对同一个点的解释不一致

因此，需要建立一套更成熟、更清晰的坐标系架构。

## 2. 设计目标

本方案的目标不是“到处加 `if (isYUp)`”，而是建立一个标准的分层架构：

- Navisworks 世界坐标统一视为**外部坐标**
- 程序内部统一使用一套**规范内部坐标**
- 工程语义使用独立的**业务基准坐标**
- 只有插件自带资源才允许引入**资产坐标系**
- 坐标转换只发生在少数边界入口
- 业务逻辑禁止直接依赖宿主坐标语义

## 3. 总体方案

### 3.1 内部统一坐标

程序内部统一使用：

- **Canonical Space（规范内部坐标）**
- **固定为 `Z-up`**

选择 `Z-up` 的原因：

1. 当前项目大量成熟逻辑本身就是按 `Z-up` 语义构建的
2. 动画、渲染、yaw 语义、很多 helper 都更接近 `Z-up`
3. 以 `Z-up` 作为内部标准，改造成本低于整体改成 `Y-up`

注意：

- 这只是程序内部选择
- 不代表客户模型必须是 `Z-up`
- 客户仍可继续使用 `Y-up` 项目

### 3.2 三层坐标语义

```mermaid
flowchart LR
    A["Navisworks Host Space\n外部坐标"] --> B["Host Coordinate Adapter\n边界适配层"]
    B --> C["Canonical Space (Z-up)\n内部统一坐标"]
    C --> D["Project Reference Frame\n业务基准坐标"]

    C --> E["路径规划/几何计算"]
    C --> F["Rail 姿态/动画"]
    C --> G["碰撞检测/结果恢复"]
    C --> H["渲染/辅助线/通行空间"]

    D --> I["球心"]
    D --> J["终端安装基准"]
    D --> K["轨道参考面"]

    C --> B
    B --> L["Navisworks 输出\n渲染/移动/截图恢复"]
```

三层职责如下：

#### A. Navisworks Host Space（外部坐标）

宿主 API 直接提供的坐标。

特点：

- 是程序的输入/输出坐标
- UI 文本框、对话框、日志、鼠标拾取结果，统一按宿主坐标系解释和显示
- 可能来自 `Y-up` 项目，也可能来自 `Z-up` 项目
- 不应直接当成内部计算坐标

#### B. Canonical Space（内部统一坐标）

程序内部唯一允许进行几何计算、路径计算、姿态计算的坐标空间。

特点：

- 固定为 `Z-up`
- 只解决坐标轴和方向语义统一问题
- 不承载业务基准含义

#### C. Project Reference Frame（业务基准坐标）

建立在内部统一坐标基础上的工程语义层。

负责表达：

- 球心
- 项目 up 方向的业务解释
- 终端安装参考面
- 轨道参考面

### 3.2.1 术语约束

后续文档、代码注释和日志中，统一使用以下说法：

- **宿主坐标系（Host Space）**
  - 指 Navisworks 文档坐标系
  - `Y-up` / `Z-up` 的判断只属于这一层
- **内部坐标系（Canonical Space）**
  - 指程序内部统一使用的 `Z-up` 坐标系
  - 仅供内部计算使用，禁止直接暴露给 UI
- **资产坐标系（Asset Space）**
  - 只用于插件自带资源
  - 当前明确只有两类：`虚拟物体` 与 `单位圆柱体（参考杆资源）`
  - 用于描述这些资源文件自身的 `Forward/Up/Side` 轴约定

禁止继续使用含糊的“本地坐标系”说法，因为它容易混淆：

- 宿主坐标系
- 内部坐标系
- 资产坐标系

后续凡是 UI 输入/输出，必须明确是**宿主坐标系**语义；凡是姿态或几何内部计算，必须明确是**内部坐标系**语义；凡是虚拟物体/单位圆柱体资源朝向，必须明确是**资产坐标系**语义。
- 业务锚点

### 3.2.2 Quaternion / Rotation3D 解释约束

坐标系分层之外，还必须固定一条旋转解释规则：

- Navisworks `Rotation3D(double, double, double, double)` 的参数顺序固定为 `x, y, z, w`
- `Rotation3D.A/B/C/D` 固定对应 quaternion 的 `x/y/z/w`

这条规则在项目中视为硬约束，禁止后续代码再次用不同顺序解释四元数。

因此：

- 内部坐标系里算出的 quaternion `(qx, qy, qz, qw)`，写回 Navisworks 时必须使用：

```csharp
var rotation = new Rotation3D(qx, qy, qz, qw);
```

- 后续遇到姿态异常时，不应再次怀疑 `Rotation3D` 分量顺序；应优先检查：
  - 宿主坐标系 / 内部坐标系 / 资产坐标系 是否混用
  - fragment 参考姿态是否被误当成业务语义姿态
  - 真实物体是否错误地只传 quaternion 而没有保留显式参考轴

这层不能偷用世界原点，也不能偷用宿主世界轴。

### 3.3 坐标系定义必须包含的语义

在本项目中，`Y-up` / `Z-up` 不能只被理解成“点坐标怎么换算”。

一个完整的坐标系定义，至少必须显式包含：

- `UpAxis`
- `ElevationAxis`
- `HorizontalPlane`
- 必要时的 `Handedness`

因此：

- `Y-up` 的含义是：
  - `Y` 为 up 轴
  - `Y` 为高程轴
  - `XZ` 为水平平面
- `Z-up` 的含义是：
  - `Z` 为 up 轴
  - `Z` 为高程轴
  - `XY` 为水平平面

后续凡是出现：

- 高度
- 底面
- 顶面
- 通行空间高度轴
- 俯仰/法向

都必须基于这组定义来解释，不能继续偷用“世界 Z 就是 up”的旧假设。

### 3.4 资产坐标系是独立层

除了宿主坐标系和内部统一坐标系，还必须区分**资产坐标系**。

注意，这一层**不是所有模型都有**。当前项目里只有插件自带资源需要它：

- 虚拟物体资源
- 单位圆柱体参考杆资源

这是另一层独立定义，至少包含：

- `AssetForwardAxis`
- `AssetUpAxis`

例如：

- 虚拟物体资源通常按程序约定构建，可能是 `Asset X = Forward, Asset Z = Up`
- 单位圆柱体参考杆资源当前约定为 `Asset X = 杆轴方向, Asset Z = 截面 Up`

而对于客户真实模型：

- 不单独引入“资产坐标系”概念
- UI 和数据输入输出仍只认宿主坐标系
- 内部算法如需统一姿态，先进入 Canonical Space，再叠加业务参考信息

这意味着：

- 即使宿主坐标系转换已经正确
- 如果资产坐标系语义没有显式处理
- 动画和姿态仍然会出现“路径对了、通行空间对了、模型自己站歪了”的现象

因此，后续姿态系统必须显式区分：

1. 宿主坐标系定义
2. 内部统一坐标系定义
3. 业务基准定义
4. 资产坐标系约定（仅限虚拟物体和单位圆柱体等插件自带资源）

### 3.5 Rail 局部坐标系与动画跟踪点

`Rail` 路径不能再只理解成“沿世界 up 做上下偏移”。

对 `Rail` 来说，必须显式建立一套局部坐标系：

- `Forward`
  - 沿轨道前进方向
- `Normal`
  - 安装法向
  - 可以是任意空间角度
  - 但应尽量贴近项目 up
- `Lateral`
  - 由 `Normal x Forward` 或正交化后确定

后续所有 `Rail` 相关计算都应建立在这套 `RailLocalFrame` 之上：

- 物体姿态
- 通行空间姿态
- 路径参考点到动画跟踪点的偏移
- 终点贴合语义

### 3.6 动画跟踪点统一语义

动画系统内部不应混用：

- 底面中心
- 顶面中心
- 包围盒中心
- 路径参考点

统一规则应为：

- **动画跟踪点 = 当前动画主链路使用的唯一位置语义**
- 当前阶段建议统一为：**几何中心**

这样做的原因是：

- 中心点对三维旋转最中性
- 不依赖“当前哪一面朝上”
- 不依赖宿主 `Y-up / Z-up`
- 物体姿态与碰撞恢复、截图回放更容易共用同一套语义

不同路径的业务需求通过“参考点 -> 跟踪中心点”的显式偏移来表达：

- 地面/吊装：通常沿 `+up / -up`
- Rail：沿 `RailLocalFrame.Normal`

这样：

- 业务层只表达“路径参考点”和“法向偏移”
- 动画层只认“中心点 + 姿态”

## 4. 核心设计原则

### 4.1 Navisworks 世界坐标统一视为外部坐标

这是本方案的第一条硬规则。

对程序而言，以下数据统一视为外部输入：

- `Point3D`
- `Vector3D`
- `BoundingBox3D`
- `Transform3D`
- `ModelItem.BoundingBox()`
- `Document.UpVector`
- 鼠标点击拾取点

无论当前 NWD/NWC 是客户原始模型，还是预先转换保存后的模型，
**只要是从 Navisworks API 读出来的，它对程序来说就是外部坐标。**

### 4.1.1 外部坐标不等于内部语义

外部坐标虽然来自 Navisworks，但不能直接拿来推导内部业务语义。

特别是以下概念必须先经过坐标定义解释：

- 哪个轴是 up
- 哪个轴是高程
- 哪个平面是水平面
- 一个 `BoundingBox` 的“底面”到底是哪一面

也就是说：

- 外部坐标是输入
- 坐标定义决定如何解释这个输入
- 业务代码不得跳过这一步

### 4.2 程序内部一律只认 Canonical Space

业务层不得直接消费 Navisworks 坐标。

禁止这样做：

```csharp
// ❌ 错误：直接拿宿主包围盒结果开始做业务计算
var bounds = item.BoundingBox();
var center = bounds.Center;
var direction = new Vector3D(center.X, center.Y, center.Z);
```

正确做法应当是：

```csharp
// ✅ 正确：先通过边界适配层转换到内部统一坐标
var hostBounds = item.BoundingBox();
var bounds = adapter.ToCanonicalBounds(hostBounds);
var center = bounds.Center;
```

### 4.3 坐标系层只解决轴变换，不解决业务语义

坐标适配层只负责：

- 外部坐标到内部坐标的变换
- 内部坐标到外部坐标的逆变换
- up 轴、高程轴、水平面定义

不负责：

- 球心是否在 `(0,0,0)`
- 终端安装是否指向球心
- 顶面/底面对接如何定义
- 轨道参考面在哪里

这些都属于业务基准层。

### 4.4 项目基准点必须显式配置或显式求解

像“球心”这类工程点不属于坐标系本身。

不能因为某一批模型里球心恰好在世界原点，就长期把它写死。

应使用以下来源之一：

- 项目配置
- 明确的设计资料
- 多条向心轴线拟合
- 其他明确的业务求解方式

### 4.5 不在业务代码中散落 `Y-up / Z-up` 分支

不推荐：

```csharp
// ❌ 错误
if (isYUp)
{
    ...
}
else
{
    ...
}
```

推荐：

- 边界适配层统一处理 `Host -> Canonical`
- 业务层只消费 Canonical 数据

### 4.6 渲染几何也必须完成坐标转换

坐标系改造不能只停留在：

- 点坐标转换
- 中心点偏移
- 业务锚点转换

对于以下可视化对象，还必须同步改造它们的**局部几何轴构造**：

- 通行空间长方体
- 辅助线杆体
- 圆形/圆柱标记
- 切向、法向、侧向相关的渲染面片

否则会出现一种典型错误：

- 对象中心点已经在正确位置
- 但渲染几何仍然按世界 `Z-up` 去构造 `right/up/height`
- 最终看起来仍然像 `Z-up`，即使业务点位已经是对的

本项目已经出现过这一类问题：

- `Y-up` 模型中，通行空间中心点偏移已经正确
- 但长方体本体仍然按世界 `XY + Z` 构造
- 导致通行空间整体看起来仍是 `Z-up`

因此，渲染层必须遵守以下规则：

1. 凡是依赖 `up/right/forward/normal` 的渲染几何，必须基于统一坐标语义构造局部轴。
2. 不允许只修改“中心点/偏移量”而保留旧的世界轴构造公式。
3. 如果渲染输出面向 Navisworks 宿主，则应先在 Canonical Space 中完成几何语义计算，再转换回宿主坐标输出。
4. 对长方体、圆柱体这类实体渲染，必须同时验证：
   - 中心点是否正确
   - 局部高度轴是否正确
   - 局部侧向轴是否正确
   - 法向/截面方向是否正确

## 5. 推荐的技术方案

### 5.1 使用完整变换矩阵作为适配基础

不推荐继续停留在：

- `GetElevation()`
- `GetHorizontalCoords()`
- `CreatePoint()`

这类偏二维、局部的接口上。

对三维动画、Rail 姿态、碰撞恢复来说，更成熟的方案是：

- 使用完整的空间变换对象
- 以矩阵/旋转+平移为核心

即：

- `ExternalToCanonical`
- `CanonicalToExternal`

这两套变换应成为边界层的基础能力。

### 5.2 建议新增的核心组件

#### 1. `HostCoordinateAdapter`

职责：

- 统一处理 Navisworks 外部坐标到 Canonical Space 的转换
- 统一处理 Canonical Space 到 Navisworks 外部坐标的反向转换

建议能力：

- `ToCanonicalPoint(...)`
- `ToCanonicalVector(...)`
- `ToCanonicalBounds(...)`
- `ToCanonicalTransform(...)`
- `FromCanonicalPoint(...)`
- `FromCanonicalVector(...)`
- `FromCanonicalBounds(...)`
- `FromCanonicalTransform(...)`

#### 2. `CanonicalTransform`

职责：

- 封装 `ExternalToCanonical / CanonicalToExternal`
- 提供点、向量、姿态、包围盒的统一变换

#### 3. `ProjectReferenceFrame`

职责：

- 管理业务基准语义

建议包含：

- `SphereCenterInCanonical`
- `ProjectUpInCanonical`
- `RailReferencePlane`
- `AssemblyReferenceFrame`

### 5.3 业务层只消费 Canonical 对象

终端安装、Rail、动画、碰撞、渲染层不应直接使用：

- Navisworks `Point3D`
- Navisworks `BoundingBox3D`
- Navisworks `Transform3D`

而应通过适配层先转成内部统一对象后再计算。

## 6. 输入输出边界

### 6.1 必须拦截的输入入口

以下都是必须拦截的宿主输入点：

- 鼠标点击获取路径点
- `ModelItem.BoundingBox()`
- `ModelItem.Transform`
- `Geometry.BoundingBox`
- `Document.UpVector`
- `PathClickToolPlugin` 等交互工具返回的点

这些点一旦进入业务逻辑，就应先转换到 Canonical Space。

### 6.2 必须反向转换的输出入口

以下都是必须回写到宿主时做反向转换的输出点：

- 3D 渲染点、线、法向
- 动画对象位置与姿态
- 碰撞点恢复
- 碰撞报告截图定位
- 辅助线/参考杆/通行空间可视化

## 7. 当前项目中的优先改造模块

以下模块优先级最高，因为它们既参与三维姿态，又直接暴露了世界坐标假设问题：

### 7.1 必改

- [PathEditingViewModel.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/UI/WPF/ViewModels/PathEditingViewModel.cs)
  - 当前终端安装仿真里仍把世界原点当球心
- [RailPathPoseHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/RailPathPoseHelper.cs)
  - 当前仍把世界 `Z` 当 `worldUp`
- [PathPointRenderPlugin.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/PathPointRenderPlugin.cs)
  - 当前大量渲染法向仍写死 `(0,0,1)`
- [PathAnimationManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Animation/PathAnimationManager.cs)
  - 需要逐步统一输入输出边界
- [CollisionSceneHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/CollisionSceneHelper.cs)
  - 需要确保碰撞恢复只使用内部语义

### 7.2 次改

- 自动路径规划相关的高度、坡度、网格构建
- 历史二维 `yaw` 辅助逻辑
- 旧视图辅助和截图辅助

这些部分可以后续逐步纳入统一架构，不必阻塞当前终端安装与 Rail 主线。

## 8. 迁移策略

不建议一次性全项目重构。

建议按以下顺序推进：

### 阶段 1

建立边界层：

- `HostCoordinateAdapter`
- `CanonicalTransform`
- `ProjectReferenceFrame`

### 阶段 2

先接入以下主线功能：

- 终端安装仿真
- Rail 姿态
- 动画播放
- 碰撞恢复
- 辅助线/通行空间渲染

阶段 2 的两个重要约束：

1. 真实物体物理尺寸必须固定

- 起点贴合、动画帧生成、通行空间尺寸、碰撞恢复
- 必须共用同一份固定物理尺寸
- 不允许在对象已经旋转后，再从当前世界 AABB 重新推导“真实高度”

否则会产生典型错误：

- 起点贴合正确
- 动画第一帧立刻出现固定间隙

2. 渲染与业务计算必须同时改

- 不能只改参考点、中心点、偏移量
- 还必须同步改渲染几何局部轴：
  - `right`
  - `up`
  - `normal`
  - `height axis`

否则会出现：

- 业务点位正确
- 但通行空间/辅助杆/长方体仍按旧 `Z-up` 轴构造

### 阶段 3

再逐步改造：

- 自动路径规划
- 高度检测
- 坡度分析
- 旧二维路径辅助逻辑

### 部署约束

WPF 插件的最终部署，必须依赖完整主项目构建产物，而不应默认复用测试顺带生成的程序集。

原因：

- DLL 时间戳正确，不代表插件可运行
- 如果 `TransportPlugin.g.resources` 不完整，Navisworks 在创建面板时仍会因缺少 `.baml` 崩溃

必须保证：

- 主项目完整构建成功
- 关键视图资源已经编入程序集

最低检查集：

- `LogisticsControlPanel.baml`
- `PathEditingView.baml`
- `AnimationControlView.baml`
- `LayerManagementView.baml`

## 9. 关键结论

本项目如果要长期稳定支持 `Y-up` 和 `Z-up` 项目，正确方向不是：

- 到处散落 `if (isYUp)`
- 强行要求客户把模型先转成 `Z-up`
- 继续混用世界原点和业务球心

而应该是：

- **Navisworks 世界坐标统一视为外部坐标**
- **程序内部统一使用 Canonical Space（Z-up）**
- **业务基准点单独建模**
- **UI 输入输出统一使用宿主坐标系**
- **资产坐标系只属于插件自带资源，不得泛化成“所有模型都有本地轴”**
- **坐标转换只发生在边界层**

这是一条更接近业界三维软件成熟做法的路线。
