From fdb857af17d05532d8bc92230216ef1d4409f437 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 13 Apr 2026 10:31:14 +0800 Subject: [PATCH] Refactor auto path planning coordinate semantics --- NavisworksTransport.UnitTests.csproj | 1 + TransportPlugin.csproj | 1 + ...utoPathPlanningCoordinateSemanticsTests.cs | 114 ++++++ ...-path-planning-coordinate-refactor-plan.md | 338 ++++++++++++++++++ src/PathPlanning/AutoPathFinder.cs | 145 ++++---- src/PathPlanning/GridMap.cs | 86 ++++- src/PathPlanning/PathOptimizer.cs | 74 ++-- .../CoordinateSystem/HostPlanarGridHelper.cs | 99 +++++ .../CoordinateSystem/YUpCoordinateSystem.cs | 23 +- .../CoordinateSystem/ZUpCoordinateSystem.cs | 23 +- 10 files changed, 797 insertions(+), 107 deletions(-) create mode 100644 UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs create mode 100644 doc/working/2026-04-13-auto-path-planning-coordinate-refactor-plan.md create mode 100644 src/Utils/CoordinateSystem/HostPlanarGridHelper.cs diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index 113b76a..8962550 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -60,6 +60,7 @@ + diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 6cf663a..4b86f88 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -359,6 +359,7 @@ + diff --git a/UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs b/UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs new file mode 100644 index 0000000..d6e1382 --- /dev/null +++ b/UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs @@ -0,0 +1,114 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class AutoPathPlanningCoordinateSemanticsTests + { + [TestMethod] + public void YUp_HostPlanarGridHelper_CreateHostPoint_ShouldApplyElevationOnHostY() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + + Vector3 point = HostPlanarGridHelper.CreateHostPoint3(2.0, 3.0, 14.83, adapter); + + AssertPoint(point, 2.0, 14.83, 3.0); + } + + [TestMethod] + public void ZUp_HostPlanarGridHelper_CreateHostPoint_ShouldApplyElevationOnHostZ() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp); + + Vector3 point = HostPlanarGridHelper.CreateHostPoint3(2.0, 3.0, 6.25, adapter); + + AssertPoint(point, 2.0, 3.0, 6.25); + } + + [TestMethod] + public void YUp_HostPlanarGridHelper_GetAndSetElevation_ShouldUseHostY() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var hostPoint = new Vector3(4.0f, 15.0f, 6.0f); + + double elevation = HostPlanarGridHelper.GetElevation3(hostPoint, adapter); + Vector3 adjusted = HostPlanarGridHelper.SetElevation3(hostPoint, 20.0, adapter); + + Assert.AreEqual(15.0, elevation, 1e-6); + AssertPoint(adjusted, 4.0, 20.0, 6.0); + } + + [TestMethod] + public void YUp_HostPlanarGridHelper_GetHorizontalCoords_ShouldProjectToHostXZPlane() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var hostPoint = new Vector3(8.0f, 14.83f, -5.35f); + + (double h1, double h2) = HostPlanarGridHelper.GetHorizontalCoords3(hostPoint, adapter); + + Assert.AreEqual(8.0, h1, 1e-6); + Assert.AreEqual(-5.35, h2, 1e-6); + } + + [TestMethod] + public void YUp_HostPlanarGridHelper_HorizontalRange_ShouldMatchHostXZPlane() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var hostMin = new Vector3(-10.0f, 2.0f, -30.0f); + var hostMax = new Vector3(40.0f, 12.0f, 5.0f); + + (double min1, double max1, double min2, double max2) = + HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter); + + Assert.AreEqual(-10.0, min1, 1e-6); + Assert.AreEqual(40.0, max1, 1e-6); + Assert.AreEqual(-30.0, min2, 1e-6); + Assert.AreEqual(5.0, max2, 1e-6); + } + + [TestMethod] + public void YUp_GridLikeWorldPointConstruction_ShouldWriteElevationToHostY() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + double originH1 = -210.0; + double originH2 = -10.0; + double cellSize = 1.0; + + Vector3 worldPoint = HostPlanarGridHelper.CreateHostPoint3( + originH1 + 4 * cellSize, + originH2 + 6 * cellSize, + 14.83, + adapter); + + Assert.AreEqual(-206.0, worldPoint.X, 1e-6); + Assert.AreEqual(14.83, worldPoint.Y, 1e-6); + Assert.AreEqual(-4.0, worldPoint.Z, 1e-6); + } + + [TestMethod] + public void YUp_GridLikeWorldToGrid_ShouldReadHostXZAsPlanarAxes() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var origin = new Vector3(-210.0f, 14.83f, -10.0f); + var endPoint = new Vector3(-169.20f, 14.83f, -5.35f); + const double cellSize = 1.0; + + (double pointH1, double pointH2) = HostPlanarGridHelper.GetHorizontalCoords3(endPoint, adapter); + (double originH1, double originH2) = HostPlanarGridHelper.GetHorizontalCoords3(origin, adapter); + int gridX = (int)System.Math.Round((pointH1 - originH1) / cellSize); + int gridY = (int)System.Math.Round((pointH2 - originH2) / cellSize); + + Assert.AreEqual(41, gridX); + Assert.AreEqual(5, gridY); + } + + private static void AssertPoint(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); + } + } +} diff --git a/doc/working/2026-04-13-auto-path-planning-coordinate-refactor-plan.md b/doc/working/2026-04-13-auto-path-planning-coordinate-refactor-plan.md new file mode 100644 index 0000000..1e24334 --- /dev/null +++ b/doc/working/2026-04-13-auto-path-planning-coordinate-refactor-plan.md @@ -0,0 +1,338 @@ +# 自动路径规划坐标系重构实施方案 + +更新时间:2026-04-13 +状态:实施中 +目标:将 `Ground` 自动路径规划整条链路并入当前稳定的“宿主坐标系 -> Canonical Space -> 宿主坐标系”架构,消除旧 `XY` 平面 / `Z` 高度硬编码,不再针对单一 `YUp` 现象打补丁。 + +--- + +## 1. 背景与问题定义 + +当前项目的坐标系架构已经明确: + +- 宿主坐标系(Host Space) + - Navisworks 文档坐标系 + - UI 输入输出、鼠标拾取、日志、渲染交互都属于这一层 +- 内部坐标系(Canonical Space) + - 项目内部统一固定为 `ZUp` + - 几何、姿态、法向、路径计算优先在这一层完成 +- 业务语义层 + - 在 canonical 之上表达地面接触点、吊点、轨道法向、动画跟踪点等业务含义 + +但自动路径规划链路目前仍大量保留旧实现习惯: + +- 默认 `XY` 是水平平面 +- 默认 `Z` 是高度轴 +- 默认世界 `Z` 可以直接代表“向上” +- 路径起终点修正仍然是“保留 `XY`,替换 `Z`” + +这说明当前问题不是“某个 `YUp` 细节没处理”,而是: + +- 自动路径规划整条链没有接入当前稳定的坐标系架构 + +因此本方案的目标不是修一个 `YUp` bug,而是把自动路径规划整体收编到现有坐标系体系中。 + +--- + +## 2. 现象与直接证据 + +以 `YUp` 文档中的地面自动路径为例,日志显示: + +- 起点:`(-209.94, 14.83, -1.68)` +- 终点:`(-169.20, 14.83, -5.35)` + +对当前文档来说,宿主 `up` 是 `Y`,所以: + +- 地面高度应主要体现在 `Y` +- `Z` 应属于宿主水平面的一部分 + +但自动路径规划链中仍有如下旧假设: + +1. `PathPlanningManager` 按 `X/Y` 计算边界与网格尺寸 +- `src/Core/PathPlanningManager.cs` +- `GetModelBounds()` +- `CalculateOptimalGridSize(...)` + +2. `AutoPathFinder` 把图节点还原为: +- `new Point3D(worldPos.X, worldPos.Y, nodeInfo.Z)` + +3. `AutoPathFinder` 的起终点修正逻辑仍是: +- 保留原始 `X/Y` +- 只替换 `Z` + +这套实现天然绑定旧 `ZUp` 语义,无法正确支持当前多坐标系架构。 + +--- + +## 3. 重构目标 + +本次重构要达成的不是“YUp 也能跑”,而是以下更高层目标: + +1. 自动路径规划输入输出继续保持宿主坐标语义 +- 用户点击点、UI 日志、路径显示、数据库路径点仍然按宿主坐标解释 + +2. 自动路径规划内部计算统一切到 canonical +- 网格范围 +- 栅格映射 +- A* 路径求解 +- 高度层处理 +- 曲线化前的路径点 + +3. Ground 自动路径与通行空间、路径渲染、动画主链共享同一套 up / 高度语义 + +4. 禁止继续保留旧 `XY/Z` 写死逻辑作为 fallback + +--- + +## 4. 范围界定 + +### 4.1 本次重构范围 + +- `PathEditingViewModel` + - 自动路径起点/终点输入边界 +- `PathPlanningManager` + - 自动路径入口、边界、网格参数、路径落库前收口 +- `GridMapGenerator` + - BIM 到网格的平面/高度语义 +- `AutoPathFinder` + - 路径搜索、节点转世界坐标、首尾点修正 +- 与自动路径规划直接耦合的测试 + +### 4.2 暂不纳入本次范围 + +- 手动路径编辑主链 +- 吊装路径自动生成链 +- Rail 自动生成链 +- 动画姿态链 +- 碰撞恢复链 + +说明: + +- 本次先聚焦 `Ground` 自动路径规划 +- 但设计必须是通用的宿主/内部坐标边界,不允许做成“只为 `YUp` 单独补丁” + +--- + +## 5. 关键设计原则 + +### 5.1 边界清晰 + +- 宿主坐标只在输入输出边界存在 +- 内部计算统一使用 canonical +- 不允许在算法中间层直接假设世界 `Z` 是高度 + +### 5.2 不新增 fallback + +不允许: + +- canonical 转换失败时回退旧 `XY/Z` 逻辑 +- `YUp` 失败时偷偷走旧分支 +- 用“如果是 YUp 就特判”掩盖整体架构问题 + +### 5.3 优先复用现有坐标工具 + +优先复用: + +- `HostCoordinateAdapter` +- `CanonicalTrackedPositionResolver` +- 坐标系统一设计文档中已经稳定的术语和边界 + +### 5.4 先锁测试,再动实现 + +本次重构必须先补针对多坐标系的自动路径测试,再改实现。 + +--- + +## 6. 目标架构 + +目标链路应收敛为: + +```text +宿主点击点 / 宿主通道边界 / 宿主模型几何 + ↓ HostCoordinateAdapter +Canonical 起点 / 终点 / 边界 / 网格 / 高度层 + ↓ 自动路径规划算法(统一在 canonical 内) +Canonical 路径点 + ↓ HostCoordinateAdapter +宿主路径点 / 渲染 / UI / 数据库存储 +``` + +关键语义: + +- canonical 内部固定: + - `XY` 为水平平面 + - `Z` 为高度 +- 宿主坐标系的 `YUp/ZUp` 差异只由 `HostCoordinateAdapter` 负责吸收 + +--- + +## 7. 分阶段实施计划 + +### Phase 0:现状锁定与测试补齐 + +目标: + +- 先把当前正确/错误行为锁定,避免重构中语义漂移 + +任务: + +- [ ] 新增 `Ground` 自动路径在 `ZUp` 文档下的首尾点测试 +- [ ] 新增 `Ground` 自动路径在 `YUp` 文档下的首尾点测试 +- [ ] 新增“宿主点 -> 自动路径 -> 路径点输出”边界测试 +- [ ] 新增“路径点 / 路径线 / 通行空间”首尾地面一致性测试 + +验收标准: + +- 在没有改实现前,测试至少能明确暴露 `YUp` 下当前错误 +- `ZUp` 当前稳定行为被锁住,不允许回归 + +### Phase 1:输入边界 canonical 化 + +目标: + +- 自动路径规划入口不再把宿主点直接扔进旧 `XY/Z` 规划逻辑 + +任务: + +- [ ] 梳理 `PathEditingViewModel` 起点/终点点击数据的宿主语义 +- [ ] 在自动路径规划入口显式引入 `HostCoordinateAdapter` +- [ ] 将 `AutoPlanPath(...)` 的内部输入切换为 canonical 起点/终点 +- [ ] 明确日志里同时打印宿主输入点与 canonical 输入点 + +验收标准: + +- 自动路径规划入口不再直接依赖宿主 `YUp/ZUp` +- 起点终点进入算法前,坐标语义清晰可见 + +### Phase 2:网格与边界 canonical 化 + +目标: + +- 网格边界、障碍物、通道数据全部按 canonical 平面/高度工作 + +任务: + +- [ ] 重构 `GetModelBounds()` / `CalculateChannelsBounds(...)` +- [ ] 重构 `CalculateOptimalGridSize(...)`,不再把 `X/Y` 固定当平面 +- [ ] 重构 `GridMapGenerator.GenerateFromBIM(...)` 的输入语义 +- [ ] 明确网格二维平面坐标与 canonical 三维坐标的边界 + +验收标准: + +- 网格生成不再依赖宿主世界 `XY` +- `YUp` / `ZUp` 两类文档进入网格阶段后语义完全一致 + +### Phase 3:路径求解与输出 canonical 化 + +目标: + +- `AutoPathFinder` 内部只处理 canonical +- 路径输出统一经适配器返回宿主 + +任务: + +- [ ] 重构图节点到三维点的转换逻辑 +- [ ] 删除或重写“保留 XY、替换 Z”的旧首尾点修正 +- [ ] 重构路径输出与落库前的宿主还原逻辑 +- [ ] 日志里同时打印 canonical 路径首尾点与宿主路径首尾点 + +验收标准: + +- 路径首尾点在 `YUp/ZUp` 下都能正确贴回宿主地面 +- 不再出现“终点被压到地面下方”的旧 `ZUp` 语义残留 + +### Phase 4:联动校验与收尾 + +目标: + +- 自动路径规划与可视化、通行空间、动画链的基础语义重新对齐 + +任务: + +- [ ] 检查路径点显示是否仍使用宿主坐标 +- [ ] 检查路径线和通行空间首尾点是否与路径点一致 +- [ ] 清理旧 `XY/Z` 注释、旧日志、旧辅助方法 +- [ ] 更新 `current-engineering-state.md` 中自动路径规划状态说明 + +验收标准: + +- 自动路径规划的起点、终点、路径线、通行空间在多坐标系文档下语义一致 +- 旧 `XY/Z` 硬编码实现不再残留为正式执行链 + +--- + +## 8. 关键改动点清单 + +高风险文件: + +- `src/UI/WPF/ViewModels/PathEditingViewModel.cs` +- `src/Core/PathPlanningManager.cs` +- `src/PathPlanning/GridMapGenerator.cs` +- `src/PathPlanning/AutoPathFinder.cs` + +重点清理的旧逻辑: + +- [ ] “默认 `XY` 为地面平面”的边界计算 +- [ ] “默认 `Z` 为高度”的路径节点组装 +- [ ] “保留 `XY`,替换 `Z`”的起终点修正 +- [ ] 任何直接写死世界 `Z` 为 up 的自动路径规划实现 + +--- + +## 9. 测试与验证策略 + +### 9.1 单元/集成测试 + +至少补齐: + +- [ ] `ZUp Ground` 自动路径首尾点测试 +- [ ] `YUp Ground` 自动路径首尾点测试 +- [ ] 起点/终点保持宿主地面接触语义测试 +- [ ] 路径输出后首尾点与通行空间一致性测试 + +### 9.2 手工验证 + +在真实 Navisworks 场景中验证: + +- [ ] `ZUp` 模型中自动地面路径仍正常显示 +- [ ] `YUp` 模型中自动地面路径终点不再掉到地面下 +- [ ] 默认显示模式下能看到起点、终点和路径线 +- [ ] 打开通行空间后,首尾位置与路径点一致 + +--- + +## 10. 风险与注意事项 + +1. 自动路径规划链路历史较老,旧实现可能分散在多个辅助方法中 +- 不能只改主入口,必须顺着网格、A*、输出一路清理 + +2. 数据库存储仍然是宿主坐标语义 +- 不能把 canonical 点直接落库 + +3. 路径渲染和自动路径规划不能各自维护一套“高度轴”解释 +- 否则会再次出现“路径点对、通行空间错”或反过来的问题 + +4. 本次重构不是 `Ground` 的最后一步 +- 后续 `Hoisting/Rail` 若要做自动路径,也应直接复用这次收好的边界层 + +--- + +## 11. 进度记录 + +### 2026-04-13 + +- [x] 确认当前自动路径规划主链仍大量保留旧 `XY/Z` 假设 +- [x] 明确本次方向应为“并入现有坐标系架构”,而不是追加 `YUp` 特判 +- [x] 建立本实施方案文档 +- [x] 引入 `HostPlanarGridHelper`,明确“旧 ICoordinateSystem 水平/高程契约”与 `HostCoordinateAdapter` 的桥接边界 +- [x] `YUpCoordinateSystem` / `ZUpCoordinateSystem` 改为复用统一桥接 helper,而不是各自散落实现 +- [x] 为宿主平面/高程桥接补充纯数学单测,锁定: + - `YUp` 高程走宿主 `Y` + - `YUp` 水平面走宿主 `XZ` + - `ZUp` 仍保持宿主 `XY + Z` +- [x] `AutoPathFinder` 第一轮收口完成: + - 起终点映射改为通过 `gridMap.GetWorldElevation(...)` + - 图节点 / 路径点还原改为通过 `gridMap.CreateWorldPoint(...)` + - 高度层匹配与路径恢复改为通过 `gridMap.GetWorldElevation(...) / SetWorldElevation(...)` +- [ ] Phase 0 剩余:补“自动路径首尾点”更贴近业务的回归测试 +- [ ] Phase 2 尚未开始:`GridMapGenerator` 仍存在较多旧 `XY/Z` 硬编码 diff --git a/src/PathPlanning/AutoPathFinder.cs b/src/PathPlanning/AutoPathFinder.cs index 415c0dc..addaedd 100644 --- a/src/PathPlanning/AutoPathFinder.cs +++ b/src/PathPlanning/AutoPathFinder.cs @@ -495,8 +495,11 @@ namespace NavisworksTransport.PathPlanning var endGridPos = gridMap.WorldToGrid(end); // 找到起点和终点最近的3D节点 - var startNode = FindClosest3DNode(graph3D, startGridPos, start.Z); - var endNode = FindClosest3DNode(graph3D, endGridPos, end.Z); + double startElevation = gridMap.GetWorldElevation(start); + double endElevation = gridMap.GetWorldElevation(end); + + var startNode = FindClosest3DNode(graph3D, startGridPos, startElevation); + var endNode = FindClosest3DNode(graph3D, endGridPos, endElevation); // 起点和终点必须检查可达性 bool startOnObstacle = (startNode == null); @@ -524,8 +527,7 @@ namespace NavisworksTransport.PathPlanning foreach (var node in graph3D.AllNodes) { var info = graph3D.NodeInfo[node]; - var nodeWorldPos = gridMap.GridToWorld3D(new GridPoint2D(info.X, info.Y)); - nodeWorldPos = new Point3D(nodeWorldPos.X, nodeWorldPos.Y, info.Z); + var nodeWorldPos = gridMap.CreateWorldPoint(new GridPoint2D(info.X, info.Y), info.Z); double distance = Math.Sqrt( Math.Pow(nodeWorldPos.X - end.X, 2) + @@ -554,8 +556,8 @@ namespace NavisworksTransport.PathPlanning var startInfo = graph3D.NodeInfo[startNode]; var endInfo = graph3D.NodeInfo[endNode]; - LogManager.Info($"[A*执行-3D] 起点映射: ({startGridPos.X},{startGridPos.Y},Z={start.Z:F2}) -> 节点({startInfo.X},{startInfo.Y},层{startInfo.LayerIndex},Z={startInfo.Z:F2})"); - LogManager.Info($"[A*执行-3D] 终点映射: ({endGridPos.X},{endGridPos.Y},Z={end.Z:F2}) -> 节点({endInfo.X},{endInfo.Y},层{endInfo.LayerIndex},Z={endInfo.Z:F2})"); + LogManager.Info($"[A*执行-3D] 起点映射: ({startGridPos.X},{startGridPos.Y},高程={startElevation:F2}) -> 节点({startInfo.X},{startInfo.Y},层{startInfo.LayerIndex},高程={startInfo.Z:F2})"); + LogManager.Info($"[A*执行-3D] 终点映射: ({endGridPos.X},{endGridPos.Y},高程={endElevation:F2}) -> 节点({endInfo.X},{endInfo.Y},层{endInfo.LayerIndex},高程={endInfo.Z:F2})"); // 执行A* var pathfinder = new Roy_T.AStar.Paths.PathFinder(); @@ -699,7 +701,7 @@ namespace NavisworksTransport.PathPlanning path3D.Add(originalStart); int pointIndex = 0; - LogManager.Debug($"[路径点{pointIndex}] 原始起点: ({originalStart.X:F2}, {originalStart.Y:F2}, Z={originalStart.Z:F2})"); + LogManager.Debug($"[路径点{pointIndex}] 原始起点: ({originalStart.X:F2}, {originalStart.Y:F2}, {originalStart.Z:F2}), 高程={gridMap.GetWorldElevation(originalStart):F2}"); pointIndex++; // 添加第一条边的起点(如果与originalStart不同) @@ -710,14 +712,13 @@ namespace NavisworksTransport.PathPlanning if (firstNode != null && graph3D.NodeInfo.ContainsKey(firstNode)) { var firstInfo = graph3D.NodeInfo[firstNode]; - var firstPos = gridMap.GridToWorld2D(new GridPoint2D(firstInfo.X, firstInfo.Y)); - var firstPoint = new Point3D(firstPos.X, firstPos.Y, firstInfo.Z); + var firstPoint = gridMap.CreateWorldPoint(new GridPoint2D(firstInfo.X, firstInfo.Y), firstInfo.Z); // 如果与起点高度不同,添加这个节点(避免直接跳跃) - if (Math.Abs(firstPoint.Z - originalStart.Z) > 0.01) + if (Math.Abs(gridMap.GetWorldElevation(firstPoint) - gridMap.GetWorldElevation(originalStart)) > 0.01) { path3D.Add(firstPoint); - LogManager.Debug($"[路径点{pointIndex}] A*起点: 网格({firstInfo.X},{firstInfo.Y}), 层{firstInfo.LayerIndex}, Z={firstInfo.Z:F2}, 类型={firstInfo.CellType}"); + LogManager.Debug($"[路径点{pointIndex}] A*起点: 网格({firstInfo.X},{firstInfo.Y}), 层{firstInfo.LayerIndex}, 高程={firstInfo.Z:F2}, 类型={firstInfo.CellType}"); pointIndex++; } } @@ -731,10 +732,9 @@ namespace NavisworksTransport.PathPlanning continue; var nodeInfo = graph3D.NodeInfo[endNode]; - var worldPos = gridMap.GridToWorld2D(new GridPoint2D(nodeInfo.X, nodeInfo.Y)); - path3D.Add(new Point3D(worldPos.X, worldPos.Y, nodeInfo.Z)); + path3D.Add(gridMap.CreateWorldPoint(new GridPoint2D(nodeInfo.X, nodeInfo.Y), nodeInfo.Z)); - LogManager.Debug($"[路径点{pointIndex}] 网格({nodeInfo.X},{nodeInfo.Y}), 层{nodeInfo.LayerIndex}, Z={nodeInfo.Z:F2}, 类型={nodeInfo.CellType}"); + LogManager.Debug($"[路径点{pointIndex}] 网格({nodeInfo.X},{nodeInfo.Y}), 层{nodeInfo.LayerIndex}, 高程={nodeInfo.Z:F2}, 类型={nodeInfo.CellType}"); pointIndex++; } @@ -829,10 +829,10 @@ namespace NavisworksTransport.PathPlanning // 初始化激活层字典 var activeLayerZ = new Dictionary(); - double targetZ = endPos.Z; // 使用终点Z作为目标高度 + double targetZ = gridMap.GetWorldElevation(endPos); // 使用宿主坐标语义下的终点高程作为目标高度 double baseSpeed = 5.0; // km/h - LogManager.Info($"[A*转换-2.5D] 目标高度: {targetZ:F2}m,基础速度: {baseSpeed}km/h"); + LogManager.Info($"[A*转换-2.5D] 目标高程: {targetZ:F2}m,基础速度: {baseSpeed}km/h"); // 输出详细的网格统计信息 LogManager.Info($"[A*转换-2.5D] 输入网格统计信息:\n{gridMap.GetStatistics()}"); @@ -1083,7 +1083,7 @@ namespace NavisworksTransport.PathPlanning // 初始化激活高度层字典和目标高度 var activeLayerZ = new Dictionary(); - double targetZ = endPos.Z; + double targetZ = gridMap.GetWorldElevation(endPos); // 1. 创建网格基础结构 double cellSizeInMeters = UnitsConverter.ConvertToMeters(gridMap.CellSize); @@ -1564,19 +1564,19 @@ namespace NavisworksTransport.PathPlanning if (endCell.HasValue) { // 调试:检查门网格的Z坐标 - double endZ = endCell.Value.HeightLayers != null && endCell.Value.HeightLayers.Count > 0 + double endElevation = endCell.Value.HeightLayers != null && endCell.Value.HeightLayers.Count > 0 ? endCell.Value.HeightLayers[0].Z : 0; if (endCell.Value.CellType == "门") { - LogManager.Info($"[路径转换-门] 门网格({endGridPos.X},{endGridPos.Y}) HeightLayer[0].Z={endZ:F3}, 位置({endWorldPos.X:F2},{endWorldPos.Y:F2})"); + LogManager.Info($"[路径转换-门] 门网格({endGridPos.X},{endGridPos.Y}) HeightLayer[0].高程={endElevation:F3}, 位置({endWorldPos.X:F2},{endWorldPos.Y:F2})"); } - endWorldPos = new Point3D(endWorldPos.X, endWorldPos.Y, endZ); + endWorldPos = gridMap.SetWorldElevation(endWorldPos, endElevation); } else { - LogManager.Warning($"[路径转换] 网格({endGridPos.X},{endGridPos.Y})获取失败,位置({endWorldPos.X:F2},{endWorldPos.Y:F2}),Z坐标保持为0"); + LogManager.Warning($"[路径转换] 网格({endGridPos.X},{endGridPos.Y})获取失败,位置({endWorldPos.X:F2},{endWorldPos.Y:F2}),高程保持为默认值"); } // 检查是否与上一个点重复(A*可能在转弯点有重复) @@ -1632,8 +1632,8 @@ namespace NavisworksTransport.PathPlanning bool canSkip = false; double maxAllowedHeightDiff = ConfigManager.Instance.Current.PathEditing.MaxHeightDiff; // 从配置文件读取高度差阈值(模型单位) - double startZ = startPoint.Z; - double endZ = endPoint.Z; + double startZ = gridMap.GetWorldElevation(startPoint); + double endZ = gridMap.GetWorldElevation(endPoint); double totalHeightDiff = Math.Abs(endZ - startZ); // 检查1:起终点高度差必须在阈值内 @@ -1645,7 +1645,7 @@ namespace NavisworksTransport.PathPlanning for (int midIndex = currentIndex + 1; midIndex < testIndex; midIndex++) { - double midZ = optimizedPath[midIndex].Z; + double midZ = gridMap.GetWorldElevation(optimizedPath[midIndex]); // 如果中间点高度与起点或终点不同,说明有高度变化,不能优化掉 if (Math.Abs(midZ - startZ) > heightTolerance || Math.Abs(midZ - endZ) > heightTolerance) @@ -1746,18 +1746,23 @@ namespace NavisworksTransport.PathPlanning { // 线性插值计算采样点 double t = samples > 0 ? (double)i / samples : 0; - var samplePoint = new Point3D( - start.X + t * (end.X - start.X), - start.Y + t * (end.Y - start.Y), - start.Z + t * (end.Z - start.Z) - ); + double interpolatedElevation = + gridMap.GetWorldElevation(start) + + t * (gridMap.GetWorldElevation(end) - gridMap.GetWorldElevation(start)); + + var samplePoint = gridMap.SetWorldElevation( + new Point3D( + start.X + t * (end.X - start.X), + start.Y + t * (end.Y - start.Y), + start.Z + t * (end.Z - start.Z)), + interpolatedElevation); // 🔥 修复:检查采样点的具体Z高度是否在可通行层范围内 // 使用IsPassableAt3DPoint而不是IsWalkable,确保不穿过障碍物层 var gridPos = gridMap.WorldToGrid(samplePoint); double tolerance = 0.1; // Z坐标容差(模型单位) - if (!gridMap.IsValidGridPosition(gridPos) || !gridMap.IsPassableAt3DPoint(gridPos, samplePoint.Z, tolerance)) + if (!gridMap.IsValidGridPosition(gridPos) || !gridMap.IsPassableAtElevation(gridPos, gridMap.GetWorldElevation(samplePoint), tolerance)) { // 🔥 修复:使用Origin计算网格左下角,而不是Bounds.Min var gridMinX = gridMap.Origin.X + gridPos.X * gridMap.CellSize; @@ -1766,7 +1771,7 @@ namespace NavisworksTransport.PathPlanning // 计算网格中心点世界坐标(基于左下角) var gridCenterX = gridMinX + 0.5 * gridMap.CellSize; var gridCenterY = gridMinY + 0.5 * gridMap.CellSize; - var gridCenterZ = samplePoint.Z; // Z保持不变 + var gridCenterZ = gridMap.GetWorldElevation(samplePoint); // 计算偏差 var deltaX = samplePoint.X - gridCenterX; @@ -2015,7 +2020,7 @@ namespace NavisworksTransport.PathPlanning /// 原始起点坐标 /// 原始终点坐标 /// 修正后的路径 - private List CorrectStartEndPoints(List path, Point3D originalStart, Point3D originalEnd) + private List CorrectStartEndPoints(List path, Point3D originalStart, Point3D originalEnd, GridMap gridMap) { if (path == null || path.Count == 0) return path; @@ -2025,21 +2030,23 @@ namespace NavisworksTransport.PathPlanning LogManager.Info($"[坐标修正] 开始修正路径起终点坐标"); var correctedPath = new List(path); - // 修正起点:保持原始XY坐标,使用路径的Z坐标 + // 修正起点:保持原始宿主水平位置,复用路径求得的高程 if (correctedPath.Count > 0) { - var originalStartWithZ = new Point3D(originalStart.X, originalStart.Y, correctedPath[0].Z); - LogManager.Info($"[坐标修正] 起点: ({correctedPath[0].X:F3}, {correctedPath[0].Y:F3}, {correctedPath[0].Z:F3}) -> ({originalStartWithZ.X:F3}, {originalStartWithZ.Y:F3}, {originalStartWithZ.Z:F3})"); - correctedPath[0] = originalStartWithZ; + double startElevation = gridMap.GetWorldElevation(correctedPath[0]); + var correctedStart = gridMap.SetWorldElevation(originalStart, startElevation); + LogManager.Info($"[坐标修正] 起点: ({correctedPath[0].X:F3}, {correctedPath[0].Y:F3}, {correctedPath[0].Z:F3}) -> ({correctedStart.X:F3}, {correctedStart.Y:F3}, {correctedStart.Z:F3}), 高程={startElevation:F3}"); + correctedPath[0] = correctedStart; } - // 修正终点:保持原始XY坐标,使用路径的Z坐标 + // 修正终点:保持原始宿主水平位置,复用路径求得的高程 if (correctedPath.Count > 1) { var lastIndex = correctedPath.Count - 1; - var originalEndWithZ = new Point3D(originalEnd.X, originalEnd.Y, correctedPath[lastIndex].Z); - LogManager.Info($"[坐标修正] 终点: ({correctedPath[lastIndex].X:F3}, {correctedPath[lastIndex].Y:F3}, {correctedPath[lastIndex].Z:F3}) -> ({originalEndWithZ.X:F3}, {originalEndWithZ.Y:F3}, {originalEndWithZ.Z:F3})"); - correctedPath[lastIndex] = originalEndWithZ; + double endElevation = gridMap.GetWorldElevation(correctedPath[lastIndex]); + var correctedEnd = gridMap.SetWorldElevation(originalEnd, endElevation); + LogManager.Info($"[坐标修正] 终点: ({correctedPath[lastIndex].X:F3}, {correctedPath[lastIndex].Y:F3}, {correctedPath[lastIndex].Z:F3}) -> ({correctedEnd.X:F3}, {correctedEnd.Y:F3}, {correctedEnd.Z:F3}), 高程={endElevation:F3}"); + correctedPath[lastIndex] = correctedEnd; } LogManager.Info($"[坐标修正] 路径起终点坐标修正完成"); @@ -2087,7 +2094,7 @@ namespace NavisworksTransport.PathPlanning if (cell.HeightLayers != null && cell.HeightLayers.Count > 0) { // 查找包含指定Z坐标且满足物体高度的层 - var matchingLayer = gridMap.FindLayerContainingZ(gridPos, point.Z, tolerance: 0.5); + var matchingLayer = gridMap.FindLayerContainingElevation(gridPos, gridMap.GetWorldElevation(point), tolerance: 0.5); if (matchingLayer.HasValue) { @@ -2098,12 +2105,12 @@ namespace NavisworksTransport.PathPlanning } else { - LogManager.Warning($"[高度检查] ❌ 找到匹配Z={point.Z:F2}的层Z={matchingLayer.Value.Z:F2},但高度不足:物体高度{objectHeight:F2},层高度跨度{matchingLayer.Value.PassableHeight.GetSpan():F2}"); + LogManager.Warning($"[高度检查] ❌ 找到匹配高程={gridMap.GetWorldElevation(point):F2}的层高程={matchingLayer.Value.Z:F2},但高度不足:物体高度{objectHeight:F2},层高度跨度{matchingLayer.Value.PassableHeight.GetSpan():F2}"); } } else { - LogManager.Warning($"[高度检查] ❌ 单元格({gridX}, {gridY})有{cell.HeightLayers.Count}个高度层,但无法找到包含Z={point.Z:F2}的层"); + LogManager.Warning($"[高度检查] ❌ 单元格({gridX}, {gridY})有{cell.HeightLayers.Count}个高度层,但无法找到包含高程={gridMap.GetWorldElevation(point):F2}的层"); } return false; } @@ -2136,17 +2143,17 @@ namespace NavisworksTransport.PathPlanning var adjustedPath = new List(); - // 1. 使用用户指定的原始起点和终点Z坐标(而不是从转换后的路径中提取) - double startZ = originalStart.Z; - double endZ = originalEnd.Z; + // 1. 使用用户指定的原始起点和终点高程(而不是从转换后的路径中提取) + double startZ = gridMap.GetWorldElevation(originalStart); + double endZ = gridMap.GetWorldElevation(originalEnd); // 对比日志:显示原始坐标与路径坐标的差异 - var pathStartZ = pathWithGridCoords[0].point.Z; - var pathEndZ = pathWithGridCoords[pathWithGridCoords.Count - 1].point.Z; + var pathStartZ = gridMap.GetWorldElevation(pathWithGridCoords[0].point); + var pathEndZ = gridMap.GetWorldElevation(pathWithGridCoords[pathWithGridCoords.Count - 1].point); - LogManager.Info($"[智能选层] 原始起点Z={startZ:F3}, 原始终点Z={endZ:F3}"); - LogManager.Info($"[智能选层] 路径起点Z={pathStartZ:F3}, 路径终点Z={pathEndZ:F3}"); - LogManager.Info($"[智能选层] 使用原始Z坐标进行智能选层,高度变化={endZ - startZ:F3}"); + LogManager.Info($"[智能选层] 原始起点高程={startZ:F3}, 原始终点高程={endZ:F3}"); + LogManager.Info($"[智能选层] 路径起点高程={pathStartZ:F3}, 路径终点高程={pathEndZ:F3}"); + LogManager.Info($"[智能选层] 使用原始高程进行智能选层,高程变化={endZ - startZ:F3}"); // 2. 为每个路径点选择高度层 for (int i = 0; i < pathWithGridCoords.Count; i++) @@ -2175,8 +2182,8 @@ namespace NavisworksTransport.PathPlanning var selectedLayer = SelectBestLayer(cell.HeightLayers, startZ, objectHeight, startZ, endZ, i, pathWithGridCoords.Count, true); if (selectedLayer.HasValue) { - adjustedPath.Add(new Point3D(point.X, point.Y, selectedLayer.Value.Z)); - LogManager.Debug($"[智能选层] 起点({gridPos.X},{gridPos.Y}) Z={selectedLayer.Value.Z:F3}"); + adjustedPath.Add(gridMap.SetWorldElevation(point, selectedLayer.Value.Z)); + LogManager.Debug($"[智能选层] 起点({gridPos.X},{gridPos.Y}) 高程={selectedLayer.Value.Z:F3}"); } else { @@ -2188,8 +2195,8 @@ namespace NavisworksTransport.PathPlanning var selectedLayer = SelectBestLayer(cell.HeightLayers, endZ, objectHeight, startZ, endZ, i, pathWithGridCoords.Count, true); if (selectedLayer.HasValue) { - adjustedPath.Add(new Point3D(point.X, point.Y, selectedLayer.Value.Z)); - LogManager.Debug($"[智能选层] 终点({gridPos.X},{gridPos.Y}) Z={selectedLayer.Value.Z:F3}"); + adjustedPath.Add(gridMap.SetWorldElevation(point, selectedLayer.Value.Z)); + LogManager.Debug($"[智能选层] 终点({gridPos.X},{gridPos.Y}) 高程={selectedLayer.Value.Z:F3}"); } else { @@ -2199,12 +2206,12 @@ namespace NavisworksTransport.PathPlanning else { // 中间点:根据高度趋势选择最佳层 - var prevZ = adjustedPath[i - 1].Z; + var prevZ = gridMap.GetWorldElevation(adjustedPath[i - 1]); var selectedLayer = SelectBestLayer(cell.HeightLayers, prevZ, objectHeight, startZ, endZ, i, pathWithGridCoords.Count, false); if (selectedLayer.HasValue) { - adjustedPath.Add(new Point3D(point.X, point.Y, selectedLayer.Value.Z)); + adjustedPath.Add(gridMap.SetWorldElevation(point, selectedLayer.Value.Z)); } else { @@ -2243,36 +2250,34 @@ namespace NavisworksTransport.PathPlanning for (int i = 0; i < gridPath.Count; i++) { var gridPos = gridPath[i]; - var worldXY = gridMap.GridToWorld3D(gridPos); - - double z; + double elevation; // 起点:使用原始起点Z if (i == 0) { - z = originalStart.Z; - LogManager.Info($"[3D还原] 起点 ({gridPos.X},{gridPos.Y}) Z={z:F2}m (原始起点)"); + elevation = gridMap.GetWorldElevation(originalStart); + LogManager.Info($"[3D还原] 起点 ({gridPos.X},{gridPos.Y}) 高程={elevation:F2}m (原始起点)"); } // 终点:使用原始终点Z else if (i == gridPath.Count - 1) { - z = originalEnd.Z; - LogManager.Info($"[3D还原] 终点 ({gridPos.X},{gridPos.Y}) Z={z:F2}m (原始终点)"); + elevation = gridMap.GetWorldElevation(originalEnd); + LogManager.Info($"[3D还原] 终点 ({gridPos.X},{gridPos.Y}) 高程={elevation:F2}m (原始终点)"); } // 中间点:使用A*阶段记录的激活层 else if (activeLayerZ.TryGetValue(gridPos, out double activeZ)) { - z = activeZ; - LogManager.Debug($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) Z={z:F2}m (激活层)"); + elevation = activeZ; + LogManager.Debug($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) 高程={elevation:F2}m (激活层)"); } else { // 降级:无激活层记录,使用前一点高度 - z = path3D[i - 1].Z; - LogManager.Warning($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) 无激活层记录,使用前点Z={z:F2}m"); + elevation = gridMap.GetWorldElevation(path3D[i - 1]); + LogManager.Warning($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) 无激活层记录,使用前点高程={elevation:F2}m"); } - path3D.Add(new Point3D(worldXY.X, worldXY.Y, z)); + path3D.Add(gridMap.CreateWorldPoint(gridPos, elevation)); } LogManager.Info($"[3D还原] 完成,生成了 {path3D.Count} 个3D路径点"); @@ -2627,7 +2632,7 @@ namespace NavisworksTransport.PathPlanning // 初始化激活高度层字典和目标高度 var activeLayerZ = new Dictionary(); - double targetZ = endPos.Z; + double targetZ = gridMap.GetWorldElevation(endPos); // 清空日志记录集合,确保每次执行都能看到映射关系 _loggedDistances.Clear(); diff --git a/src/PathPlanning/GridMap.cs b/src/PathPlanning/GridMap.cs index 458f317..3d1bc85 100644 --- a/src/PathPlanning/GridMap.cs +++ b/src/PathPlanning/GridMap.cs @@ -137,6 +137,36 @@ namespace NavisworksTransport.PathPlanning LogManager.Info($"[网格地图] 创建完成: {Width}x{Height}, 坐标系: {_coordinateSystem.Type}"); } + /// + /// 轻量构造函数,主要用于单元测试与脱离 Navisworks 原生边界句柄的纯坐标语义验证。 + /// + public GridMap(Point3D origin, int width, int height, double cellSize, ICoordinateSystem coordinateSystem) + { + if (origin == null) + throw new ArgumentNullException(nameof(origin)); + if (width <= 0) + throw new ArgumentException("网格宽度必须大于0", nameof(width)); + if (height <= 0) + throw new ArgumentException("网格高度必须大于0", nameof(height)); + if (cellSize <= 0) + throw new ArgumentException("网格单元格大小必须大于0", nameof(cellSize)); + if (coordinateSystem == null) + throw new ArgumentNullException(nameof(coordinateSystem)); + + Origin = origin; + Width = width; + Height = height; + CellSize = cellSize; + _coordinateSystem = coordinateSystem; + Bounds = null; + + Cells = new GridCell[Width, Height]; + InitializeCells(); + InitializeHeightCalculation(); + + LogManager.Info($"[网格地图] 轻量创建完成: {Width}x{Height}, 坐标系: {_coordinateSystem.Type}"); + } + /// /// 初始化高度计算组件 /// @@ -230,6 +260,44 @@ namespace NavisworksTransport.PathPlanning return _coordinateSystem.SetElevation(world2D, elevation); } + /// + /// 获取宿主坐标点在当前网格坐标语义下的高程值。 + /// 注意: + /// - 对 ZUp 文档,高程是 point.Z + /// - 对 YUp 文档,高程是 point.Y + /// + public double GetWorldElevation(Point3D worldPoint) + { + if (worldPoint == null) + { + throw new ArgumentNullException(nameof(worldPoint)); + } + + return _coordinateSystem.GetElevation(worldPoint); + } + + /// + /// 在当前网格坐标语义下,为一个宿主点设置高程。 + /// + public Point3D SetWorldElevation(Point3D worldPoint, double elevation) + { + if (worldPoint == null) + { + throw new ArgumentNullException(nameof(worldPoint)); + } + + return _coordinateSystem.SetElevation(worldPoint, elevation); + } + + /// + /// 根据网格坐标和指定高程创建宿主坐标点。 + /// + public Point3D CreateWorldPoint(GridPoint2D gridPosition, double elevation) + { + Point3D world2D = GridToWorld2D(gridPosition); + return _coordinateSystem.SetElevation(world2D, elevation); + } + /// @@ -344,7 +412,7 @@ namespace NavisworksTransport.PathPlanning /// Z坐标(米) /// 容差(米),默认0.1m /// 符合条件的高度层,如果没有则返回null - public HeightLayer? FindLayerContainingZ(GridPoint2D gridPosition, double z, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS) + public HeightLayer? FindLayerContainingElevation(GridPoint2D gridPosition, double elevation, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS) { if (!IsValidGridPosition(gridPosition)) return null; @@ -359,7 +427,7 @@ namespace NavisworksTransport.PathPlanning foreach (var layer in cell.HeightLayers) { - double distance = Math.Abs(layer.Z - z); + double distance = Math.Abs(layer.Z - elevation); if (distance < minDistance && distance <= tolerance) { minDistance = distance; @@ -370,6 +438,11 @@ namespace NavisworksTransport.PathPlanning return bestLayer; } + public HeightLayer? FindLayerContainingZ(GridPoint2D gridPosition, double z, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS) + { + return FindLayerContainingElevation(gridPosition, z, tolerance); + } + /// /// 获取指定网格位置的所有高度层 /// @@ -441,7 +514,7 @@ namespace NavisworksTransport.PathPlanning /// Z坐标(模型单位) /// Z坐标容差(模型单位),默认0.1 /// 是否可通行 - public bool IsPassableAt3DPoint(GridPoint2D gridPosition, double zCoord, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS) + public bool IsPassableAtElevation(GridPoint2D gridPosition, double elevation, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS) { if (!IsValidGridPosition(gridPosition)) return false; @@ -463,7 +536,7 @@ namespace NavisworksTransport.PathPlanning double layerMinZ = layer.Z; double layerMaxZ = layer.Z + layer.PassableHeight.GetSpan(); - if (zCoord >= layerMinZ - tolerance && zCoord <= layerMaxZ + tolerance) + if (elevation >= layerMinZ - tolerance && elevation <= layerMaxZ + tolerance) { return true; } @@ -473,6 +546,11 @@ namespace NavisworksTransport.PathPlanning return false; } + public bool IsPassableAt3DPoint(GridPoint2D gridPosition, double zCoord, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS) + { + return IsPassableAtElevation(gridPosition, zCoord, tolerance); + } + /// /// 获取所有通道类型的网格单元 /// diff --git a/src/PathPlanning/PathOptimizer.cs b/src/PathPlanning/PathOptimizer.cs index e5cd381..4df1a70 100644 --- a/src/PathPlanning/PathOptimizer.cs +++ b/src/PathPlanning/PathOptimizer.cs @@ -118,7 +118,7 @@ namespace NavisworksTransport.PathPlanning } else { - optimizedPoints = SimplifyCollinearPoints(optimizedPoints); + optimizedPoints = SimplifyCollinearPoints(optimizedPoints, gridMap); LogManager.Info($"[路径优化] 简化完成,点数:{originalCount} -> {optimizedPoints.Count}"); } } @@ -170,7 +170,7 @@ namespace NavisworksTransport.PathPlanning // 3D场景:保留同网格不同高度的点 bool hasGridChange = i == 0 || !gridPath[i].Equals(gridPath[i-1]); bool hasSpeedChange = i > 0 && HasSpeedLimitChange(points[i-1], points[i]); - bool hasHeightChange = i > 0 && Math.Abs(points[i].Position.Z - points[i-1].Position.Z) > 1e-6; + bool hasHeightChange = i > 0 && HasHeightChange(points[i], points[i - 1], gridMap); bool shouldKeep = hasGridChange || hasSpeedChange || hasHeightChange; @@ -213,7 +213,8 @@ namespace NavisworksTransport.PathPlanning bool hasHeightChange = HasHeightChange( dedupedPoints[i-2], dedupedPoints[i-1], - dedupedPoints[i] + dedupedPoints[i], + gridMap ); // 检查限速变化 @@ -264,7 +265,7 @@ namespace NavisworksTransport.PathPlanning /// /// 原始路径点列表 /// 简化后的路径点列表 - private List SimplifyCollinearPoints(List points) + private List SimplifyCollinearPoints(List points, GridMap gridMap = null) { if (points.Count <= 2) return points; @@ -281,7 +282,7 @@ namespace NavisworksTransport.PathPlanning var nextPoint = points[i + 1]; // 检查是否在同一直线上 - bool isCollinear = IsCollinear(prevPoint.Position, currPoint.Position, nextPoint.Position); + bool isCollinear = IsCollinear(prevPoint.Position, currPoint.Position, nextPoint.Position, gridMap); // 🔥 关键修复:检查限速变化,即使共线也不能移除限速边界点 bool hasSpeedLimitChange = HasSpeedLimitChange(prevPoint, currPoint, nextPoint); @@ -316,10 +317,12 @@ namespace NavisworksTransport.PathPlanning /// 第一个路径点 /// 第二个路径点 /// 如果有高度变化返回true,需要保留中间点 - private bool HasHeightChange(PathPoint point1, PathPoint point2) + private bool HasHeightChange(PathPoint point1, PathPoint point2, GridMap gridMap = null) { const double heightTolerance = 1e-6; // 仅覆盖浮点数精度误差 - return Math.Abs(point1.Position.Z - point2.Position.Z) > heightTolerance; + double elevation1 = GetElevation(point1.Position, gridMap); + double elevation2 = GetElevation(point2.Position, gridMap); + return Math.Abs(elevation1 - elevation2) > heightTolerance; } /// @@ -329,13 +332,17 @@ namespace NavisworksTransport.PathPlanning /// 中间点的3D坐标 /// 第三个点的3D坐标 /// 如果有高度变化返回true,不能跳过中间点 - private bool HasHeightChange(Point3D p1, Point3D p2, Point3D p3) + private bool HasHeightChange(Point3D p1, Point3D p2, Point3D p3, GridMap gridMap = null) { const double heightTolerance = 1e-6; // 仅覆盖浮点数精度误差 // 检查任意两点间是否有高度变化 - if (Math.Abs(p1.Z - p2.Z) > heightTolerance || - Math.Abs(p2.Z - p3.Z) > heightTolerance) + double elevation1 = GetElevation(p1, gridMap); + double elevation2 = GetElevation(p2, gridMap); + double elevation3 = GetElevation(p3, gridMap); + + if (Math.Abs(elevation1 - elevation2) > heightTolerance || + Math.Abs(elevation2 - elevation3) > heightTolerance) { return true; } @@ -349,9 +356,9 @@ namespace NavisworksTransport.PathPlanning /// 中间路径点 /// 第三个路径点 /// 如果有高度变化返回true,不能跳过中间点 - private bool HasHeightChange(PathPoint point1, PathPoint point2, PathPoint point3) + private bool HasHeightChange(PathPoint point1, PathPoint point2, PathPoint point3, GridMap gridMap = null) { - return HasHeightChange(point1.Position, point2.Position, point3.Position); + return HasHeightChange(point1.Position, point2.Position, point3.Position, gridMap); } /// @@ -395,25 +402,41 @@ namespace NavisworksTransport.PathPlanning /// 第二个点(中间点) /// 第三个点 /// 如果三点在同一直线上且方向一致返回true - private bool IsCollinear(Point3D p1, Point3D p2, Point3D p3) + private bool IsCollinear(Point3D p1, Point3D p2, Point3D p3, GridMap gridMap = null) { const double tolerance = COLLINEAR_TOLERANCE_METERS; // 容差值,单位:模型单位 // 🔥 修复:使用统一的高度检查函数 - if (HasHeightChange(p1, p2, p3)) + if (HasHeightChange(p1, p2, p3, gridMap)) { LogManager.Debug($"[共线检测] ❌ 拒绝Z方向变化:({p1.X:F3},{p1.Y:F3},{p1.Z:F3}) -> ({p2.X:F3},{p2.Y:F3},{p2.Z:F3}) -> ({p3.X:F3},{p3.Y:F3},{p3.Z:F3})"); return false; } - // 🔥 关键修复:分别检查两个线段的方向 - // 线段1: p1 -> p2 - double dx12 = p2.X - p1.X; - double dy12 = p2.Y - p1.Y; - - // 线段2: p2 -> p3 - double dx23 = p3.X - p2.X; - double dy23 = p3.Y - p2.Y; + double dx12; + double dy12; + double dx23; + double dy23; + + if (gridMap != null) + { + var g1 = gridMap.WorldToGrid(p1); + var g2 = gridMap.WorldToGrid(p2); + var g3 = gridMap.WorldToGrid(p3); + + dx12 = g2.X - g1.X; + dy12 = g2.Y - g1.Y; + dx23 = g3.X - g2.X; + dy23 = g3.Y - g2.Y; + } + else + { + // 回退到历史 ZUp 语义,仅用于未提供网格地图的旧调用链。 + dx12 = p2.X - p1.X; + dy12 = p2.Y - p1.Y; + dx23 = p3.X - p2.X; + dy23 = p3.Y - p2.Y; + } // 🔥 修复:使用更严格的容差检查真正的重复点(坐标完全相同) const double duplicatePointTolerance = DUPLICATE_POINT_TOLERANCE_METERS; // 用很小的值检查真正的重复点 @@ -453,6 +476,11 @@ namespace NavisworksTransport.PathPlanning } } + private static double GetElevation(Point3D point, GridMap gridMap) + { + return gridMap?.GetWorldElevation(point) ?? point.Z; + } + /// /// 创建优化后的路径对象 /// @@ -521,4 +549,4 @@ namespace NavisworksTransport.PathPlanning #endregion } -} \ No newline at end of file +} diff --git a/src/Utils/CoordinateSystem/HostPlanarGridHelper.cs b/src/Utils/CoordinateSystem/HostPlanarGridHelper.cs new file mode 100644 index 0000000..e6dd2bc --- /dev/null +++ b/src/Utils/CoordinateSystem/HostPlanarGridHelper.cs @@ -0,0 +1,99 @@ +using System; +using System.Numerics; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + /// + /// 自动路径规划 / 网格系统使用的宿主平面与高程语义辅助。 + /// + /// 关键约束: + /// - 对外继续维持 ICoordinateSystem 的历史契约: + /// - ZUp: 水平面 = Host XY,高程 = Host Z + /// - YUp: 水平面 = Host XZ,高程 = Host Y + /// - 对内通过 HostCoordinateAdapter 统一解释“哪根轴是宿主 up”。 + /// + /// 这样自动路径规划既不会继续把世界 Z 硬编码成高度轴, + /// 也不会把旧的水平坐标 h1/h2 语义误替换成 raw canonical XY。 + /// + public static class HostPlanarGridHelper + { + public static double GetElevation3(Vector3 hostPoint, HostCoordinateAdapter adapter) + { + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + return adapter.ToCanonicalPoint3(hostPoint).Z; + } + + public static Vector3 SetElevation3(Vector3 hostPoint, double elevation, HostCoordinateAdapter adapter) + { + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint); + Vector3 canonicalResult = new Vector3(canonicalPoint.X, canonicalPoint.Y, (float)elevation); + return adapter.FromCanonicalPoint3(canonicalResult); + } + + public static (double h1, double h2) GetHorizontalCoords3(Vector3 hostPoint, HostCoordinateAdapter adapter) + { + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + switch (adapter.HostType) + { + case CoordinateSystemType.ZUp: + return (hostPoint.X, hostPoint.Y); + case CoordinateSystemType.YUp: + return (hostPoint.X, hostPoint.Z); + default: + throw new InvalidOperationException($"不支持的宿主坐标系: {adapter.HostType}"); + } + } + + public static Vector3 CreateHostPoint3(double h1, double h2, double elevation, HostCoordinateAdapter adapter) + { + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + switch (adapter.HostType) + { + case CoordinateSystemType.ZUp: + return new Vector3((float)h1, (float)h2, (float)elevation); + case CoordinateSystemType.YUp: + return new Vector3((float)h1, (float)elevation, (float)h2); + default: + throw new InvalidOperationException($"不支持的宿主坐标系: {adapter.HostType}"); + } + } + + public static (double min1, double max1, double min2, double max2) GetHorizontalRange3( + Vector3 hostMin, + Vector3 hostMax, + HostCoordinateAdapter adapter) + { + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + switch (adapter.HostType) + { + case CoordinateSystemType.ZUp: + return (hostMin.X, hostMax.X, hostMin.Y, hostMax.Y); + case CoordinateSystemType.YUp: + return (hostMin.X, hostMax.X, hostMin.Z, hostMax.Z); + default: + throw new InvalidOperationException($"不支持的宿主坐标系: {adapter.HostType}"); + } + } + } +} diff --git a/src/Utils/CoordinateSystem/YUpCoordinateSystem.cs b/src/Utils/CoordinateSystem/YUpCoordinateSystem.cs index dee8577..c55362b 100644 --- a/src/Utils/CoordinateSystem/YUpCoordinateSystem.cs +++ b/src/Utils/CoordinateSystem/YUpCoordinateSystem.cs @@ -1,4 +1,5 @@ using Autodesk.Navisworks.Api; +using System.Numerics; namespace NavisworksTransport.Utils.CoordinateSystem { @@ -8,6 +9,8 @@ namespace NavisworksTransport.Utils.CoordinateSystem /// public class YUpCoordinateSystem : ICoordinateSystem { + private static readonly HostCoordinateAdapter Adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + public CoordinateSystemType Type => CoordinateSystemType.YUp; public Vector3D UpVector => new Vector3D(0, 1, 0); @@ -18,22 +21,22 @@ namespace NavisworksTransport.Utils.CoordinateSystem public double GetElevation(Point3D point) { - return point.Y; + return HostPlanarGridHelper.GetElevation3(ToVector3(point), Adapter); } public Point3D SetElevation(Point3D point, double elevation) { - return new Point3D(point.X, elevation, point.Z); + return ToPoint3D(HostPlanarGridHelper.SetElevation3(ToVector3(point), elevation, Adapter)); } public (double h1, double h2) GetHorizontalCoords(Point3D point) { - return (point.X, point.Z); + return HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(point), Adapter); } public Point3D CreatePoint(double h1, double h2, double elevation) { - return new Point3D(h1, elevation, h2); + return ToPoint3D(HostPlanarGridHelper.CreateHostPoint3(h1, h2, elevation, Adapter)); } public (double min, double max) GetHeightRange(BoundingBox3D bounds) @@ -43,12 +46,22 @@ namespace NavisworksTransport.Utils.CoordinateSystem public (double min1, double max1, double min2, double max2) GetHorizontalRange(BoundingBox3D bounds) { - return (bounds.Min.X, bounds.Max.X, bounds.Min.Z, bounds.Max.Z); + return HostPlanarGridHelper.GetHorizontalRange3(ToVector3(bounds.Min), ToVector3(bounds.Max), Adapter); } public override string ToString() { return "Y-Up 坐标系 (Y轴向上)"; } + + private static Vector3 ToVector3(Point3D point) + { + return new Vector3((float)point.X, (float)point.Y, (float)point.Z); + } + + private static Point3D ToPoint3D(Vector3 point) + { + return new Point3D(point.X, point.Y, point.Z); + } } } diff --git a/src/Utils/CoordinateSystem/ZUpCoordinateSystem.cs b/src/Utils/CoordinateSystem/ZUpCoordinateSystem.cs index d19ac32..c263633 100644 --- a/src/Utils/CoordinateSystem/ZUpCoordinateSystem.cs +++ b/src/Utils/CoordinateSystem/ZUpCoordinateSystem.cs @@ -1,4 +1,5 @@ using Autodesk.Navisworks.Api; +using System.Numerics; namespace NavisworksTransport.Utils.CoordinateSystem { @@ -8,6 +9,8 @@ namespace NavisworksTransport.Utils.CoordinateSystem /// public class ZUpCoordinateSystem : ICoordinateSystem { + private static readonly HostCoordinateAdapter Adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp); + public CoordinateSystemType Type => CoordinateSystemType.ZUp; public Vector3D UpVector => new Vector3D(0, 0, 1); @@ -18,22 +21,22 @@ namespace NavisworksTransport.Utils.CoordinateSystem public double GetElevation(Point3D point) { - return point.Z; + return HostPlanarGridHelper.GetElevation3(ToVector3(point), Adapter); } public Point3D SetElevation(Point3D point, double elevation) { - return new Point3D(point.X, point.Y, elevation); + return ToPoint3D(HostPlanarGridHelper.SetElevation3(ToVector3(point), elevation, Adapter)); } public (double h1, double h2) GetHorizontalCoords(Point3D point) { - return (point.X, point.Y); + return HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(point), Adapter); } public Point3D CreatePoint(double h1, double h2, double elevation) { - return new Point3D(h1, h2, elevation); + return ToPoint3D(HostPlanarGridHelper.CreateHostPoint3(h1, h2, elevation, Adapter)); } public (double min, double max) GetHeightRange(BoundingBox3D bounds) @@ -43,12 +46,22 @@ namespace NavisworksTransport.Utils.CoordinateSystem public (double min1, double max1, double min2, double max2) GetHorizontalRange(BoundingBox3D bounds) { - return (bounds.Min.X, bounds.Max.X, bounds.Min.Y, bounds.Max.Y); + return HostPlanarGridHelper.GetHorizontalRange3(ToVector3(bounds.Min), ToVector3(bounds.Max), Adapter); } public override string ToString() { return "Z-Up 坐标系 (Z轴向上)"; } + + private static Vector3 ToVector3(Point3D point) + { + return new Vector3((float)point.X, (float)point.Y, (float)point.Z); + } + + private static Point3D ToPoint3D(Vector3 point) + { + return new Point3D(point.X, point.Y, point.Z); + } } }