From 1a9d0972f02bece8df1c1dab65b1a377e6fed653 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Tue, 14 Apr 2026 09:22:01 +0800
Subject: [PATCH] Fix auto path obstacle geometry collection
---
NavisworksTransport.UnitTests.csproj | 1 +
...utoPathPlanningCoordinateSemanticsTests.cs | 243 ++++++++++
.../AutoPathGridGenerationAutomationTests.cs | 40 ++
.../NavisworksTestAutomationClient.cs | 9 +
src/Core/PathPlanningManager.cs | 65 ++-
src/Core/PathPointRenderPlugin.cs | 56 ++-
.../Services/TestAutomationHttpService.cs | 436 ++++++++++++++++++
src/PathPlanning/AutoPathFinder.cs | 15 +-
src/PathPlanning/ChannelBasedGridBuilder.cs | 41 ++
src/PathPlanning/ChannelHeightDetector.cs | 239 ++++++++--
src/PathPlanning/GridMap.cs | 5 +
src/PathPlanning/GridMapGenerator.cs | 405 +++++++++++-----
src/PathPlanning/OptimizedHeightCalculator.cs | 42 +-
src/PathPlanning/SlopeAnalyzer.cs | 88 ++--
src/Utils/ModelItemAnalysisHelper.cs | 17 +-
15 files changed, 1438 insertions(+), 264 deletions(-)
create mode 100644 UnitTests/Integration/AutoPathGridGenerationAutomationTests.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 8962550..ec90fa9 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -61,6 +61,7 @@
+
diff --git a/UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs b/UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs
index d6e1382..3f6495b 100644
--- a/UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs
+++ b/UnitTests/CoordinateSystem/AutoPathPlanningCoordinateSemanticsTests.cs
@@ -1,6 +1,11 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Core;
+using NavisworksTransport.PathPlanning;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
+using System.Reflection;
+using System.Collections.Generic;
+using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
@@ -104,6 +109,244 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(5, gridY);
}
+ [TestMethod]
+ public void YUp_GridMapGenerator_GetObstacleElevationRange_ShouldUseHostYInsteadOfWorldZ()
+ {
+ var generator = new GridMapGenerator();
+
+ var method = typeof(GridMapGenerator).GetMethod(
+ "GetObstacleElevationRange",
+ BindingFlags.Instance | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(CoordinateSystemType)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 GetObstacleElevationRange 私有方法。");
+
+ var elevationRange = ((double min, double max))method.Invoke(generator, new object[]
+ {
+ -200.0, 10.0, -30.0,
+ -198.0, 30.0, -10.0,
+ CoordinateSystemType.YUp
+ });
+
+ Assert.AreEqual(10.0, elevationRange.min, 1e-6, "YUp 下障碍物最小高程应来自宿主 Y。");
+ Assert.AreEqual(30.0, elevationRange.max, 1e-6, "YUp 下障碍物最大高程应来自宿主 Y。");
+ }
+
+ [TestMethod]
+ public void YUp_GridMapGenerator_CalculateBoundingBoxGridCoverage_ShouldProjectToHostXZPlane()
+ {
+ var generator = new GridMapGenerator();
+
+ var method = typeof(GridMapGenerator).GetMethod(
+ "CalculateGridCoverageFromWorldExtents",
+ BindingFlags.Instance | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(int), typeof(int), typeof(double),
+ typeof(CoordinateSystemType)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 CalculateGridCoverageFromWorldExtents 私有方法。");
+
+ var coveredCells = (List<(int x, int y)>)method.Invoke(generator, new object[]
+ {
+ -200.0, 10.0, -30.0,
+ -198.0, 30.0, -10.0,
+ -210.0, 14.83, -80.0,
+ 200, 200, 1.0,
+ CoordinateSystemType.YUp
+ });
+
+ Assert.AreEqual(63, coveredCells.Count, "YUp 下障碍物包围盒应按宿主 XZ 平面覆盖 3x21 个网格。");
+ CollectionAssert.Contains(coveredCells, (10, 50));
+ CollectionAssert.Contains(coveredCells, (12, 70));
+ }
+
+ [TestMethod]
+ public void YUp_GridMapGenerator_CalculateDoorOpeningCoverage_ShouldProjectToHostXZPlane()
+ {
+ var generator = new GridMapGenerator();
+
+ var method = typeof(GridMapGenerator).GetMethod(
+ "CalculateDoorOpeningCoverageFromWorldExtents",
+ BindingFlags.Instance | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(int), typeof(int), typeof(double),
+ typeof(CoordinateSystemType)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 CalculateDoorOpeningCoverageFromWorldExtents 私有方法。");
+
+ var coveredCells = (List<(int x, int y)>)method.Invoke(generator, new object[]
+ {
+ 10.0, 14.0, 20.0,
+ 12.0, 16.0, 30.0,
+ 4.0,
+ 0.0, 0.0, 0.0,
+ 100, 100, 1.0,
+ CoordinateSystemType.YUp
+ });
+
+ Assert.AreEqual(15, coveredCells.Count, "YUp 下门开口应按宿主 XZ 平面覆盖 3x5 个网格。");
+ CollectionAssert.Contains(coveredCells, (10, 23));
+ CollectionAssert.Contains(coveredCells, (12, 27));
+ }
+
+ [TestMethod]
+ public void YUp_PathPlanningManager_CalculateOptimalGridSize_ShouldUseHostXZHorizontalRange()
+ {
+ var method = typeof(PathPlanningManager).GetMethod(
+ "CalculateOptimalGridSizeFromBounds",
+ BindingFlags.Static | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(CoordinateSystemType)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 CalculateOptimalGridSizeFromBounds 私有方法。");
+
+ var gridSize = (double)method.Invoke(null, new object[]
+ {
+ -10.0, 100.0, -300.0,
+ 90.0, 120.0, 400.0,
+ CoordinateSystemType.YUp
+ });
+
+ Assert.AreEqual(2.0, gridSize, 1e-6, "YUp 下网格大小应基于宿主 XZ 平面最大跨度 700 计算。");
+ }
+
+ [TestMethod]
+ public void YUp_ChannelHeightDetector_GetBoundsElevationRange_ShouldUseHostY()
+ {
+ var method = typeof(ChannelHeightDetector).GetMethod(
+ "GetBoundsElevationRange",
+ BindingFlags.Static | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(CoordinateSystemType)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 GetBoundsElevationRange 私有方法。");
+
+ var elevationRange = ((double min, double max))method.Invoke(null, new object[]
+ {
+ -10.0, 14.83, -30.0,
+ 20.0, 18.50, 40.0,
+ CoordinateSystemType.YUp
+ });
+
+ Assert.AreEqual(14.83, elevationRange.min, 1e-6);
+ Assert.AreEqual(18.50, elevationRange.max, 1e-6);
+ }
+
+ [TestMethod]
+ public void YUp_ChannelHeightDetector_CreateHeightProfileSample_ShouldUseHostXZPlaneAndHostYElevation()
+ {
+ var method = typeof(ChannelHeightDetector).GetMethod(
+ "CreateHeightProfileSamplePoint3",
+ BindingFlags.Static | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(double), typeof(double), typeof(double),
+ typeof(CoordinateSystemType),
+ typeof(double)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 CreateHeightProfileSamplePoint3 私有方法。");
+
+ var samplePoint = (Vector3)method.Invoke(null, new object[]
+ {
+ -10.0, 14.83, -30.0,
+ 20.0, 18.50, 40.0,
+ CoordinateSystemType.YUp,
+ 0.5
+ });
+
+ Assert.AreEqual(5.0, samplePoint.X, 1e-6, "YUp 下采样点第一水平轴应沿宿主 X 插值。");
+ Assert.AreEqual(14.83, samplePoint.Y, 1e-6, "YUp 下采样点高程应落在宿主底面 Y。");
+ Assert.AreEqual(5.0, samplePoint.Z, 1e-6, "YUp 下采样点第二水平轴应沿宿主 Z 插值。");
+ }
+
+ [TestMethod]
+ public void YUp_ChannelHeightDetector_GetPickedSurfaceElevation_ShouldUseHostYInsteadOfWorldZ()
+ {
+ var method = typeof(ChannelHeightDetector).GetMethod(
+ "GetPickedSurfaceElevation",
+ BindingFlags.Static | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(CoordinateSystemType)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 GetPickedSurfaceElevation 私有方法。");
+
+ var elevation = (double)method.Invoke(null, new object[]
+ {
+ 11.0, 22.0, 33.0,
+ CoordinateSystemType.YUp
+ });
+
+ Assert.AreEqual(22.0, elevation, 1e-6, "YUp 下拾取表面点的高程应来自宿主 Y。");
+ }
+
+ [TestMethod]
+ public void YUp_GridMapGenerator_ResolveBoundsMinElevation_ShouldUseHostYInsteadOfWorldZ()
+ {
+ var method = typeof(GridMapGenerator).GetMethod(
+ "ResolveBoundsMinElevation",
+ BindingFlags.Static | BindingFlags.NonPublic,
+ null,
+ new[]
+ {
+ typeof(double), typeof(double), typeof(double),
+ typeof(CoordinateSystemType)
+ },
+ null);
+
+ Assert.IsNotNull(method, "未找到 ResolveBoundsMinElevation 私有方法。");
+
+ var elevation = (double)method.Invoke(null, new object[]
+ {
+ 0.0, 12.5, -100.0,
+ CoordinateSystemType.YUp
+ });
+
+ Assert.AreEqual(12.5, elevation, 1e-6, "YUp 下边界最小高程应来自宿主 Y,而不是 bounds.Min.Z。");
+ }
+
private static void AssertPoint(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
diff --git a/UnitTests/Integration/AutoPathGridGenerationAutomationTests.cs b/UnitTests/Integration/AutoPathGridGenerationAutomationTests.cs
new file mode 100644
index 0000000..6fa673c
--- /dev/null
+++ b/UnitTests/Integration/AutoPathGridGenerationAutomationTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json.Linq;
+
+namespace NavisworksTransport.UnitTests.Integration
+{
+ [TestClass]
+ [TestCategory("NavisworksIntegration")]
+ [TestCategory("NavisworksIntegration.Ground")]
+ public class AutoPathGridGenerationAutomationTests
+ {
+ [TestMethod]
+ [Timeout(240000)]
+ public async Task GroundAutoPathGrid_DefaultRoute_ObstacleCellsShouldCoverAtLeastHalfOfGrid()
+ {
+ using (var client = new NavisworksTestAutomationClient())
+ {
+ await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
+
+ JObject response = await client.AnalyzeAutoPathGridAsync("Ground").ConfigureAwait(false);
+ Assert.IsTrue(response.Value("ok"), "测试 HTTP 接口返回失败: " + (string)response["error"]);
+
+ JObject data = (JObject)response["data"];
+ Assert.IsNotNull(data, "缺少测试结果 data");
+
+ JObject gridStats = (JObject)data["gridStats"];
+ Assert.IsNotNull(gridStats, "缺少 gridStats");
+
+ int totalCellCount = (int)gridStats["totalCellCount"];
+ int obstacleCellCount = (int)gridStats["obstacleCellCount"];
+
+ Assert.IsTrue(totalCellCount > 0, "网格总数必须大于 0");
+ Assert.IsTrue(
+ obstacleCellCount * 2 >= totalCellCount,
+ $"当前障碍物网格数量过少: obstacle={obstacleCellCount}, total={totalCellCount}");
+ }
+ }
+ }
+}
diff --git a/UnitTests/Integration/NavisworksTestAutomationClient.cs b/UnitTests/Integration/NavisworksTestAutomationClient.cs
index e5bfab5..68d1222 100644
--- a/UnitTests/Integration/NavisworksTestAutomationClient.cs
+++ b/UnitTests/Integration/NavisworksTestAutomationClient.cs
@@ -57,6 +57,15 @@ namespace NavisworksTransport.UnitTests.Integration
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
+ public async Task AnalyzeAutoPathGridAsync(string pathType)
+ {
+ string requestUri = string.Format(
+ "/api/test/analyze-auto-path-grid?pathType={0}",
+ Uri.EscapeDataString(pathType));
+
+ return await PostJsonAsync(requestUri).ConfigureAwait(false);
+ }
+
private async Task GetJsonAsync(string requestUri)
{
using (HttpResponseMessage response = await _httpClient.GetAsync(requestUri).ConfigureAwait(false))
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index f325675..b93ac8c 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -1072,7 +1072,12 @@ namespace NavisworksTransport
// 1. 获取模型边界
var bounds = GetModelBounds() ?? throw new Exception("无法获取模型边界,请确保模型已加载");
- LogManager.Info($"模型边界: Min({bounds.Min.X:F2}, {bounds.Min.Y:F2}), Max({bounds.Max.X:F2}, {bounds.Max.Y:F2})");
+ var hostAdapter = new HostCoordinateAdapter(CoordinateSystemManager.Instance.ResolvedType);
+ var hostBoundsMin = new System.Numerics.Vector3((float)bounds.Min.X, (float)bounds.Min.Y, (float)bounds.Min.Z);
+ var hostBoundsMax = new System.Numerics.Vector3((float)bounds.Max.X, (float)bounds.Max.Y, (float)bounds.Max.Z);
+ var (boundsMinH1, boundsMaxH1, boundsMinH2, boundsMaxH2) =
+ HostPlanarGridHelper.GetHorizontalRange3(hostBoundsMin, hostBoundsMax, hostAdapter);
+ LogManager.Info($"模型边界: H1[{boundsMinH1:F2}, {boundsMaxH1:F2}], H2[{boundsMinH2:F2}, {boundsMaxH2:F2}]");
// 智能选择网格大小
if (gridSize <= 0)
@@ -1096,7 +1101,7 @@ namespace NavisworksTransport
// 获取当前文档
var document = Application.ActiveDocument;
- LogManager.Info($"网格生成参数 - 边界: {bounds.Min.X:F2},{bounds.Min.Y:F2} -> {bounds.Max.X:F2},{bounds.Max.Y:F2}");
+ LogManager.Info($"网格生成参数 - 边界: H1 {boundsMinH1:F2}->{boundsMaxH1:F2}, H2 {boundsMinH2:F2}->{boundsMaxH2:F2}");
LogManager.Info($"网格生成参数 - 网格大小: {gridSize}m, 物体半径: {objectRadius}m, 安全边距: {safetyMargin}m");
LogManager.Info($"网格生成参数 - 起点: ({startPoint.Position.X:F2}, {startPoint.Position.Y:F2}, {startPoint.Position.Z:F2})");
LogManager.Info($"网格生成参数 - 终点: ({endPoint.Position.X:F2}, {endPoint.Position.Y:F2}, {endPoint.Position.Z:F2})");
@@ -4717,14 +4722,10 @@ namespace NavisworksTransport
{
try
{
- var width = bounds.Max.X - bounds.Min.X;
- var height = bounds.Max.Y - bounds.Min.Y;
- var maxDimension = Math.Max(width, height);
-
- // 基于模型大小智能选择网格大小
- if (maxDimension > 1000) return 5.0; // 大型模型:5米
- if (maxDimension > 500) return 2.0; // 中型模型:2米
- return 0.5; // 小型模型:0.5米
+ return CalculateOptimalGridSizeFromBounds(
+ bounds.Min.X, bounds.Min.Y, bounds.Min.Z,
+ bounds.Max.X, bounds.Max.Y, bounds.Max.Z,
+ CoordinateSystemManager.Instance.ResolvedType);
}
catch
{
@@ -4732,6 +4733,32 @@ namespace NavisworksTransport
}
}
+ ///
+ /// 根据宿主包围盒和当前宿主坐标系语义,计算自动路径规划的最优网格大小。
+ /// 关键约束:
+ /// - 仅修正“水平平面”的解释,不改变历史阈值与业务策略。
+ /// - ZUp: 使用 Host XY 平面跨度。
+ /// - YUp: 使用 Host XZ 平面跨度。
+ ///
+ private static double CalculateOptimalGridSizeFromBounds(
+ double minX, double minY, double minZ,
+ double maxX, double maxY, double maxZ,
+ CoordinateSystemType coordinateSystemType)
+ {
+ var adapter = new HostCoordinateAdapter(coordinateSystemType);
+ var hostMin = new System.Numerics.Vector3((float)minX, (float)minY, (float)minZ);
+ var hostMax = new System.Numerics.Vector3((float)maxX, (float)maxY, (float)maxZ);
+ var (min1, max1, min2, max2) = HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
+
+ var width = max1 - min1;
+ var height = max2 - min2;
+ var maxDimension = Math.Max(width, height);
+
+ if (maxDimension > 1000) return 5.0;
+ if (maxDimension > 500) return 2.0;
+ return 0.5;
+ }
+
///
/// 获取通道模型项用于高度计算
///
@@ -5198,11 +5225,14 @@ namespace NavisworksTransport
{
var cell = gridMap.Cells[x, y];
- // 计算网格单元格的XY中心位置(Z坐标在多层逻辑中单独处理)
+ // 计算网格单元格的宿主平面中心位置(高程在多层逻辑中单独处理)
var gridPos = new NavisworksTransport.PathPlanning.GridPoint2D(x, y);
- var gridCorner = gridMap.GridToWorld3D(gridPos);
- double centerX = gridCorner.X + gridMap.CellSize / 2;
- double centerY = gridCorner.Y + gridMap.CellSize / 2;
+ var gridCorner2D = gridMap.GridToWorld2D(gridPos);
+ var diagonalCorner2D = gridMap.GridToWorld2D(new NavisworksTransport.PathPlanning.GridPoint2D(x + 1, y + 1));
+ var gridCenter2D = new Point3D(
+ (gridCorner2D.X + diagonalCorner2D.X) / 2.0,
+ (gridCorner2D.Y + diagonalCorner2D.Y) / 2.0,
+ (gridCorner2D.Z + diagonalCorner2D.Z) / 2.0);
// 所有网格都基于高度层,统一处理
if (cell.HeightLayers != null && cell.HeightLayers.Count > 0)
@@ -5250,7 +5280,7 @@ namespace NavisworksTransport
// 创建该层的可视化标记
if (cellType.HasValue)
{
- var gridCenter = new Point3D(centerX, centerY, layerZ);
+ var gridCenter = gridMap.SetWorldElevation(gridCenter2D, layerZ);
var marker = new GridMarker(gridCenter, cellType.Value, x, y, layerZ);
// 根据类型添加到对应列表
@@ -5278,8 +5308,9 @@ namespace NavisworksTransport
// Unknown网格没有高度层,用于调试
if (_showUnknownGrid)
{
- var gridCenter = new Point3D(centerX, centerY, gridCorner.Z);
- var marker = new GridMarker(gridCenter, GridVisualizationType.Unknown, x, y, gridCorner.Z);
+ double unknownElevation = gridMap.GetWorldElevation(gridCenter2D);
+ var gridCenter = gridMap.SetWorldElevation(gridCenter2D, unknownElevation);
+ var marker = new GridMarker(gridCenter, GridVisualizationType.Unknown, x, y, unknownElevation);
gridVis.Unknown.Add(marker);
unknownCells++;
totalVisualized++;
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index b7b5f87..e4da8b3 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -2144,6 +2144,37 @@ namespace NavisworksTransport
return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length);
}
+ private static Point3D ApplyVectorOffset(Point3D point, Vector3D direction, double distance)
+ {
+ var normalized = Normalize(direction);
+ return new Point3D(
+ point.X + normalized.X * distance,
+ point.Y + normalized.Y * distance,
+ point.Z + normalized.Z * distance);
+ }
+
+ private static (Vector3D axis1, Vector3D axis2) GetHostPlanarAxes(Vector3D hostUp)
+ {
+ var normalizedUp = Normalize(hostUp);
+ var reference = Math.Abs(normalizedUp.X) > 0.9
+ ? new Vector3D(0, 1, 0)
+ : new Vector3D(1, 0, 0);
+
+ double projection = reference.X * normalizedUp.X + reference.Y * normalizedUp.Y + reference.Z * normalizedUp.Z;
+ var axis1 = Normalize(new Vector3D(
+ reference.X - projection * normalizedUp.X,
+ reference.Y - projection * normalizedUp.Y,
+ reference.Z - projection * normalizedUp.Z));
+
+ if (axis1.X == 0 && axis1.Y == 0 && axis1.Z == 0)
+ {
+ axis1 = new Vector3D(0, 0, 1);
+ }
+
+ var axis2 = Normalize(Cross(normalizedUp, axis1));
+ return (axis1, axis2);
+ }
+
///
/// 添加切点标记
///
@@ -3086,15 +3117,18 @@ namespace NavisworksTransport
// 计算正方形的边长(直径)
double sideLength = pointMarker.Radius * 2;
- // 定义正方形的两个轴向量(沿X、Y轴)
- var xVector = new Vector3D(sideLength, 0, 0);
- var yVector = new Vector3D(0, sideLength, 0);
+ // 网格矩形必须落在宿主水平平面内,不能写死世界 XY。
+ var hostUp = GetHostUpVector();
+ var (planarAxis1, planarAxis2) = GetHostPlanarAxes(hostUp);
+ var xVector = new Vector3D(planarAxis1.X * sideLength, planarAxis1.Y * sideLength, planarAxis1.Z * sideLength);
+ var yVector = new Vector3D(planarAxis2.X * sideLength, planarAxis2.Y * sideLength, planarAxis2.Z * sideLength);
- // 计算正方形原点(中心点减去各轴向量的一半),略微抬高以避免与模型重叠
+ // 沿宿主 up 略微抬高,避免与模型重叠。
+ var liftedCenter = ApplyVectorOffset(pointMarker.Center, hostUp, 0.01);
var origin = new Point3D(
- pointMarker.Center.X - pointMarker.Radius,
- pointMarker.Center.Y - pointMarker.Radius,
- pointMarker.Center.Z + 0.01
+ liftedCenter.X - planarAxis1.X * pointMarker.Radius - planarAxis2.X * pointMarker.Radius,
+ liftedCenter.Y - planarAxis1.Y * pointMarker.Radius - planarAxis2.Y * pointMarker.Radius,
+ liftedCenter.Z - planarAxis1.Z * pointMarker.Radius - planarAxis2.Z * pointMarker.Radius
);
// 使用Rectangle API渲染正方形
@@ -3108,12 +3142,8 @@ namespace NavisworksTransport
/// 点标记
private void RenderCircleMarker(Graphics graphics, CircleMarker pointMarker)
{
- // 调整中心点位置,略微抬高以避免与模型重叠
- Point3D adjustCenterPoint = new Point3D(
- pointMarker.Center.X,
- pointMarker.Center.Y,
- pointMarker.Center.Z + 0.01
- );
+ // 调整中心点位置,沿宿主 up 略微抬高以避免与模型重叠
+ Point3D adjustCenterPoint = ApplyVectorOffset(pointMarker.Center, pointMarker.Normal, 0.01);
// 使用Circle API渲染圆形
graphics.Circle(adjustCenterPoint, pointMarker.Normal, pointMarker.Radius, true);
diff --git a/src/Core/Services/TestAutomationHttpService.cs b/src/Core/Services/TestAutomationHttpService.cs
index ece2ce5..1aa6f6a 100644
--- a/src/Core/Services/TestAutomationHttpService.cs
+++ b/src/Core/Services/TestAutomationHttpService.cs
@@ -11,6 +11,8 @@ using System.Threading.Tasks;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Commands;
using NavisworksTransport.Core.Animation;
+using NavisworksTransport.Core.Config;
+using NavisworksTransport.PathPlanning;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
using Newtonsoft.Json;
@@ -297,6 +299,22 @@ namespace NavisworksTransport.Core.Services
return;
}
+ if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(request.Path, "/api/test/analyze-auto-path-grid", StringComparison.OrdinalIgnoreCase))
+ {
+ object payload = InvokeOnUiThread(() => AnalyzeAutoPathGridPayload(request.Query));
+ await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
+ return;
+ }
+
+ if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(request.Path, "/api/test/run-auto-path", StringComparison.OrdinalIgnoreCase))
+ {
+ object payload = InvokeOnUiThread(() => RunAutoPathPayload(request.Query));
+ await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
+ return;
+ }
+
if (!string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase))
{
@@ -714,6 +732,150 @@ namespace NavisworksTransport.Core.Services
route.Name.StartsWith(DefaultAutoTestRoutePrefix, StringComparison.OrdinalIgnoreCase));
}
+ private static object AnalyzeAutoPathGridPayload(Dictionary query)
+ {
+ PathPlanningManager pathManager = RequirePathManager();
+ PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
+ if (route.Points == null || route.Points.Count < 2)
+ {
+ throw new InvalidOperationException($"路径点不足,无法分析自动路径网格: {route.Name}");
+ }
+
+ List orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
+ PathPoint startPoint = orderedPoints.First();
+ PathPoint endPoint = orderedPoints.Last();
+
+ BoundingBox3D bounds = InvokePathManagerPrivate(pathManager, "GetModelBounds");
+ double gridSize = InvokePathManagerPrivate(pathManager, "CalculateOptimalGridSize", bounds);
+
+ var config = ConfigManager.Instance.Current?.PathEditing
+ ?? throw new InvalidOperationException("缺少 PathEditing 配置,无法分析自动路径网格");
+
+ double objectLengthInMeters = config.ObjectLengthMeters;
+ double objectWidthInMeters = config.ObjectWidthMeters;
+ double objectHeightInMeters = config.ObjectHeightMeters;
+ double safetyMarginInMeters = config.SafetyMarginMeters;
+ double objectRadiusInMeters = Math.Max(objectLengthInMeters, objectWidthInMeters) / 2.0;
+
+ var gridMapGenerator = new GridMapGenerator();
+ GridMap gridMap = gridMapGenerator.GenerateFromBIM(
+ bounds,
+ gridSize,
+ objectRadiusInMeters,
+ safetyMarginInMeters,
+ startPoint.Position,
+ endPoint.Position,
+ objectHeightInMeters);
+
+ return new
+ {
+ requestedPathType = route.PathType.ToString(),
+ route = new
+ {
+ id = route.Id,
+ name = route.Name,
+ pathType = route.PathType.ToString(),
+ pointCount = route.Points.Count,
+ startPoint = SerializePathPoint(startPoint),
+ endPoint = SerializePathPoint(endPoint)
+ },
+ planningParameters = new
+ {
+ gridSizeInMeters = gridSize,
+ objectLengthInMeters,
+ objectWidthInMeters,
+ objectHeightInMeters,
+ objectRadiusInMeters,
+ safetyMarginInMeters
+ },
+ bounds = new
+ {
+ min = SerializePoint3D(bounds.Min),
+ max = SerializePoint3D(bounds.Max)
+ },
+ gridStats = SerializeGridStats(gridMap),
+ obstacleDiagnostics = BuildObstacleDiagnostics(gridMapGenerator, gridMap, objectHeightInMeters, safetyMarginInMeters)
+ };
+ }
+
+ private static object RunAutoPathPayload(Dictionary query)
+ {
+ PathPlanningManager pathManager = RequirePathManager();
+ PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
+ if (route.Points == null || route.Points.Count < 2)
+ {
+ throw new InvalidOperationException($"路径点不足,无法执行自动路径规划: {route.Name}");
+ }
+
+ List orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
+ PathPoint startPoint = orderedPoints.First();
+ PathPoint endPoint = orderedPoints.Last();
+
+ var config = ConfigManager.Instance.Current?.PathEditing
+ ?? throw new InvalidOperationException("缺少 PathEditing 配置,无法执行自动路径规划");
+
+ BoundingBox3D bounds = InvokePathManagerPrivate(pathManager, "GetModelBounds");
+ double gridSize = InvokePathManagerPrivate(pathManager, "CalculateOptimalGridSize", bounds);
+ double objectHeightInMeters = config.ObjectHeightMeters;
+ double objectLengthInMeters = config.ObjectLengthMeters;
+ double objectWidthInMeters = config.ObjectWidthMeters;
+ double safetyMarginInMeters = config.SafetyMarginMeters;
+ double objectRadiusInMeters = Math.Max(objectLengthInMeters, objectWidthInMeters) / 2.0;
+ PathStrategy strategy = PathStrategy.Shortest;
+
+ string strategyText = GetOptionalQueryValue(query, "strategy");
+ if (!string.IsNullOrWhiteSpace(strategyText) &&
+ Enum.TryParse(strategyText, true, out PathStrategy parsedStrategy))
+ {
+ strategy = parsedStrategy;
+ }
+
+ PathRoute autoRoute = pathManager
+ .AutoPlanPath(
+ startPoint,
+ endPoint,
+ objectRadiusInMeters,
+ safetyMarginInMeters,
+ gridSize,
+ objectHeightInMeters,
+ strategy)
+ .GetAwaiter()
+ .GetResult();
+
+ if (autoRoute == null)
+ {
+ throw new InvalidOperationException("AutoPlanPath 返回空结果");
+ }
+
+ return new
+ {
+ sourceRoute = new
+ {
+ id = route.Id,
+ name = route.Name,
+ pathType = route.PathType.ToString()
+ },
+ generatedRoute = new
+ {
+ id = autoRoute.Id,
+ name = autoRoute.Name,
+ pathType = autoRoute.PathType.ToString(),
+ pointCount = autoRoute.Points?.Count ?? 0,
+ length = autoRoute.TotalLength
+ },
+ planningParameters = new
+ {
+ gridSizeInMeters = gridSize,
+ objectHeightInMeters,
+ objectLengthInMeters,
+ objectWidthInMeters,
+ objectRadiusInMeters,
+ safetyMarginInMeters,
+ strategy = strategy.ToString()
+ }
+ };
+ }
+
private static PathPlanningManager RequirePathManager()
{
PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
@@ -867,6 +1029,280 @@ namespace NavisworksTransport.Core.Services
};
}
+ private static object SerializePathPoint(PathPoint point)
+ {
+ if (point == null)
+ {
+ return null;
+ }
+
+ return new
+ {
+ id = point.Id,
+ name = point.Name,
+ type = point.Type.ToString(),
+ index = point.Index,
+ position = SerializePoint3D(point.Position)
+ };
+ }
+
+ private static object SerializeGridStats(GridMap gridMap)
+ {
+ if (gridMap == null)
+ {
+ return null;
+ }
+
+ int walkableCellCount = 0;
+ int nonWalkableCellCount = 0;
+ int obstacleCellCount = 0;
+ int holeCellCount = 0;
+ int boundaryLayerCount = 0;
+
+ for (int x = 0; x < gridMap.Width; x++)
+ {
+ for (int y = 0; y < gridMap.Height; y++)
+ {
+ GridCell cell = gridMap.Cells[x, y];
+ bool isWalkable = cell.HasAnyWalkableLayer();
+ if (isWalkable)
+ {
+ walkableCellCount++;
+ }
+ else
+ {
+ nonWalkableCellCount++;
+ }
+
+ if (string.Equals(cell.CellType, "障碍物", StringComparison.Ordinal))
+ {
+ obstacleCellCount++;
+ }
+
+ if (string.Equals(cell.CellType, "空洞", StringComparison.Ordinal))
+ {
+ holeCellCount++;
+ }
+
+ if (cell.HeightLayers != null)
+ {
+ boundaryLayerCount += cell.HeightLayers.Count(layer => layer.IsBoundary);
+ }
+ }
+ }
+
+ int totalCellCount = gridMap.Width * gridMap.Height;
+
+ return new
+ {
+ width = gridMap.Width,
+ height = gridMap.Height,
+ totalCellCount,
+ walkableCellCount,
+ nonWalkableCellCount,
+ obstacleCellCount,
+ holeCellCount,
+ boundaryLayerCount,
+ obstacleRatio = totalCellCount == 0 ? 0.0 : (double)obstacleCellCount / totalCellCount,
+ nonWalkableRatio = totalCellCount == 0 ? 0.0 : (double)nonWalkableCellCount / totalCellCount
+ };
+ }
+
+ private static object BuildObstacleDiagnostics(GridMapGenerator gridMapGenerator, GridMap gridMap, double objectHeightInMeters, double safetyMarginInMeters)
+ {
+ if (gridMapGenerator == null || gridMap == null)
+ {
+ return null;
+ }
+
+ double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units);
+ double scanHeightInModelUnits = (objectHeightInMeters + safetyMarginInMeters) * metersToModelUnits;
+
+ var traversableItems = CategoryAttributeManager.GetAllTraversableLogisticsItems().ToList();
+ var irrelevantItems = CategoryAttributeManager.GetLogisticsItemsByType("无关项");
+
+ var collectRelatedItemsMethod = typeof(GridMapGenerator).GetMethod(
+ "CollectRelatedItems",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
+ var postProcessGeometryItemsMethod = typeof(GridMapGenerator).GetMethod(
+ "PostProcessGeometryItems",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
+ var getObstacleElevationRangeMethod = typeof(GridMapGenerator).GetMethod(
+ "GetObstacleElevationRange",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
+ null,
+ new[] { typeof(Point3D), typeof(Point3D), typeof(GridMap) },
+ null);
+ var calculateBoundingBoxGridCoverageMethod = typeof(GridMapGenerator).GetMethod(
+ "CalculateBoundingBoxGridCoverage",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
+
+ var traversableRelatedItems = (HashSet)collectRelatedItemsMethod.Invoke(gridMapGenerator, new object[] { traversableItems });
+ var irrelevantRelatedItems = (HashSet)collectRelatedItemsMethod.Invoke(gridMapGenerator, new object[] { irrelevantItems });
+ var geometryItems = ModelItemAnalysisHelper.GetAllVisibleGeometryItems();
+ var itemCache = (Dictionary)postProcessGeometryItemsMethod.Invoke(
+ gridMapGenerator,
+ new object[] { geometryItems, traversableRelatedItems, irrelevantRelatedItems });
+
+ var validItems = itemCache
+ .Where(kvp => kvp.Value.HasGeometry &&
+ !kvp.Value.IsContainer &&
+ !kvp.Value.IsChannelItem &&
+ !kvp.Value.IsChildOfChannel &&
+ kvp.Value.BoundingBox != null &&
+ kvp.Value.IsInScanHeightRange)
+ .ToList();
+
+ double walkableMinElevation = double.MaxValue;
+ double walkableMaxElevation = double.MinValue;
+ int walkableGridCount = 0;
+ for (int x = 0; x < gridMap.Width; x++)
+ {
+ for (int y = 0; y < gridMap.Height; y++)
+ {
+ GridCell cell = gridMap.Cells[x, y];
+ if (!cell.HasAnyWalkableLayer() || cell.HeightLayers == null || cell.HeightLayers.Count == 0)
+ {
+ continue;
+ }
+
+ walkableGridCount++;
+ double cellElevation = cell.HeightLayers[0].Z;
+ walkableMinElevation = Math.Min(walkableMinElevation, cellElevation);
+ walkableMaxElevation = Math.Max(walkableMaxElevation, cellElevation);
+ }
+ }
+
+ if (walkableGridCount == 0)
+ {
+ walkableMinElevation = 0.0;
+ walkableMaxElevation = 0.0;
+ }
+
+ double scanMin = walkableMinElevation;
+ double scanMax = walkableMaxElevation + scanHeightInModelUnits;
+
+ var keptItems = new List