From 3f2d66c25529f880d79efc918e99b5b7509309dc Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 29 Sep 2025 23:25:21 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=92=8C=E4=BF=AE=E6=94=B9A*?=
=?UTF-8?q?=E8=BF=94=E5=9B=9E=E7=9A=84=E5=9D=90=E6=A0=87=E8=BD=AC=E6=8D=A2?=
=?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.claude/settings.local.json | 19 +-
AStarTestRunner.csproj | 57 ++
CLAUDE.md | 20 +
NavisworksTransport.UnitTests.csproj | 23 +-
.../AStarDebuggingTest.cs | 867 ++++++++++++++++++
NavisworksTransportPlugin.csproj | 2 +
TestRunner.cs | 86 ++
...deo_export_ffmpegcore_solution_20250929.md | 462 ++++++++++
src/Core/PathPlanningManager.cs | 42 +-
src/Core/PathPlanningModels.cs | 6 +
.../Properties/CategoryAttributeManager.cs | 41 +-
.../NavisworksComPropertyManager.cs | 1 -
src/PathPlanning/AutoPathFinder.cs | 251 +++--
src/PathPlanning/ChannelBasedGridBuilder.cs | 29 +-
src/PathPlanning/GridCellBuilder.cs | 151 +++
src/PathPlanning/GridMap.cs | 65 +-
src/PathPlanning/GridMapGenerator.cs | 96 +-
src/PathPlanning/PathOptimizer.cs | 91 +-
.../TimeMarkerCalculationService.cs | 278 ++++++
src/UI/WPF/ViewModels/TimeTagViewModel.cs | 264 ++++--
src/UI/WPF/Views/TimeTagDialog.xaml | 68 +-
21 files changed, 2657 insertions(+), 262 deletions(-)
create mode 100644 AStarTestRunner.csproj
create mode 100644 NavisworksTransport.UnitTests/AStarDebuggingTest.cs
create mode 100644 TestRunner.cs
create mode 100644 doc/working/video_export_ffmpegcore_solution_20250929.md
create mode 100644 src/PathPlanning/GridCellBuilder.cs
create mode 100644 src/PathPlanning/TimeMarkerCalculationService.cs
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 09b748d..848666d 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -137,7 +137,24 @@
"Read(//c/ProgramData/Autodesk/Navisworks Manage 2026/NavisworksTransport/logs/**)",
"Bash(sed:*)",
"Read(//c/ProgramData/Autodesk/**)",
- "Bash(cd:*)"
+ "Bash(cd:*)",
+ "Bash(cat:*)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" NavisworksTransport.UnitTests.csproj /p:Configuration=Debug /p:Platform=AnyCPU /verbosity:minimal)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" NavisworksTransport.UnitTests.csproj /p:Configuration=Debug /p:Platform=AnyCPU)",
+ "Read(//c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin//**)",
+ "Read(//c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/Extensions/TestPlatform//**)",
+ "Bash(\"/c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe\" NavisworksTransport.UnitTests.csproj)",
+ "Bash(\"/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/Extensions/TestPlatform/vstest.console.exe\" bin/Debug/NavisworksTransport.UnitTests.dll --logger:console)",
+ "Bash(\"/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/Extensions/TestPlatform/vstest.console.exe\" bin/Debug/NavisworksTransport.UnitTests.dll --TestAdapterPath:packagesMSTest.TestAdapter.3.0.4build_common --logger:console)",
+ "Bash(\"/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/Extensions/TestPlatform/vstest.console.exe\" bin/Debug/NavisworksTransport.UnitTests.dll --TestAdapterPath:packagesMSTest.TestAdapter.3.0.4buildnet462 --logger:console)",
+ "Bash(\"/c/Program Files (x86)/Microsoft SDKs/Windows/v10.0A/bin/NETFX 4.8 Tools/x64/ildasm.exe\" /text bin/Debug/NavisworksTransport.UnitTests.dll)",
+ "Bash(\"/c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe\" AStarTestRunner.csproj)",
+ "Bash(./AStarTestRunner.exe)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" AStarTestRunner.csproj /p:Configuration=Debug /p:Platform=AnyCPU /verbosity:minimal)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" AStarTestRunner.csproj /p:Configuration=Debug /p:Platform=AnyCPU)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" AStarTestRunner.csproj)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" NavisworksTransport.UnitTests.csproj)",
+ "Bash(\"bin\\Debug\\AStarTestRunner.exe\")"
],
"deny": [],
"additionalDirectories": [
diff --git a/AStarTestRunner.csproj b/AStarTestRunner.csproj
new file mode 100644
index 0000000..d96ad8f
--- /dev/null
+++ b/AStarTestRunner.csproj
@@ -0,0 +1,57 @@
+
+
+
+
+
+ Debug
+ AnyCPU
+ {F2B4A8C1-2DEF-4765-8392-E67AD8372FG2}
+ Exe
+ Properties
+ TestRunner
+ AStarTestRunner
+ v4.8
+ 512
+ true
+ x64
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+ packages\RoyT.AStar.2.1.0\lib\netstandard2.0\Roy-T.AStar.dll
+ True
+
+
+
+
+ bin\Debug\NavisworksTransport.UnitTests.dll
+ True
+
+
+
+
+ bin\Debug\NavisworksTransportPlugin.dll
+ True
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 3835421..9a3cb04 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -211,3 +211,23 @@ public class PathClickToolPlugin : ToolPlugin { }
- 程序的日志在:C:\ProgramData\Autodesk\Navisworks Manage 2026\NavisworksTransport\logs\debug.log
- 使用agent完成任务前,一定要先用Plan模式设计好方案和任务清单,并征得我同意。
- 网格坐标代表的是网格单元的左下角,而不是中心点!
+
+## 运行路径测试
+
+需要先编译单元测试DLL,然后再编译TestRunner:
+
+● Bash(powershell -Command "& 'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe'
+ NavisworksTransport.UnitTests.csproj /p:Configu…)
+ ⎿ 适用于 .NET Framework MSBuild 版本 17.14.10+8b8e13593
+
+ NavisworksTransport.UnitTests -> C:\Users\Tellme\apps\NavisworksTransport\bin\Debug\NavisworksTransport.UnitTests.dll
+
+● Bash(powershell -Command "& 'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe'
+ AStarTestRunner.csproj /p:Configuration=Debug /…)
+ ⎿ 适用于 .NET Framework MSBuild 版本 17.14.10+8b8e13593
+
+ AStarTestRunner -> C:\Users\Tellme\apps\NavisworksTransport\bin\Debug\AStarTestRunner.exe
+
+● 现在运行新的测试程序:
+
+● Bash("bin\Debug\AStarTestRunner.exe")
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index dc86821..12be7f2 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -73,25 +73,24 @@
bin\Debug\NavisworksTransportPlugin.dll
True
+
+
+
+ packages\RoyT.AStar.2.1.0\lib\netstandard2.0\Roy-T.AStar.dll
+ True
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
diff --git a/NavisworksTransport.UnitTests/AStarDebuggingTest.cs b/NavisworksTransport.UnitTests/AStarDebuggingTest.cs
new file mode 100644
index 0000000..c34495a
--- /dev/null
+++ b/NavisworksTransport.UnitTests/AStarDebuggingTest.cs
@@ -0,0 +1,867 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Roy_T.AStar.Grids;
+using Roy_T.AStar.Primitives;
+using Roy_T.AStar.Paths;
+using NavisworksTransport.PathPlanning;
+using NavisworksTransport.Utils;
+using NavisworksTransport;
+
+namespace NavisworksTransport.UnitTests
+{
+ ///
+ /// 简化的3D点结构体(用于测试)
+ ///
+ public struct Point3D
+ {
+ public double X { get; set; }
+ public double Y { get; set; }
+ public double Z { get; set; }
+
+ public Point3D(double x, double y, double z)
+ {
+ X = x;
+ Y = y;
+ Z = z;
+ }
+
+ public override string ToString() => $"({X:F2}, {Y:F2}, {Z:F2})";
+ }
+
+ ///
+ /// 简化的2D点结构体(用于测试)
+ ///
+ public struct Point2D
+ {
+ public double X { get; set; }
+ public double Y { get; set; }
+
+ public Point2D(double x, double y)
+ {
+ X = x;
+ Y = y;
+ }
+
+ public override string ToString() => $"({X:F2}, {Y:F2})";
+ }
+
+ ///
+ /// 简化的网格点结构体(用于测试)
+ ///
+ public struct GridPoint2D
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+
+ public GridPoint2D(int x, int y)
+ {
+ X = x;
+ Y = y;
+ }
+
+ public override string ToString() => $"({X}, {Y})";
+ }
+
+ ///
+ /// 简化的边界框结构体(用于测试)
+ ///
+ public struct BoundingBox3D
+ {
+ public Point3D Min { get; set; }
+ public Point3D Max { get; set; }
+
+ public BoundingBox3D(Point3D min, Point3D max)
+ {
+ Min = min;
+ Max = max;
+ }
+ }
+
+ ///
+ /// A*算法问题检测测试用例
+ /// 专门用于调试A*路径穿越障碍物网格的问题
+ ///
+ [TestClass]
+ public class AStarDebuggingTest
+ {
+ ///
+ /// 基础障碍物避让测试
+ /// 创建一个3x3网格,中间设置障碍物,测试A*是否能正确绕过
+ ///
+ [TestMethod]
+ public void TestBasicObstacleAvoidance()
+ {
+ // 创建3x3网格
+ var gridSize = new GridSize(3, 3);
+ var cellSize = new Size(Distance.FromMeters(1.0f), Distance.FromMeters(1.0f));
+ var velocity = Velocity.FromKilometersPerHour(5.0f);
+
+ var grid = Grid.CreateGridWithLateralConnections(gridSize, cellSize, velocity);
+
+ // 断开中间位置(1,1),模拟障碍物
+ var obstaclePos = new GridPosition(1, 1);
+ grid.DisconnectNode(obstaclePos);
+
+ // 验证障碍物位置确实被断开
+ var obstacleNode = grid.GetNode(obstaclePos);
+ var obstacleEdges = obstacleNode.Outgoing.Count();
+ Console.WriteLine($"障碍物位置({obstaclePos.X},{obstaclePos.Y})的出边数: {obstacleEdges}");
+ Assert.AreEqual(0, obstacleEdges, "障碍物节点应该没有出边");
+
+ // 尝试从(0,0)到(2,2)寻路,必须绕过中间的障碍物
+ var startPos = new GridPosition(0, 0);
+ var endPos = new GridPosition(2, 2);
+
+ var pathfinder = new PathFinder();
+ var path = pathfinder.FindPath(startPos, endPos, grid);
+
+ Assert.IsNotNull(path, "应该能找到绕过障碍物的路径");
+ Assert.IsTrue(path.Edges.Count > 0, "路径应该包含边");
+
+ // 关键检查:验证路径不经过障碍物位置
+ var pathNodes = new List { startPos };
+ foreach (var edge in path.Edges)
+ {
+ var nodeGridX = (int)Math.Floor(edge.End.Position.X / 1.0); // cellSize = 1米
+ var nodeGridY = (int)Math.Floor(edge.End.Position.Y / 1.0);
+ var nodeGridPos = new GridPosition(nodeGridX, nodeGridY);
+ pathNodes.Add(nodeGridPos);
+ }
+
+ Console.WriteLine("路径经过的网格坐标:");
+ foreach (var node in pathNodes)
+ {
+ Console.WriteLine($" 网格({node.X},{node.Y})");
+ }
+
+ // 验证路径不包含障碍物坐标
+ bool pathContainsObstacle = pathNodes.Any(pos => pos.X == obstaclePos.X && pos.Y == obstaclePos.Y);
+ Assert.IsFalse(pathContainsObstacle, $"路径不应该经过障碍物位置({obstaclePos.X},{obstaclePos.Y})");
+ }
+
+ ///
+ /// 坐标映射验证测试
+ /// 验证GridPosition和Node.Position之间的对应关系
+ ///
+ [TestMethod]
+ public void TestCoordinateMapping()
+ {
+ var gridSize = new GridSize(5, 5);
+ var cellSize = new Size(Distance.FromMeters(2.0f), Distance.FromMeters(2.0f));
+ var velocity = Velocity.FromKilometersPerHour(5.0f);
+
+ var grid = Grid.CreateGridWithLateralConnections(gridSize, cellSize, velocity);
+
+ // 测试几个关键坐标的映射关系
+ var testPositions = new[]
+ {
+ new GridPosition(0, 0),
+ new GridPosition(1, 1),
+ new GridPosition(2, 3),
+ new GridPosition(4, 4)
+ };
+
+ Console.WriteLine("GridPosition到Node.Position映射验证:");
+ foreach (var gridPos in testPositions)
+ {
+ var node = grid.GetNode(gridPos);
+ var nodePos = node.Position;
+
+ Console.WriteLine($"网格({gridPos.X},{gridPos.Y}) -> 节点位置({nodePos.X:F2},{nodePos.Y:F2})");
+
+ // 验证映射关系:GridPosition * CellSize = Node.Position
+ var expectedX = gridPos.X * 2.0; // cellSize = 2米
+ var expectedY = gridPos.Y * 2.0;
+
+ Assert.AreEqual(expectedX, nodePos.X, 0.01, $"X坐标映射错误:网格{gridPos.X}应对应节点{expectedX}");
+ Assert.AreEqual(expectedY, nodePos.Y, 0.01, $"Y坐标映射错误:网格{gridPos.Y}应对应节点{expectedY}");
+ }
+ }
+
+ ///
+ /// 实际问题场景重现测试
+ /// 重现日志中出现的49,25到62,25障碍物路径问题
+ ///
+ [TestMethod]
+ public void TestActualProblemScenario()
+ {
+ // 模拟实际场景:创建包含问题区域的网格
+ var gridSize = new GridSize(120, 50); // 足够大的网格
+ var cellSize = new Size(Distance.FromMeters(1.0f), Distance.FromMeters(1.0f));
+ var velocity = Velocity.FromKilometersPerHour(5.0f);
+
+ var grid = Grid.CreateGridWithLateralConnections(gridSize, cellSize, velocity);
+
+ // 根据日志,将50,25到61,25设置为障碍物
+ var obstaclePositions = new List();
+ for (int x = 50; x <= 61; x++)
+ {
+ obstaclePositions.Add(new GridPosition(x, 25));
+ }
+
+ Console.WriteLine("设置障碍物位置:");
+ foreach (var obstaclePos in obstaclePositions)
+ {
+ grid.DisconnectNode(obstaclePos);
+ Console.WriteLine($" 断开网格({obstaclePos.X},{obstaclePos.Y})");
+
+ // 验证断开成功
+ var node = grid.GetNode(obstaclePos);
+ var edgeCount = node.Outgoing.Count();
+ Assert.AreEqual(0, edgeCount, $"障碍物网格({obstaclePos.X},{obstaclePos.Y})应该没有出边,实际有{edgeCount}条");
+ }
+
+ // 尝试从49,25到62,25寻路(跨越障碍物区域)
+ var startPos = new GridPosition(49, 25);
+ var endPos = new GridPosition(62, 25);
+
+ Console.WriteLine($"开始寻路:从({startPos.X},{startPos.Y})到({endPos.X},{endPos.Y})");
+
+ var pathfinder = new PathFinder();
+ var path = pathfinder.FindPath(startPos, endPos, grid);
+
+ if (path != null && path.Edges.Count > 0)
+ {
+ Console.WriteLine($"找到路径,包含{path.Edges.Count}条边");
+ Console.WriteLine($"路径类型: {path.Type}");
+
+ // 提取路径经过的所有网格坐标
+ var pathGridPositions = new List { startPos };
+
+ foreach (var edge in path.Edges)
+ {
+ var nodeGridX = (int)Math.Floor(edge.End.Position.X / 1.0);
+ var nodeGridY = (int)Math.Floor(edge.End.Position.Y / 1.0);
+ var nodeGridPos = new GridPosition(nodeGridX, nodeGridY);
+ pathGridPositions.Add(nodeGridPos);
+ }
+
+ Console.WriteLine("路径经过的网格坐标:");
+ foreach (var pos in pathGridPositions)
+ {
+ Console.WriteLine($" 网格({pos.X},{pos.Y})");
+ }
+
+ // 关键验证:检查路径是否经过任何障碍物位置
+ var pathThroughObstacles = new List();
+ foreach (var pathPos in pathGridPositions)
+ {
+ if (obstaclePositions.Any(obs => obs.X == pathPos.X && obs.Y == pathPos.Y))
+ {
+ pathThroughObstacles.Add(pathPos);
+ }
+ }
+
+ if (pathThroughObstacles.Any())
+ {
+ Console.WriteLine("❌ 发现问题:路径穿越了以下障碍物位置:");
+ foreach (var obs in pathThroughObstacles)
+ {
+ Console.WriteLine($" 障碍物网格({obs.X},{obs.Y})");
+ }
+
+ Assert.Fail($"路径不应该穿越障碍物!发现{pathThroughObstacles.Count}个穿越点");
+ }
+ else
+ {
+ Console.WriteLine("✅ 路径正确避开了所有障碍物");
+ }
+ }
+ else
+ {
+ Console.WriteLine("未找到路径");
+ // 在这种情况下,应该尝试找到最近接近路径
+ Assert.IsNotNull(path, "至少应该返回ClosestApproach类型的路径");
+ }
+ }
+
+ ///
+ /// 网格连接状态详细检查测试
+ /// 验证断开节点后,相邻节点的连接状态
+ ///
+ [TestMethod]
+ public void TestGridConnectionState()
+ {
+ var gridSize = new GridSize(5, 5);
+ var cellSize = new Size(Distance.FromMeters(1.0f), Distance.FromMeters(1.0f));
+ var velocity = Velocity.FromKilometersPerHour(5.0f);
+
+ var grid = Grid.CreateGridWithLateralConnections(gridSize, cellSize, velocity);
+
+ // 断开中心位置(2,2)
+ var centerPos = new GridPosition(2, 2);
+ grid.DisconnectNode(centerPos);
+
+ // 检查中心位置的连接状态
+ var centerNode = grid.GetNode(centerPos);
+ Console.WriteLine($"中心节点({centerPos.X},{centerPos.Y})出边数: {centerNode.Outgoing.Count()}");
+ Console.WriteLine($"中心节点({centerPos.X},{centerPos.Y})入边数: {centerNode.Incoming.Count()}");
+
+ // 检查相邻节点是否还指向已断开的中心节点
+ var adjacentPositions = new[]
+ {
+ new GridPosition(1, 2), // 左
+ new GridPosition(3, 2), // 右
+ new GridPosition(2, 1), // 下
+ new GridPosition(2, 3) // 上
+ };
+
+ Console.WriteLine("相邻节点连接检查:");
+ foreach (var adjPos in adjacentPositions)
+ {
+ var adjNode = grid.GetNode(adjPos);
+ var connectsToCenter = adjNode.Outgoing.Any(edge =>
+ {
+ var targetGridX = (int)Math.Floor(edge.End.Position.X / 1.0);
+ var targetGridY = (int)Math.Floor(edge.End.Position.Y / 1.0);
+ return targetGridX == centerPos.X && targetGridY == centerPos.Y;
+ });
+
+ Console.WriteLine($" 节点({adjPos.X},{adjPos.Y}) -> 中心节点: {(connectsToCenter ? "是" : "否")}");
+ Assert.IsFalse(connectsToCenter, $"相邻节点({adjPos.X},{adjPos.Y})不应该连接到已断开的中心节点");
+ }
+ }
+
+ ///
+ /// 使用真实坐标系统的A*算法测试
+ /// 集成实际系统的坐标转换方法,以复现真实环境中的问题
+ ///
+ [TestMethod]
+ public void TestWithRealCoordinateSystem()
+ {
+ Console.WriteLine("=== 开始真实坐标系统A*测试 ===");
+
+ // 创建模拟的GridMap - 使用实际系统的参数
+ var mockGridMap = CreateRealGridMap();
+
+ // 创建对应的Roy_T.AStar网格
+ var astarGrid = CreateAStarGridFromGridMap(mockGridMap);
+
+ // 设置障碍物 - 复现实际问题场景中的障碍物位置
+ SetupObstaclesInMockGridMap(mockGridMap, astarGrid);
+
+ // 定义测试起点和终点(使用实际世界坐标)
+ var startWorld = new Point3D(25.0, 12.5, 0.0); // 对应网格(49,25)的世界坐标
+ var endWorld = new Point3D(31.0, 12.5, 0.0); // 对应网格(62,25)的世界坐标
+
+ Console.WriteLine($"测试场景: 从世界坐标({startWorld.X}, {startWorld.Y})到({endWorld.X}, {endWorld.Y})");
+
+ // 执行真实的坐标转换和A*寻路
+ var pathResult = ExecuteRealCoordinatePathfinding(startWorld, endWorld, mockGridMap, astarGrid);
+
+ // 验证结果
+ ValidateRealCoordinatePathResult(pathResult, startWorld, endWorld, mockGridMap);
+ }
+
+ ///
+ /// 通道路径查找测试
+ /// 创建一个有明确通道的场景,验证A*返回的路径坐标是否正确
+ ///
+ [TestMethod]
+ public void TestChannelPathfinding()
+ {
+ Console.WriteLine("=== 开始通道路径查找测试 ===");
+
+ // 创建测试场景:60x50网格,0.5米单元
+ var gridSize = new GridSize(60, 50);
+ var cellSize = new Size(Distance.FromMeters(0.5f), Distance.FromMeters(0.5f));
+ var velocity = Velocity.FromKilometersPerHour(5.0f);
+
+ var grid = Grid.CreateGridWithLateralConnections(gridSize, cellSize, velocity);
+
+ // 设置障碍物:创建一个通道场景
+ // 障碍物区域:X=48-54, Y=24和Y=27(上下墙)
+ // X=48和X=54, Y=24-27(左右墙)
+ // 通道区域:X=49-53, Y=25-26(可通行)
+
+ var obstaclePositions = new List();
+
+ // 上下墙
+ for (int x = 48; x <= 54; x++)
+ {
+ obstaclePositions.Add(new GridPosition(x, 24)); // 下墙
+ obstaclePositions.Add(new GridPosition(x, 27)); // 上墙
+ }
+
+ // 左右墙
+ for (int y = 25; y <= 26; y++)
+ {
+ obstaclePositions.Add(new GridPosition(48, y)); // 左墙
+ obstaclePositions.Add(new GridPosition(54, y)); // 右墙
+ }
+
+ Console.WriteLine("设置障碍物布局:");
+ Console.WriteLine(" 上墙: X=48-54, Y=27");
+ Console.WriteLine(" 下墙: X=48-54, Y=24");
+ Console.WriteLine(" 左墙: X=48, Y=25-26");
+ Console.WriteLine(" 右墙: X=54, Y=25-26");
+ Console.WriteLine(" 通道: X=49-53, Y=25-26");
+
+ // 设置障碍物
+ foreach (var obstaclePos in obstaclePositions)
+ {
+ grid.DisconnectNode(obstaclePos);
+ }
+
+ // 验证障碍物设置
+ Console.WriteLine("\n障碍物设置验证:");
+ int obstacleCount = 0;
+ foreach (var obstaclePos in obstaclePositions)
+ {
+ var node = grid.GetNode(obstaclePos);
+ var edgeCount = node.Outgoing.Count();
+ if (edgeCount == 0) obstacleCount++;
+ Console.WriteLine($" 障碍物({obstaclePos.X},{obstaclePos.Y})出边数: {edgeCount}");
+ }
+ Console.WriteLine($"成功设置{obstacleCount}个障碍物网格");
+
+ // 设置测试起点和终点:在通道内但靠近障碍物
+ var startPos = new GridPosition(49, 25); // 通道左下角
+ var endPos = new GridPosition(53, 26); // 通道右上角
+
+ Console.WriteLine($"\n开始寻路:从网格({startPos.X},{startPos.Y})到网格({endPos.X},{endPos.Y})");
+
+ // 执行A*寻路
+ var pathfinder = new PathFinder();
+ var path = pathfinder.FindPath(startPos, endPos, grid);
+
+ // 验证找到了路径
+ Assert.IsNotNull(path, "应该能在通道中找到路径");
+ Assert.IsTrue(path.Edges.Count > 0, $"路径应该包含边,实际边数: {path.Edges.Count}");
+
+ Console.WriteLine($"✅ 找到路径,包含{path.Edges.Count}条边");
+ Console.WriteLine($"路径类型: {path.Type}");
+
+ // 关键验证:检查路径上的每个点
+ var pathPoints = new List { startPos };
+
+ Console.WriteLine("\nA*路径坐标详细分析:");
+ Console.WriteLine("起点转换: A*位置 -> 米坐标 -> 网格坐标");
+
+ int pointIndex = 0;
+ foreach (var edge in path.Edges)
+ {
+ pointIndex++;
+ var endMeterPos = edge.End.Position;
+
+ // 关键:将A*返回的米坐标转换回网格坐标
+ var calculatedGridX = (int)Math.Floor(endMeterPos.X / 0.5); // cellSize = 0.5米
+ var calculatedGridY = (int)Math.Floor(endMeterPos.Y / 0.5);
+ var calculatedGridPos = new GridPosition(calculatedGridX, calculatedGridY);
+
+ pathPoints.Add(calculatedGridPos);
+
+ Console.WriteLine($" 点{pointIndex}: A*米坐标({endMeterPos.X:F2}, {endMeterPos.Y:F2}) -> 计算网格({calculatedGridPos.X}, {calculatedGridPos.Y})");
+ }
+
+ // 验证所有路径点是否在允许的通道区域内
+ Console.WriteLine("\n路径点合法性检查:");
+ var invalidPoints = new List();
+
+ foreach (var point in pathPoints)
+ {
+ bool isInChannel = (point.X >= 49 && point.X <= 53) && (point.Y >= 25 && point.Y <= 26);
+ bool isObstacle = obstaclePositions.Any(obs => obs.X == point.X && obs.Y == point.Y);
+
+ string status;
+ if (isObstacle)
+ {
+ status = "❌障碍物";
+ invalidPoints.Add(point);
+ }
+ else if (isInChannel)
+ {
+ status = "✅通道内";
+ }
+ else
+ {
+ status = "⚠️通道外";
+ invalidPoints.Add(point);
+ }
+
+ Console.WriteLine($" 网格({point.X},{point.Y}) - {status}");
+ }
+
+ // 最终验证
+ if (invalidPoints.Any())
+ {
+ Console.WriteLine($"\n🔥 发现问题:{invalidPoints.Count}个路径点在非法位置!");
+ foreach (var invalid in invalidPoints)
+ {
+ Console.WriteLine($" 非法点: 网格({invalid.X},{invalid.Y})");
+ }
+
+ Assert.Fail($"A*路径包含{invalidPoints.Count}个非法位置的点!这证明了A*算法或坐标转换存在问题。");
+ }
+ else
+ {
+ Console.WriteLine("\n✅ 所有路径点都在合法的通道区域内");
+ Console.WriteLine("✅ A*算法返回的坐标转换正确");
+ }
+ }
+
+ ///
+ /// 创建模拟的GridMap,使用实际系统的参数
+ ///
+ private MockGridMap CreateRealGridMap()
+ {
+ // 模拟实际系统的GridMap参数
+ var origin = new Point2D(0.0, 0.0); // 网格原点
+ var cellSize = 0.5; // 网格单元大小:0.5米
+ var width = 150; // 网格宽度:150个单元
+ var height = 80; // 网格高度:80个单元
+ var bounds = new BoundingBox3D(
+ new Point3D(0, 0, 0),
+ new Point3D(width * cellSize, height * cellSize, 10.0)
+ );
+
+ Console.WriteLine($"创建GridMap - 原点:({origin.X}, {origin.Y}), 单元大小:{cellSize}m, 尺寸:{width}x{height}");
+
+ var gridMap = new MockGridMap(origin, cellSize, width, height, bounds);
+
+ // 初始化为可通行网格
+ for (int x = 0; x < width; x++)
+ {
+ for (int y = 0; y < height; y++)
+ {
+ gridMap.SetCell(new GridPoint2D(x, y), true);
+ }
+ }
+
+ return gridMap;
+ }
+
+ ///
+ /// 从GridMap创建对应的Roy_T.AStar网格
+ ///
+ private Grid CreateAStarGridFromGridMap(MockGridMap gridMap)
+ {
+ // 使用真实的单位转换
+ double cellSizeInMeters = MockUnitsConverter.ConvertToMeters(gridMap.CellSize);
+
+ var gridSize = new GridSize(gridMap.Width, gridMap.Height);
+ var cellSize = new Size(
+ Distance.FromMeters((float)cellSizeInMeters),
+ Distance.FromMeters((float)cellSizeInMeters)
+ );
+ var velocity = Velocity.FromKilometersPerHour(5.0f);
+
+ Console.WriteLine($"创建A*网格 - 网格大小:{gridSize.Columns}x{gridSize.Rows}, 单元大小:{cellSizeInMeters}m");
+
+ return Grid.CreateGridWithLateralConnections(gridSize, cellSize, velocity);
+ }
+
+ ///
+ /// 在网格中设置障碍物
+ ///
+ private void SetupObstaclesInMockGridMap(MockGridMap gridMap, Grid astarGrid)
+ {
+ // 模拟实际问题场景:在路径中间设置障碍物
+ var obstaclePositions = new List();
+
+ // 设置从网格(50,25)到(61,25)为障碍物
+ for (int x = 50; x <= 61; x++)
+ {
+ obstaclePositions.Add(new GridPoint2D(x, 25));
+ }
+
+ Console.WriteLine("设置障碍物位置:");
+ foreach (var obstaclePos in obstaclePositions)
+ {
+ if (gridMap.IsValidGridPosition(obstaclePos))
+ {
+ // 在MockGridMap中设置为不可通行
+ gridMap.SetCell(obstaclePos, false);
+
+ // 在A*网格中断开连接
+ var astarPos = new GridPosition(obstaclePos.X, obstaclePos.Y);
+ astarGrid.DisconnectNode(astarPos);
+
+ var worldPos = gridMap.GridToWorld2D(obstaclePos);
+ Console.WriteLine($" 障碍物网格({obstaclePos.X},{obstaclePos.Y}) -> 世界坐标({worldPos.X:F2},{worldPos.Y:F2})");
+
+ // 验证断开成功
+ var node = astarGrid.GetNode(astarPos);
+ var edgeCount = node.Outgoing.Count();
+ Console.WriteLine($" A*节点出边数: {edgeCount}");
+ }
+ }
+ }
+
+ ///
+ /// 执行真实的坐标转换和路径查找
+ ///
+ private PathFindingResult ExecuteRealCoordinatePathfinding(Point3D startWorld, Point3D endWorld, MockGridMap gridMap, Grid astarGrid)
+ {
+ Console.WriteLine("=== 执行真实坐标转换和A*寻路 ===");
+
+ // 步骤1: 世界坐标转换为网格坐标
+ var startGrid = gridMap.WorldToGrid(startWorld);
+ var endGrid = gridMap.WorldToGrid(endWorld);
+
+ Console.WriteLine($"世界坐标转换:");
+ Console.WriteLine($" 起点: 世界({startWorld.X}, {startWorld.Y}) -> 网格({startGrid.X}, {startGrid.Y})");
+ Console.WriteLine($" 终点: 世界({endWorld.X}, {endWorld.Y}) -> 网格({endGrid.X}, {endGrid.Y})");
+
+ // 步骤2: 网格坐标转换为A*位置
+ var startPos = new GridPosition(startGrid.X, startGrid.Y);
+ var endPos = new GridPosition(endGrid.X, endGrid.Y);
+
+ Console.WriteLine($"A*位置设置:");
+ Console.WriteLine($" 起点A*位置: ({startPos.X}, {startPos.Y})");
+ Console.WriteLine($" 终点A*位置: ({endPos.X}, {endPos.Y})");
+
+ // 步骤3: 执行A*算法
+ var pathfinder = new PathFinder();
+ var astarPath = pathfinder.FindPath(startPos, endPos, astarGrid);
+
+ Console.WriteLine($"A*寻路结果: {(astarPath != null ? $"找到路径,包含{astarPath.Edges.Count}条边" : "未找到路径")}");
+
+ if (astarPath != null && astarPath.Edges.Count > 0)
+ {
+ // 步骤4: 转换A*路径回世界坐标
+ var worldPath = ConvertAStarPathToWorldCoordinates(astarPath, gridMap);
+
+ return new PathFindingResult
+ {
+ PathPoints = worldPath,
+ OriginalEndPoint = endWorld,
+ IsSuccessful = true
+ };
+ }
+
+ return new PathFindingResult
+ {
+ PathPoints = new List(),
+ OriginalEndPoint = endWorld,
+ IsSuccessful = false
+ };
+ }
+
+ ///
+ /// 转换A*路径为世界坐标(模拟AutoPathFinder的逻辑)
+ ///
+ private List ConvertAStarPathToWorldCoordinates(Path astarPath, MockGridMap gridMap)
+ {
+ var worldPath = new List();
+
+ Console.WriteLine("=== A*路径转换为世界坐标 ===");
+
+ // 获取单位转换因子(模拟真实系统)
+ double metersToWorldUnit = 1.0 / MockUnitsConverter.ConvertToMeters(1.0);
+ Console.WriteLine($"单位转换因子: metersToWorldUnit = {metersToWorldUnit}");
+
+ if (astarPath.Edges.Count > 0)
+ {
+ // 添加起点
+ var startNode = astarPath.Edges[0].Start;
+ var startWorldPos = new Point3D(
+ gridMap.Origin.X + startNode.Position.X * metersToWorldUnit,
+ gridMap.Origin.Y + startNode.Position.Y * metersToWorldUnit,
+ 0
+ );
+
+ Console.WriteLine($"起点转换: A*米坐标({startNode.Position.X:F2}, {startNode.Position.Y:F2}) -> 世界坐标({startWorldPos.X:F2}, {startWorldPos.Y:F2})");
+ worldPath.Add(startWorldPos);
+
+ // 添加路径上的每个点
+ foreach (var edge in astarPath.Edges)
+ {
+ var endWorldPos = new Point3D(
+ gridMap.Origin.X + edge.End.Position.X * metersToWorldUnit,
+ gridMap.Origin.Y + edge.End.Position.Y * metersToWorldUnit,
+ 0
+ );
+
+ // 转换为网格坐标以便分析
+ var endGridPos = gridMap.WorldToGrid(endWorldPos);
+
+ Console.WriteLine($"路径点: A*米坐标({edge.End.Position.X:F2}, {edge.End.Position.Y:F2}) -> 世界坐标({endWorldPos.X:F2}, {endWorldPos.Y:F2}) -> 网格({endGridPos.X}, {endGridPos.Y})");
+
+ worldPath.Add(endWorldPos);
+ }
+ }
+
+ return worldPath;
+ }
+
+ ///
+ /// 验证真实坐标系统下的路径结果
+ ///
+ private void ValidateRealCoordinatePathResult(PathFindingResult result, Point3D startWorld, Point3D endWorld, MockGridMap gridMap)
+ {
+ Console.WriteLine("=== 验证路径结果 ===");
+
+ if (!result.IsSuccessful || result.PathPoints.Count == 0)
+ {
+ Console.WriteLine("❌ 未找到路径 - 这可能是正常的,因为障碍物阻挡了直接路径");
+ return;
+ }
+
+ Console.WriteLine($"✅ 找到路径,包含{result.PathPoints.Count}个点");
+
+ // 转换所有路径点为网格坐标以便检查
+ var pathGridPositions = new List();
+ foreach (var worldPos in result.PathPoints)
+ {
+ var gridPos = gridMap.WorldToGrid(worldPos);
+ pathGridPositions.Add(gridPos);
+ }
+
+ Console.WriteLine("路径经过的网格坐标:");
+ foreach (var gridPos in pathGridPositions)
+ {
+ var cell = gridMap.GetCell(gridPos);
+ var isWalkable = cell.IsWalkable;
+ var status = isWalkable ? "✅可通行" : "❌障碍物";
+ Console.WriteLine($" 网格({gridPos.X},{gridPos.Y}) - {status}");
+ }
+
+ // 检查是否有路径穿越障碍物
+ var obstacleTraversals = new List();
+ foreach (var gridPos in pathGridPositions)
+ {
+ var cell = gridMap.GetCell(gridPos);
+ if (!cell.IsWalkable)
+ {
+ obstacleTraversals.Add(gridPos);
+ }
+ }
+
+ if (obstacleTraversals.Any())
+ {
+ Console.WriteLine($"🔥 发现问题:路径穿越了{obstacleTraversals.Count}个障碍物网格!");
+ foreach (var obs in obstacleTraversals)
+ {
+ Console.WriteLine($" 障碍物穿越: 网格({obs.X},{obs.Y})");
+ }
+
+ Assert.Fail($"真实坐标系统下发现A*路径穿越障碍物问题!穿越了{obstacleTraversals.Count}个障碍物网格。");
+ }
+ else
+ {
+ Console.WriteLine("✅ 路径正确避开了所有障碍物");
+ }
+ }
+
+ ///
+ /// 模拟路径查找结果类
+ ///
+ private class PathFindingResult
+ {
+ public List PathPoints { get; set; } = new List();
+ public Point3D OriginalEndPoint { get; set; }
+ public bool IsSuccessful { get; set; }
+ }
+
+ ///
+ /// 模拟UnitsConverter类(用于测试)
+ ///
+ private static class MockUnitsConverter
+ {
+ ///
+ /// 模拟:将距离从文档单位转换为米
+ /// 假设文档单位就是米,转换因子为1.0
+ ///
+ public static double ConvertToMeters(double distance)
+ {
+ return distance * 1.0; // 假设文档单位就是米
+ }
+ }
+
+ ///
+ /// 模拟GridMap类(用于测试)
+ ///
+ private class MockGridMap
+ {
+ public Point2D Origin { get; private set; }
+ public double CellSize { get; private set; }
+ public int Width { get; private set; }
+ public int Height { get; private set; }
+ public BoundingBox3D Bounds { get; private set; }
+
+ private MockGridCell[,] cells;
+
+ public MockGridMap(Point2D origin, double cellSize, int width, int height, BoundingBox3D bounds)
+ {
+ Origin = origin;
+ CellSize = cellSize;
+ Width = width;
+ Height = height;
+ Bounds = bounds;
+ cells = new MockGridCell[width, height];
+
+ // 初始化所有单元格
+ for (int x = 0; x < width; x++)
+ {
+ for (int y = 0; y < height; y++)
+ {
+ cells[x, y] = new MockGridCell { IsWalkable = false };
+ }
+ }
+ }
+
+ ///
+ /// 世界坐标转换为网格坐标
+ ///
+ public GridPoint2D WorldToGrid(Point3D worldPosition)
+ {
+ double gridX = (worldPosition.X - Origin.X) / CellSize;
+ double gridY = (worldPosition.Y - Origin.Y) / CellSize;
+ return new GridPoint2D((int)Math.Floor(gridX), (int)Math.Floor(gridY));
+ }
+
+ ///
+ /// 网格坐标转换为世界2D坐标(网格左下角)
+ ///
+ public Point3D GridToWorld2D(GridPoint2D gridPosition)
+ {
+ double worldX = Origin.X + gridPosition.X * CellSize;
+ double worldY = Origin.Y + gridPosition.Y * CellSize;
+ return new Point3D(worldX, worldY, 0);
+ }
+
+ ///
+ /// 检查网格坐标是否有效
+ ///
+ public bool IsValidGridPosition(GridPoint2D gridPosition)
+ {
+ return gridPosition.X >= 0 && gridPosition.X < Width &&
+ gridPosition.Y >= 0 && gridPosition.Y < Height;
+ }
+
+ ///
+ /// 设置网格单元
+ ///
+ public void SetCell(GridPoint2D gridPosition, bool isWalkable)
+ {
+ if (IsValidGridPosition(gridPosition))
+ {
+ cells[gridPosition.X, gridPosition.Y].IsWalkable = isWalkable;
+ }
+ }
+
+ ///
+ /// 获取网格单元
+ ///
+ public MockGridCell GetCell(GridPoint2D gridPosition)
+ {
+ if (IsValidGridPosition(gridPosition))
+ {
+ return cells[gridPosition.X, gridPosition.Y];
+ }
+ return new MockGridCell { IsWalkable = false };
+ }
+ }
+
+ ///
+ /// 模拟GridCell类(用于测试)
+ ///
+ private class MockGridCell
+ {
+ public bool IsWalkable { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/NavisworksTransportPlugin.csproj b/NavisworksTransportPlugin.csproj
index cae68b1..04cb8b4 100644
--- a/NavisworksTransportPlugin.csproj
+++ b/NavisworksTransportPlugin.csproj
@@ -155,6 +155,7 @@
+
@@ -164,6 +165,7 @@
+
diff --git a/TestRunner.cs b/TestRunner.cs
new file mode 100644
index 0000000..d590258
--- /dev/null
+++ b/TestRunner.cs
@@ -0,0 +1,86 @@
+using System;
+using NavisworksTransport.UnitTests;
+
+namespace TestRunner
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("开始运行A*算法调试测试...");
+
+ var tester = new AStarDebuggingTest();
+
+ try
+ {
+ Console.WriteLine("=== 基础障碍物避让测试 ===");
+ tester.TestBasicObstacleAvoidance();
+ Console.WriteLine("✅ 基础障碍物避让测试通过");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ 基础障碍物避让测试失败: {ex.Message}");
+ }
+
+ try
+ {
+ Console.WriteLine("\n=== 坐标映射验证测试 ===");
+ tester.TestCoordinateMapping();
+ Console.WriteLine("✅ 坐标映射验证测试通过");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ 坐标映射验证测试失败: {ex.Message}");
+ }
+
+ try
+ {
+ Console.WriteLine("\n=== 实际问题场景重现测试 ===");
+ tester.TestActualProblemScenario();
+ Console.WriteLine("✅ 实际问题场景重现测试通过");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ 实际问题场景重现测试失败: {ex.Message}");
+ }
+
+ try
+ {
+ Console.WriteLine("\n=== 网格连接状态检查测试 ===");
+ tester.TestGridConnectionState();
+ Console.WriteLine("✅ 网格连接状态检查测试通过");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ 网格连接状态检查测试失败: {ex.Message}");
+ }
+
+ try
+ {
+ Console.WriteLine("\n=== 真实坐标系统A*算法测试 ===");
+ tester.TestWithRealCoordinateSystem();
+ Console.WriteLine("✅ 真实坐标系统A*算法测试通过");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ 真实坐标系统A*算法测试失败: {ex.Message}");
+ Console.WriteLine($"详细错误信息: {ex}");
+ }
+
+ try
+ {
+ Console.WriteLine("\n=== 通道路径查找测试 ===");
+ tester.TestChannelPathfinding();
+ Console.WriteLine("✅ 通道路径查找测试通过");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ 通道路径查找测试失败: {ex.Message}");
+ Console.WriteLine($"详细错误信息: {ex}");
+ }
+
+ Console.WriteLine("\n测试完成!按任意键退出...");
+ Console.ReadKey();
+ }
+ }
+}
\ No newline at end of file
diff --git a/doc/working/video_export_ffmpegcore_solution_20250929.md b/doc/working/video_export_ffmpegcore_solution_20250929.md
new file mode 100644
index 0000000..cee1d47
--- /dev/null
+++ b/doc/working/video_export_ffmpegcore_solution_20250929.md
@@ -0,0 +1,462 @@
+# FFMpegCore视频导出方案
+
+**文档日期:** 2025年9月29日
+**项目:** NavisworksTransport插件
+**功能:** 动画导出为导航视频
+
+## 技术架构概述
+
+### 核心组件设计
+```
+VideoExportManager
+├── VideoExportSettings(导出参数配置)
+├── FrameCaptureService(动画帧捕获服务)
+├── VideoEncodingService(视频编码服务)
+└── ProgressReportingService(进度报告服务)
+```
+
+### 集成架构
+- **帧捕获点:** PathAnimationManager.OnTimerTick方法中,在RequestDelayedRedraw()后调用Document.GenerateImage()
+- **UI集成:** AnimationControlView.xaml添加"导出为视频"按钮
+- **数据流:** Bitmap序列 → FFMpegCore → MP4/AVI视频文件
+
+## FFMpegCore技术细节
+
+### 库信息
+- **NuGet包:** FFMpegCore 5.2.0
+- **兼容性:** .NET Framework 4.7(项目当前版本)
+- **目标框架:** .NET Standard 2.0
+
+### 依赖项
+```xml
+
+
+
+```
+
+## 核心代码实现
+
+### 1. 视频导出管理器
+```csharp
+using FFMpegCore;
+using FFMpegCore.Pipes;
+using FFMpegCore.Extensions.System.Drawing.Common;
+using System.Drawing;
+using System.Collections.Generic;
+
+public class VideoExportManager
+{
+ public class VideoExportSettings
+ {
+ public string OutputPath { get; set; }
+ public int Width { get; set; } = 1920;
+ public int Height { get; set; } = 1080;
+ public int FrameRate { get; set; } = 30;
+ public VideoFormat Format { get; set; } = VideoFormat.Mp4;
+ public VideoCodec Codec { get; set; } = VideoCodec.LibX264;
+ public ImageGenerationStyle RenderStyle { get; set; } = ImageGenerationStyle.ScenePlusOverlay;
+ }
+
+ private List _capturedFrames;
+ private bool _isCapturing = false;
+ private VideoExportSettings _currentSettings;
+
+ public void StartVideoCapture(VideoExportSettings settings)
+ {
+ _currentSettings = settings;
+ _capturedFrames = new List();
+ _isCapturing = true;
+
+ // 配置FFmpeg二进制路径
+ GlobalFFOptions.Configure(new FFOptions
+ {
+ BinaryFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg"),
+ TemporaryFilesFolder = Path.GetTempPath()
+ });
+
+ LogManager.Info($"开始视频捕获:{settings.Width}x{settings.Height}@{settings.FrameRate}fps");
+ }
+
+ public void CaptureCurrentFrame()
+ {
+ if (!_isCapturing) return;
+
+ try
+ {
+ // 使用Navisworks API捕获当前帧
+ using (var bitmap = Document.GenerateImage(
+ _currentSettings.RenderStyle,
+ _currentSettings.Width,
+ _currentSettings.Height,
+ enableSectioning: false))
+ {
+ // 复制Bitmap避免内存问题
+ var frameCopy = new Bitmap(bitmap);
+ _capturedFrames.Add(frameCopy);
+
+ LogManager.Info($"已捕获第{_capturedFrames.Count}帧");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"帧捕获失败: {ex.Message}", ex);
+ }
+ }
+
+ public async Task FinishVideoCapture()
+ {
+ if (!_isCapturing || _capturedFrames.Count == 0)
+ {
+ LogManager.Warning("没有捕获到有效帧,取消视频生成");
+ return;
+ }
+
+ _isCapturing = false;
+
+ try
+ {
+ LogManager.Info($"开始生成视频,共{_capturedFrames.Count}帧");
+
+ // 创建视频帧源
+ var videoFramesSource = new RawVideoPipeSource(CreateFrameEnumerable())
+ {
+ FrameRate = _currentSettings.FrameRate
+ };
+
+ // 生成视频文件
+ await FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(_currentSettings.OutputPath, true, options => options
+ .WithVideoCodec(_currentSettings.Codec)
+ .WithFramerate(_currentSettings.FrameRate)
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(_currentSettings.Width, _currentSettings.Height)))
+ .ProcessAsynchronously();
+
+ LogManager.Info($"视频生成完成:{_currentSettings.OutputPath}");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"视频生成失败: {ex.Message}", ex);
+ throw;
+ }
+ finally
+ {
+ // 清理内存
+ foreach (var frame in _capturedFrames)
+ {
+ frame?.Dispose();
+ }
+ _capturedFrames.Clear();
+ }
+ }
+
+ private IEnumerable CreateFrameEnumerable()
+ {
+ foreach (var bitmap in _capturedFrames)
+ {
+ yield return new BitmapVideoFrameWrapper(bitmap);
+ }
+ }
+}
+```
+
+### 2. PathAnimationManager集成
+```csharp
+// 在PathAnimationManager类中添加视频导出支持
+public class PathAnimationManager
+{
+ private VideoExportManager _videoExportManager;
+
+ private void OnTimerTick(object sender, EventArgs e)
+ {
+ try
+ {
+ // 处理动画帧
+ ProcessAnimationFrame();
+
+ // 关键改进:显式请求重绘,解决卡顿
+ NavisApplication.ActiveDocument.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.All);
+
+ // 新增:视频导出时捕获帧
+ if (_videoExportManager?.IsCapturing == true)
+ {
+ _videoExportManager.CaptureCurrentFrame();
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[DispatcherTimer模式] 动画更新异常: {ex.Message}");
+ ShutdownAnimation();
+ }
+ }
+
+ public void StartVideoExport(VideoExportManager.VideoExportSettings settings)
+ {
+ _videoExportManager = new VideoExportManager();
+ _videoExportManager.StartVideoCapture(settings);
+ }
+
+ public async Task FinishVideoExport()
+ {
+ if (_videoExportManager != null)
+ {
+ await _videoExportManager.FinishVideoCapture();
+ _videoExportManager = null;
+ }
+ }
+}
+```
+
+### 3. UI集成代码
+```csharp
+// AnimationControlView.xaml.cs中添加
+private async void OnExportVideoButtonClick(object sender, RoutedEventArgs e)
+{
+ try
+ {
+ LogManager.Info("用户点击导出视频按钮");
+
+ // 检查前置条件
+ if (ViewModel?.CurrentPathRoute == null || ViewModel.CurrentPathRoute.Points.Count == 0)
+ {
+ MessageBox.Show("请先选择有效的路径", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (ViewModel?.SelectedAnimatedObject == null)
+ {
+ MessageBox.Show("请先选择移动物体", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // 显示视频导出设置对话框
+ var exportDialog = new VideoExportSettingsDialog();
+
+ var parentWindow = Window.GetWindow(this);
+ if (parentWindow != null)
+ {
+ exportDialog.Owner = parentWindow;
+ }
+
+ if (exportDialog.ShowDialog() == true && exportDialog.IsConfirmed)
+ {
+ var settings = new VideoExportManager.VideoExportSettings
+ {
+ OutputPath = exportDialog.OutputPath,
+ Width = exportDialog.VideoWidth,
+ Height = exportDialog.VideoHeight,
+ FrameRate = exportDialog.FrameRate,
+ Format = exportDialog.VideoFormat,
+ Codec = exportDialog.VideoCodec
+ };
+
+ // 开始视频导出
+ await ViewModel.StartVideoExportAsync(settings);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"导出视频失败: {ex.Message}", ex);
+ MessageBox.Show($"导出视频失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+}
+```
+
+## 部署要求
+
+### 文件分发清单
+```
+NavisworksTransport插件目录/
+├── NavisworksTransportPlugin.dll(主插件)
+├── FFMpegCore.dll(NuGet自动处理)
+├── Instances.dll(依赖项)
+├── System.Text.Json.dll(依赖项)
+└── ffmpeg/
+ ├── ffmpeg.exe(Windows x64版本,~50MB)
+ └── ffprobe.exe(探测工具,~20MB)
+```
+
+### FFmpeg二进制文件获取
+1. **官方下载:** https://ffmpeg.org/download.html#build-windows
+2. **推荐版本:** FFmpeg 4.4.0+ Windows x64 static builds
+3. **文件来源:** Gyan.dev或BtbN builds(预编译静态版本)
+
+### 配置代码
+```csharp
+// 在应用程序启动时配置FFmpeg路径
+public static void ConfigureFFmpeg()
+{
+ var ffmpegPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg");
+
+ if (!Directory.Exists(ffmpegPath))
+ {
+ throw new DirectoryNotFoundException($"FFmpeg二进制目录不存在: {ffmpegPath}");
+ }
+
+ GlobalFFOptions.Configure(new FFOptions
+ {
+ BinaryFolder = ffmpegPath,
+ TemporaryFilesFolder = Path.GetTempPath()
+ });
+
+ LogManager.Info($"FFmpeg已配置: {ffmpegPath}");
+}
+```
+
+## 支持的视频格式
+
+### 输出格式
+- **MP4**(推荐):H.264编码,兼容性最好
+- **AVI**:MPEG-4编码,文件较大但兼容性好
+- **WebM**:VP9编码,适合web播放
+- **MOV**:QuickTime格式,适合Mac用户
+
+### 编码参数建议
+```csharp
+// 高质量设置
+.WithVideoCodec(VideoCodec.LibX264)
+.WithConstantRateFactor(23) // CRF值,18-28范围,数值越小质量越高
+.WithFramerate(30)
+
+// 压缩优化设置
+.WithVideoCodec(VideoCodec.LibX264)
+.WithVideoBitrate(2000) // 2Mbps码率
+.WithAudioCodec(AudioCodec.Aac) // 如果需要音频
+```
+
+## 性能优化策略
+
+### 内存管理
+```csharp
+// 避免内存泄漏的Bitmap处理
+public void CaptureCurrentFrame()
+{
+ using (var originalBitmap = Document.GenerateImage(...))
+ {
+ // 创建副本避免引用原始对象
+ var frameCopy = new Bitmap(originalBitmap.Width, originalBitmap.Height, originalBitmap.PixelFormat);
+ using (var graphics = Graphics.FromImage(frameCopy))
+ {
+ graphics.DrawImage(originalBitmap, 0, 0);
+ }
+ _capturedFrames.Add(frameCopy);
+ }
+}
+```
+
+### 异步处理
+```csharp
+// UI不会冻结的异步导出
+public async Task ExportVideoAsync(VideoExportSettings settings)
+{
+ await Task.Run(async () =>
+ {
+ // 在后台线程执行视频编码
+ await _videoExportManager.FinishVideoCapture();
+ });
+
+ // 回到UI线程更新状态
+ Dispatcher.Invoke(() =>
+ {
+ UpdateStatus("视频导出完成");
+ });
+}
+```
+
+### 进度报告
+```csharp
+public class VideoExportManager
+{
+ public event EventHandler ProgressChanged;
+
+ private void ReportProgress(int current, int total, string status)
+ {
+ var progress = (double)current / total * 100;
+ ProgressChanged?.Invoke(this, new ProgressEventArgs(progress, status));
+ }
+}
+```
+
+## 错误处理和诊断
+
+### 常见问题排查
+```csharp
+public static class VideoExportDiagnostics
+{
+ public static bool VerifyFFmpegInstallation()
+ {
+ try
+ {
+ var ffmpegPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg", "ffmpeg.exe");
+ if (!File.Exists(ffmpegPath))
+ {
+ LogManager.Error($"FFmpeg可执行文件不存在: {ffmpegPath}");
+ return false;
+ }
+
+ // 测试FFmpeg是否可用
+ var processInfo = new ProcessStartInfo(ffmpegPath, "-version")
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true
+ };
+
+ using (var process = Process.Start(processInfo))
+ {
+ process.WaitForExit(5000);
+ return process.ExitCode == 0;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"FFmpeg验证失败: {ex.Message}", ex);
+ return false;
+ }
+ }
+}
+```
+
+## 风险评估和缓解
+
+### 技术风险
+| 风险等级 | 描述 | 缓解措施 |
+|---------|------|----------|
+| 低 | FFMpegCore兼容性 | 使用稳定版本5.2.0,社区活跃 |
+| 中 | FFmpeg二进制分发 | 提供详细安装指南,支持便携版 |
+| 中 | 内存消耗 | 实现流式处理,及时释放Bitmap |
+| 低 | 文件大小 | 提供压缩参数配置 |
+
+### 部署风险
+- **用户环境差异**:提供自包含的FFmpeg二进制文件
+- **权限问题**:确保插件目录有写入权限
+- **杀毒软件**:FFmpeg可能被误报,提供白名单说明
+
+## 替代方案对比
+
+| 方案 | 优势 | 劣势 | 推荐度 |
+|-----|------|------|--------|
+| FFMpegCore | 功能强大,格式丰富,性能好 | 需要分发FFmpeg二进制 | ⭐⭐⭐⭐⭐ |
+| AForge.NET | API简单 | 已停止维护,功能有限 | ⭐⭐ |
+| 图像序列导出 | 零依赖,部署简单 | 用户需要额外工具合成 | ⭐⭐⭐ |
+
+## 实施时间估算
+
+- **阶段1:基础框架**(1-2天)
+- **阶段2:动画集成**(1天)
+- **阶段3:视频编码**(1天)
+- **阶段4:UI优化**(0.5天)
+- **总计:3.5-4.5天**
+
+## 总结
+
+FFMpegCore方案虽然需要分发额外的二进制文件,但提供了最完整和专业的视频导出功能。通过合理的架构设计和错误处理,可以为用户提供高质量的导航视频导出体验。
+
+**推荐实施顺序:**
+1. 先实现基础的帧捕获功能
+2. 集成FFMpegCore进行视频编码测试
+3. 完善UI和用户体验
+4. 优化性能和错误处理
+
+这个方案能够满足专业用户对视频质量和格式的要求,是长期来看最具价值的技术选择。
\ No newline at end of file
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index 74a07a1..cca9077 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -973,6 +973,10 @@ namespace NavisworksTransport
// 6. 创建PathRoute对象并保存GridMap
var routeName = $"自动路径_{DateTime.Now:HHmmss}";
+
+ // 🔥 关键修复:在创建路径前设置_currentGridMap,确保GetSpeedLimitAtPosition能正常工作
+ _currentGridMap = gridMap;
+
var autoRoute = CreateAutoPathRoute(pathResult, routeName);
// 保存GridMap和参数到PathRoute以便后续恢复网格可视化
@@ -2912,7 +2916,8 @@ namespace NavisworksTransport
{
Name = GenerateAutoPathPointName(i, pathPoints.Count),
Position = point,
- Type = DeterminePointType(i, pathPoints.Count)
+ Type = DeterminePointType(i, pathPoints.Count),
+ SpeedLimit = GetSpeedLimitAtPosition(point)
};
route.Points.Add(pathPoint);
}
@@ -2930,6 +2935,41 @@ namespace NavisworksTransport
}
}
+ ///
+ /// 根据世界坐标获取该位置的限速
+ ///
+ /// 世界坐标
+ /// 限速值(米/秒),0表示未设置限速
+ private double GetSpeedLimitAtPosition(Point3D worldPosition)
+ {
+ try
+ {
+ // 如果没有当前网格地图,返回0
+ if (_currentGridMap == null)
+ {
+ return 0;
+ }
+
+ // 将世界坐标转换为网格坐标
+ var gridPos = _currentGridMap.WorldToGrid(worldPosition);
+
+ // 检查网格坐标是否有效
+ if (!_currentGridMap.IsValidGridPosition(gridPos))
+ {
+ return 0;
+ }
+
+ // 获取网格单元格
+ var cell = _currentGridMap.GetCell(gridPos);
+ return cell?.SpeedLimit ?? 0;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"获取位置限速失败: {ex.Message}");
+ return 0;
+ }
+ }
+
///
/// 生成自动路径点名称
///
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index 5759845..7afe631 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -263,6 +263,10 @@ namespace NavisworksTransport
/// 备注信息
///
public string Notes { get; set; }
+ ///
+ /// 限速(米/秒),0表示未设置限速
+ ///
+ public double SpeedLimit { get; set; }
///
/// 构造函数
@@ -276,6 +280,7 @@ namespace NavisworksTransport
CreatedTime = DateTime.Now;
Index = 0;
Notes = string.Empty;
+ SpeedLimit = 0;
}
///
@@ -293,6 +298,7 @@ namespace NavisworksTransport
CreatedTime = DateTime.Now;
Index = 0;
Notes = string.Empty;
+ SpeedLimit = 0;
}
///
diff --git a/src/Core/Properties/CategoryAttributeManager.cs b/src/Core/Properties/CategoryAttributeManager.cs
index 2c42b32..a73479e 100644
--- a/src/Core/Properties/CategoryAttributeManager.cs
+++ b/src/Core/Properties/CategoryAttributeManager.cs
@@ -592,11 +592,38 @@ namespace NavisworksTransport
public static string GetLogisticsPropertyValueViaCom(ModelItem item, string propertyName)
{
return NavisworksComPropertyManager.GetPropertyValue(
- item,
- LogisticsCategories.LOGISTICS,
+ item,
+ LogisticsCategories.LOGISTICS,
propertyName);
}
+ ///
+ /// 获取模型项的速度限制
+ ///
+ /// 模型项
+ /// 速度限制(米/秒),如果未设置返回0
+ public static double GetSpeedLimit(ModelItem item)
+ {
+ try
+ {
+ string speedStr = GetLogisticsPropertyValueViaCom(item, LogisticsProperties.SPEED_LIMIT);
+
+ if (string.IsNullOrEmpty(speedStr))
+ return 0;
+
+ // 使用统一的解析方法处理限速值
+ double parsedValue = ParseLogisticsLimitValue(speedStr, "限速");
+
+ // ParseLogisticsLimitValue返回-1表示解析失败,我们转换为0
+ return parsedValue > 0 ? parsedValue : 0;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"获取速度限制失败: {ex.Message}");
+ return 0;
+ }
+ }
+
///
/// 通用方法:获取模型项的指定属性值(不限于物流属性)
///
@@ -844,11 +871,13 @@ namespace NavisworksTransport
try
{
- // 移除单位后缀和多余空格
+ // 移除单位后缀和多余空格(按从复杂到简单的顺序处理)
string cleanValue = limitValueStr.Trim()
- .Replace(" m", "") // 移除 " m"
- .Replace("m", "") // 移除 "m"
- .Replace(" ", ""); // 移除所有空格
+ .Replace(" m/s", "") // 先移除 " m/s"
+ .Replace("m/s", "") // 再移除 "m/s"
+ .Replace(" m", "") // 然后移除 " m"
+ .Replace("m", "") // 移除单独的 "m"
+ .Replace(" ", ""); // 最后移除所有空格
if (double.TryParse(cleanValue, out double value) && value > 0)
{
diff --git a/src/Core/Properties/NavisworksComPropertyManager.cs b/src/Core/Properties/NavisworksComPropertyManager.cs
index 00e4604..294021d 100644
--- a/src/Core/Properties/NavisworksComPropertyManager.cs
+++ b/src/Core/Properties/NavisworksComPropertyManager.cs
@@ -287,7 +287,6 @@ namespace NavisworksTransport
if (property.UserName == propertyName)
{
string value = property.value.ToString();
- LogManager.WriteLog($"[COM API读取] 模型: {item.DisplayName}, 属性: {propertyName}, 值: {value}");
return value;
}
}
diff --git a/src/PathPlanning/AutoPathFinder.cs b/src/PathPlanning/AutoPathFinder.cs
index de37c2d..cc2cb92 100644
--- a/src/PathPlanning/AutoPathFinder.cs
+++ b/src/PathPlanning/AutoPathFinder.cs
@@ -160,13 +160,29 @@ namespace NavisworksTransport.PathPlanning
LogManager.Info($"[A*转换-2.5D] A*网格创建完成");
// 先断开所有节点连接
+ LogManager.Info($"[2.5D断开诊断] 开始断开所有节点连接,网格尺寸: {gridMap.Width}x{gridMap.Height}");
for (int x = 0; x < gridMap.Width; x++)
{
for (int y = 0; y < gridMap.Height; y++)
{
- grid.DisconnectNode(new GridPosition(x, y));
+ var pos = new GridPosition(x, y);
+ var nodeBefore = grid.GetNode(pos);
+ var outgoingCountBefore = nodeBefore.Outgoing.Count();
+
+ grid.DisconnectNode(pos);
+
+ // 🔥 诊断记录:断开后验证
+ var nodeAfter = grid.GetNode(pos);
+ var outgoingCountAfter = nodeAfter.Outgoing.Count();
+
+ // 只记录关键坐标的详细信息
+ if ((x >= 49 && x <= 62 && y == 25) || (x == 71 && y == 23))
+ {
+ LogManager.Info($"[2.5D断开诊断] 网格({x},{y}): 断开前边数={outgoingCountBefore} -> 断开后边数={outgoingCountAfter}");
+ }
}
}
+ LogManager.Info($"[2.5D断开诊断] 所有节点连接断开完成");
// 只连接可通行的节点
int connectedCells = 0;
@@ -240,8 +256,9 @@ namespace NavisworksTransport.PathPlanning
if (rightPassable)
{
var rightPos = new GridPosition(x + 1, y);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理rightPos时自然添加(如果rightPos也可通行)
grid.AddEdge(pos, rightPos, traversalVelocity);
- grid.AddEdge(rightPos, pos, traversalVelocity);
}
}
@@ -257,8 +274,45 @@ namespace NavisworksTransport.PathPlanning
if (bottomPassable)
{
var bottomPos = new GridPosition(x, y + 1);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理bottomPos时自然添加(如果bottomPos也可通行)
grid.AddEdge(pos, bottomPos, traversalVelocity);
- grid.AddEdge(bottomPos, pos, traversalVelocity);
+ }
+ }
+
+ // 🔥 连通性修复:添加左侧邻居连接
+ if (x - 1 >= 0)
+ {
+ var leftCell = gridMap.Cells[x - 1, y];
+ bool leftPassable = leftCell.IsWalkable;
+ if (leftPassable && leftCell.PassableHeight.MaxZ > leftCell.PassableHeight.MinZ)
+ {
+ leftPassable = leftCell.PassableHeight.GetSpan() >= vehicleHeight;
+ }
+ if (leftPassable)
+ {
+ var leftPos = new GridPosition(x - 1, y);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理leftPos时自然添加(如果leftPos也可通行)
+ grid.AddEdge(pos, leftPos, traversalVelocity);
+ }
+ }
+
+ // 🔥 连通性修复:添加上方邻居连接
+ if (y - 1 >= 0)
+ {
+ var topCell = gridMap.Cells[x, y - 1];
+ bool topPassable = topCell.IsWalkable;
+ if (topPassable && topCell.PassableHeight.MaxZ > topCell.PassableHeight.MinZ)
+ {
+ topPassable = topCell.PassableHeight.GetSpan() >= vehicleHeight;
+ }
+ if (topPassable)
+ {
+ var topPos = new GridPosition(x, y - 1);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理topPos时自然添加(如果topPos也可通行)
+ grid.AddEdge(pos, topPos, traversalVelocity);
}
}
}
@@ -345,9 +399,14 @@ namespace NavisworksTransport.PathPlanning
{
for (int y = 0; y < gridMap.Height; y++)
{
- grid.DisconnectNode(new GridPosition(x, y));
+ var pos = new GridPosition(x, y);
+ var nodeBefore = grid.GetNode(pos);
+ var outgoingCountBefore = nodeBefore.Outgoing.Count();
+
+ grid.DisconnectNode(pos);
}
}
+ LogManager.Info($"[直线优先断开诊断] 所有节点连接断开完成");
// 3. 对角线固定速度(较低优先级)
var diagonalVelocity = Velocity.FromKilometersPerHour(6);
@@ -394,10 +453,24 @@ namespace NavisworksTransport.PathPlanning
var rightDistance = CalculateStraightDistance(gridPos, new GridPoint2D(1, 0), gridMap, vehicleHeight);
var rightVelocity = CalculateVelocityByDistance(rightDistance);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理rightPos时自然添加(如果rightPos也可通行)
+
+ // 🔥 重连验证:确保不会连接到障碍物
+ var sourceCell = gridMap.Cells[x, y];
+ var targetCell = gridMap.Cells[x + 1, y];
+ if (!sourceCell.IsWalkable || !targetCell.IsWalkable)
+ {
+ LogManager.Error($"[重连验证失败] 尝试连接障碍物: ({x},{y})[IsWalkable={sourceCell.IsWalkable}] -> ({x+1},{y})[IsWalkable={targetCell.IsWalkable}]");
+ }
+
grid.AddEdge(pos, rightPos, rightVelocity);
- grid.AddEdge(rightPos, pos, rightVelocity);
dynamicConnections++;
}
+ else if ((x >= 49 && x <= 62 && y == 25) || (x == 71 && y == 23))
+ {
+ LogManager.Info($"[A*连接诊断] 跳过连接({x},{y})->({x+1},{y}): 目标网格IsWalkable={rightCell.IsWalkable}, CellType={rightCell.CellType}");
+ }
}
// 🔥 核心改动:下方向连接(基于局部直线距离)
@@ -412,8 +485,9 @@ namespace NavisworksTransport.PathPlanning
var downDistance = CalculateStraightDistance(gridPos, new GridPoint2D(0, 1), gridMap, vehicleHeight);
var downVelocity = CalculateVelocityByDistance(downDistance);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理bottomPos时自然添加(如果bottomPos也可通行)
grid.AddEdge(pos, bottomPos, downVelocity);
- grid.AddEdge(bottomPos, pos, downVelocity);
dynamicConnections++;
}
}
@@ -430,8 +504,9 @@ namespace NavisworksTransport.PathPlanning
var leftDistance = CalculateStraightDistance(gridPos, new GridPoint2D(-1, 0), gridMap, vehicleHeight);
var leftVelocity = CalculateVelocityByDistance(leftDistance);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理leftPos时自然添加(如果leftPos也可通行)
grid.AddEdge(pos, leftPos, leftVelocity);
- grid.AddEdge(leftPos, pos, leftVelocity);
dynamicConnections++;
}
}
@@ -448,8 +523,9 @@ namespace NavisworksTransport.PathPlanning
var upDistance = CalculateStraightDistance(gridPos, new GridPoint2D(0, -1), gridMap, vehicleHeight);
var upVelocity = CalculateVelocityByDistance(upDistance);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理topPos时自然添加(如果topPos也可通行)
grid.AddEdge(pos, topPos, upVelocity);
- grid.AddEdge(topPos, pos, upVelocity);
dynamicConnections++;
}
}
@@ -462,8 +538,9 @@ namespace NavisworksTransport.PathPlanning
if (IsPassableWithHeight(diagonalCell, vehicleHeight))
{
var diagonalPos = new GridPosition(x + 1, y + 1);
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理diagonalPos时自然添加(如果diagonalPos也可通行)
grid.AddEdge(pos, diagonalPos, diagonalVelocity);
- grid.AddEdge(diagonalPos, pos, diagonalVelocity);
}
}
}
@@ -583,16 +660,13 @@ namespace NavisworksTransport.PathPlanning
{
// 添加第一条边的起点
var startNode = astarPath.Edges[0].Start;
- var startGridX = (int)Math.Floor(startNode.Position.X / cellSizeInMeters);
- var startGridY = (int)Math.Floor(startNode.Position.Y / cellSizeInMeters);
- gridPath.Add(new GridPoint2D(startGridX, startGridY));
+ var startGridPos = ConvertAStarPositionToGrid(startNode.Position, gridMap);
+ gridPath.Add(startGridPos);
// 添加每条边的终点(避免重复)
foreach (var edge in astarPath.Edges)
{
- var gridX = (int)Math.Floor(edge.End.Position.X / cellSizeInMeters);
- var gridY = (int)Math.Floor(edge.End.Position.Y / cellSizeInMeters);
- var gridPoint = new GridPoint2D(gridX, gridY);
+ var gridPoint = ConvertAStarPositionToGrid(edge.End.Position, gridMap);
// 检查是否与上一个点重复(理论上A*不应该产生重复,但保险起见)
if (gridPath.Count == 0 || !gridPath[gridPath.Count - 1].Equals(gridPoint))
@@ -617,44 +691,27 @@ namespace NavisworksTransport.PathPlanning
LogManager.Info($"[路径转换] 开始转换A*路径,共{astarPath.Edges.Count}条边");
- // 获取单位转换因子(米转世界单位)
- double metersToWorldUnit = 1.0 / UnitsConverter.ConvertToMeters(1.0);
-
- // A*返回的是网格左下角坐标,直接使用,不需要偏移
+ // A*返回的是网格左下角坐标,使用统一的转换方法
if (astarPath.Edges.Count > 0)
{
- // 添加第一条边的起点(网格左下角)
+ // 🔥 修复:使用统一方法转换A*米坐标为网格坐标,然后转为世界坐标
var startNode = astarPath.Edges[0].Start;
- var startWorldPos = new Point3D(
- gridMap.Origin.X + startNode.Position.X * metersToWorldUnit, // 直接转换,不加偏移
- gridMap.Origin.Y + startNode.Position.Y * metersToWorldUnit, // 直接转换,不加偏移
- 0 // Z坐标后续通过高度插值处理
- );
-
- // 获取对应网格的Z坐标(用于高度)
- var startGridPos = gridMap.WorldToGrid(startWorldPos);
- var startCell = gridMap.GetCell(startGridPos);
- if (startCell.HasValue)
- {
- startWorldPos = new Point3D(startWorldPos.X, startWorldPos.Y, startCell.Value.WorldPosition.Z);
- }
-
+ var startGridPos = ConvertAStarPositionToGrid(startNode.Position, gridMap);
+ var startWorldPos = gridMap.GridToWorld3D(startGridPos);
+
LogManager.Info($"[路径转换] 起始节点: A*米坐标({startNode.Position.X:F2}, {startNode.Position.Y:F2}) -> 世界坐标({startWorldPos.X:F2}, {startWorldPos.Y:F2}, {startWorldPos.Z:F2})");
worldPath.Add(startWorldPos);
// 添加每条边的终点(避免重复)
foreach (var edge in astarPath.Edges)
{
- // 将A*的米坐标转换为世界坐标(网格左下角)
- var endWorldPos = new Point3D(
- gridMap.Origin.X + edge.End.Position.X * metersToWorldUnit, // 直接转换,不加偏移
- gridMap.Origin.Y + edge.End.Position.Y * metersToWorldUnit, // 直接转换,不加偏移
- 0 // Z坐标后续处理
- );
-
- // 获取对应网格的Z坐标
- var endGridPos = gridMap.WorldToGrid(endWorldPos);
+ // 使用统一方法转换A*米坐标为网格坐标
+ var endGridPos = ConvertAStarPositionToGrid(edge.End.Position, gridMap);
+
+ // 将网格坐标转换为世界坐标以获取Z坐标
+ var endWorldPos = gridMap.GridToWorld3D(endGridPos);
var endCell = gridMap.GetCell(endGridPos);
+
if (endCell.HasValue)
{
// 调试:检查门网格的Z坐标
@@ -1149,7 +1206,7 @@ namespace NavisworksTransport.PathPlanning
// 检查基本可行走性
if (!cell.IsWalkable)
{
- LogManager.Info($"[高度检查] 单元格不可通行:({gridX}, {gridY}),类型:{cell.CellType}");
+ LogManager.Info($"[高度检查] 单元格不可通行:({gridX}, {gridY}) - IsWalkable={cell.IsWalkable}, CellType={cell.CellType}, SpeedLimit={cell.SpeedLimit:F2}m/s, 原因:网格标记为不可通行");
return false;
}
@@ -1166,7 +1223,7 @@ namespace NavisworksTransport.PathPlanning
return true;
}
- LogManager.Warning($"[高度检查] ❌ 高度区间不满足要求");
+ LogManager.Warning($"[高度检查] ❌ 高度区间不满足要求:({gridX}, {gridY}) - PassableHeight=[{cell.PassableHeight.MinZ:F2}, {cell.PassableHeight.MaxZ:F2}], VehicleHeight={vehicleHeight:F2}, RelativeZ={relativeZ:F2}, SpanOK={spanOk}, ContainsHeight={containsHeight}, 原因:高度约束不满足");
return false;
}
@@ -1225,6 +1282,25 @@ namespace NavisworksTransport.PathPlanning
return adjustedPath;
}
+ ///
+ /// 将A*返回的米坐标转换为网格坐标(统一方法)
+ ///
+ /// A*位置(米坐标)
+ /// 网格地图
+ /// 网格坐标
+ private GridPoint2D ConvertAStarPositionToGrid(Position astarPosition, GridMap gridMap)
+ {
+ LogManager.Info($"[A*原始坐标] A*返回: ({astarPosition.X:F3}, {astarPosition.Y:F3})");
+
+ double cellSizeInMeters = UnitsConverter.ConvertToMeters(gridMap.CellSize);
+ int gridX = (int)Math.Round(astarPosition.X / cellSizeInMeters);
+ int gridY = (int)Math.Round(astarPosition.Y / cellSizeInMeters);
+
+ LogManager.Info($"[A*转换坐标] 转换结果: ({gridX}, {gridY})");
+
+ return new GridPoint2D(gridX, gridY);
+ }
+
///
/// 执行A*算法的核心逻辑
///
@@ -1267,16 +1343,14 @@ namespace NavisworksTransport.PathPlanning
};
// 检查路径类型
- if (astarPath.Type == Roy_T.AStar.Paths.PathType.ClosestApproach)
+ if (astarPath.Type == PathType.ClosestApproach)
{
LogManager.Info($"[A*执行] 找到部分路径(最近接近),包含 {astarPath.Edges.Count + 1} 个网格点");
LogManager.Warning($"[A*执行] 无法完全到达目标点,已找到最接近的可达点");
// 计算完成百分比
var lastNode = astarPath.Edges.Last().End;
- var actualEndGridX = (int)Math.Floor(lastNode.Position.X / cellSizeInMeters);
- var actualEndGridY = (int)Math.Floor(lastNode.Position.Y / cellSizeInMeters);
- var actualEndGrid = new GridPoint2D(actualEndGridX, actualEndGridY);
+ var actualEndGrid = ConvertAStarPositionToGrid(lastNode.Position, gridMap);
var actualEndCell = gridMap.GetCell(actualEndGrid);
var actualEndWorld = gridMap.GridToWorld3D(actualEndGrid);
@@ -1497,14 +1571,29 @@ namespace NavisworksTransport.PathPlanning
LogManager.Info($"[安全优先] A*网格创建完成,尺寸: {gridMap.Width}x{gridMap.Height}");
// 2. 🔥 关键步骤:断开所有连接,准备重新按安全距离连接
+ LogManager.Info($"[安全优先断开诊断] 开始断开所有节点连接,网格尺寸: {gridMap.Width}x{gridMap.Height}");
for (int x = 0; x < gridMap.Width; x++)
{
for (int y = 0; y < gridMap.Height; y++)
{
- grid.DisconnectNode(new GridPosition(x, y));
+ var pos = new GridPosition(x, y);
+ var nodeBefore = grid.GetNode(pos);
+ var outgoingCountBefore = nodeBefore.Outgoing.Count();
+
+ grid.DisconnectNode(pos);
+
+ // 🔥 诊断记录:断开后验证
+ var nodeAfter = grid.GetNode(pos);
+ var outgoingCountAfter = nodeAfter.Outgoing.Count();
+
+ // 只记录关键坐标的详细信息
+ if ((x >= 49 && x <= 62 && y == 25) || (x == 71 && y == 23))
+ {
+ LogManager.Info($"[安全优先断开诊断] 网格({x},{y}): 断开前边数={outgoingCountBefore} -> 断开后边数={outgoingCountAfter}");
+ }
}
}
- LogManager.Info($"[安全优先] 所有节点连接已断开,准备重新构建");
+ LogManager.Info($"[安全优先断开诊断] 所有节点连接断开完成,准备重新构建");
// 3. 计算安全距离图
var safetyDistanceMap = CalculateSafetyDistanceFromGridMap(gridMap);
@@ -1561,8 +1650,9 @@ namespace NavisworksTransport.PathPlanning
else
velocityStats[speedValue] = 1;
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理rightPos时自然添加(如果rightPos也可通行)
grid.AddEdge(pos, rightPos, rightVelocity);
- grid.AddEdge(rightPos, pos, rightVelocity);
safetyConnections++;
}
}
@@ -1587,8 +1677,63 @@ namespace NavisworksTransport.PathPlanning
else
velocityStats[speedValue] = 1;
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理bottomPos时自然添加(如果bottomPos也可通行)
grid.AddEdge(pos, bottomPos, bottomVelocity);
- grid.AddEdge(bottomPos, pos, bottomVelocity);
+ safetyConnections++;
+ }
+ }
+
+ // 🔥 连通性修复:左方向连接(基于安全距离)
+ if (x - 1 >= 0)
+ {
+ var leftCell = gridMap.Cells[x - 1, y];
+ if (IsPassableWithHeight(leftCell, vehicleHeight))
+ {
+ var leftPos = new GridPosition(x - 1, y);
+ var leftSafetyDistance = safetyDistanceMap[x - 1, y];
+
+ // 使用两个位置中更安全的距离
+ var maxSafetyDistance = Math.Max(currentSafetyDistance, leftSafetyDistance);
+ var speedValue = CalculateSpeedBySafetyDistance(maxSafetyDistance);
+ var leftVelocity = Velocity.FromKilometersPerHour(speedValue);
+
+ // 统计速度分布
+ if (velocityStats.ContainsKey(speedValue))
+ velocityStats[speedValue]++;
+ else
+ velocityStats[speedValue] = 1;
+
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理leftPos时自然添加(如果leftPos也可通行)
+ grid.AddEdge(pos, leftPos, leftVelocity);
+ safetyConnections++;
+ }
+ }
+
+ // 🔥 连通性修复:上方向连接(基于安全距离)
+ if (y - 1 >= 0)
+ {
+ var topCell = gridMap.Cells[x, y - 1];
+ if (IsPassableWithHeight(topCell, vehicleHeight))
+ {
+ var topPos = new GridPosition(x, y - 1);
+ var topSafetyDistance = safetyDistanceMap[x, y - 1];
+
+ // 使用两个位置中更安全的距离
+ var maxSafetyDistance = Math.Max(currentSafetyDistance, topSafetyDistance);
+ var speedValue = CalculateSpeedBySafetyDistance(maxSafetyDistance);
+ var topVelocity = Velocity.FromKilometersPerHour(speedValue);
+
+ // 统计速度分布
+ if (velocityStats.ContainsKey(speedValue))
+ velocityStats[speedValue]++;
+ else
+ velocityStats[speedValue] = 1;
+
+ // 🔥 修复:只添加单向边,避免强制连接障碍物网格
+ // 反向边会在处理topPos时自然添加(如果topPos也可通行)
+ grid.AddEdge(pos, topPos, topVelocity);
safetyConnections++;
}
}
diff --git a/src/PathPlanning/ChannelBasedGridBuilder.cs b/src/PathPlanning/ChannelBasedGridBuilder.cs
index aea6ad7..0dc867f 100644
--- a/src/PathPlanning/ChannelBasedGridBuilder.cs
+++ b/src/PathPlanning/ChannelBasedGridBuilder.cs
@@ -167,7 +167,10 @@ namespace NavisworksTransport.PathPlanning
private void ProjectChannelToGrid(ModelItem channel, GridMap gridMap)
{
LogManager.Info($"[通道网格构建器] 投影通道: {channel.DisplayName}");
-
+
+ // 获取通道限速(一次性查询,避免重复COM API调用)
+ double channelSpeedLimit = CategoryAttributeManager.GetSpeedLimit(channel);
+
// 提取三角形几何
var triangles = GeometryHelper.ExtractTriangles(channel);
if (triangles.Count == 0)
@@ -175,17 +178,17 @@ namespace NavisworksTransport.PathPlanning
LogManager.Warning($"[通道网格构建器] 未能从 '{channel.DisplayName}' 提取到三角形");
return;
}
-
+
// 统一投影处理
int processedTriangles = 0;
foreach (var triangle in triangles)
{
var normal = ComputeTriangleNormal(triangle);
-
+
// 根据法向量决定处理方式
if (normal.Z > 0) // 朝上的面
{
- RasterizeTriangleToGrid(gridMap, triangle);
+ RasterizeTriangleToGrid(gridMap, triangle, channel, channelSpeedLimit);
processedTriangles++;
}
// 垂直面和朝下的面不投影
@@ -242,7 +245,9 @@ namespace NavisworksTransport.PathPlanning
///
/// 网格地图
/// 三角形
- private void RasterizeTriangleToGrid(GridMap gridMap, Triangle3D triangle)
+ /// 通道模型项
+ /// 预计算的通道限速
+ private void RasterizeTriangleToGrid(GridMap gridMap, Triangle3D triangle, ModelItem channel, double channelSpeedLimit)
{
// 获取三角形的2D投影边界框
var minX = Math.Min(triangle.Point1.X, Math.Min(triangle.Point2.X, triangle.Point3.X));
@@ -269,16 +274,12 @@ namespace NavisworksTransport.PathPlanning
// 计算网格中心点在三角形平面上的精确高度
double gridCenterHeight = GetHeightAtPoint(triangle, worldPos.X, worldPos.Y);
- // 使用SetCell方法正确创建通道网格并更新统计信息
- gridMap.SetCell(gridPos, true, 0, CategoryAttributeManager.LogisticsElementType.通道);
+ // 使用精确的世界坐标位置创建通道GridCell
+ var preciseWorldPosition = new Point3D(worldPos.X, worldPos.Y, gridCenterHeight);
+ var cell = GridCellBuilder.Channel(preciseWorldPosition, channel, channelSpeedLimit);
- // 然后更新特定的通道属性
- var updatedCell = gridMap.Cells[gridPos.X, gridPos.Y];
- updatedCell.WorldPosition = new Point3D(worldPos.X, worldPos.Y, gridCenterHeight);
- updatedCell.PassableHeight = new HeightInterval();
- updatedCell.ChannelType = ChannelType.Corridor;
- updatedCell.RelatedModelItem = null;
- gridMap.Cells[gridPos.X, gridPos.Y] = updatedCell;
+ // 一次性放置完整配置的GridCell
+ gridMap.PlaceCell(gridPos, cell);
}
}
}
diff --git a/src/PathPlanning/GridCellBuilder.cs b/src/PathPlanning/GridCellBuilder.cs
new file mode 100644
index 0000000..844561c
--- /dev/null
+++ b/src/PathPlanning/GridCellBuilder.cs
@@ -0,0 +1,151 @@
+using System;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.PathPlanning
+{
+ ///
+ /// GridCell工厂类,提供常用GridCell类型的便捷创建方法
+ /// 避免手动设置大量属性,减少错误,提高代码可读性
+ ///
+ public static class GridCellBuilder
+ {
+ ///
+ /// 创建通道类型的GridCell
+ ///
+ /// 世界坐标位置
+ /// 关联的通道ModelItem
+ /// 预计算的限速值
+ /// 遍历成本(0表示使用默认值)
+ /// 配置完整的通道GridCell
+ public static GridCell Channel(Point3D worldPosition, ModelItem channel, double speedLimit, double cost = 0)
+ {
+ return new GridCell
+ {
+ IsWalkable = true,
+ CellType = CategoryAttributeManager.LogisticsElementType.通道,
+ IsInChannel = true,
+ ChannelType = ChannelType.Corridor,
+ WorldPosition = worldPosition,
+ RelatedModelItem = channel,
+ SpeedLimit = speedLimit,
+ PassableHeight = new HeightInterval(),
+ Cost = cost // 会在PlaceCell时由ApplyCellValidationRules处理
+ };
+ }
+
+ ///
+ /// 创建门类型的GridCell
+ ///
+ /// 世界坐标位置
+ /// 关联的门ModelItem
+ /// 预计算的限速值
+ /// 遍历成本(0表示使用默认值)
+ /// 配置完整的门GridCell
+ public static GridCell Door(Point3D worldPosition, ModelItem door, double speedLimit, double cost = 0)
+ {
+ return new GridCell
+ {
+ IsWalkable = true,
+ CellType = CategoryAttributeManager.LogisticsElementType.门,
+ IsInChannel = false,
+ ChannelType = ChannelType.Other,
+ WorldPosition = worldPosition,
+ RelatedModelItem = door,
+ SpeedLimit = speedLimit,
+ PassableHeight = new HeightInterval(),
+ Cost = cost
+ };
+ }
+
+ ///
+ /// 创建障碍物类型的GridCell
+ ///
+ /// 世界坐标位置
+ /// 关联的障碍物ModelItem
+ /// 配置完整的障碍物GridCell
+ public static GridCell Obstacle(Point3D worldPosition, ModelItem obstacle)
+ {
+ return new GridCell
+ {
+ IsWalkable = false, // 障碍物永远不可通行
+ CellType = CategoryAttributeManager.LogisticsElementType.障碍物,
+ IsInChannel = false,
+ ChannelType = ChannelType.Other,
+ WorldPosition = worldPosition,
+ RelatedModelItem = obstacle,
+ SpeedLimit = 0,
+ PassableHeight = new HeightInterval(),
+ Cost = double.MaxValue // 障碍物成本最大
+ };
+ }
+
+ ///
+ /// 创建电梯类型的GridCell
+ ///
+ /// 世界坐标位置
+ /// 关联的电梯ModelItem
+ /// 预计算的限速值
+ /// 遍历成本(0表示使用默认值)
+ /// 配置完整的电梯GridCell
+ public static GridCell Elevator(Point3D worldPosition, ModelItem elevator, double speedLimit, double cost = 0)
+ {
+ return new GridCell
+ {
+ IsWalkable = true,
+ CellType = CategoryAttributeManager.LogisticsElementType.电梯,
+ IsInChannel = false,
+ ChannelType = ChannelType.Other,
+ WorldPosition = worldPosition,
+ RelatedModelItem = elevator,
+ SpeedLimit = speedLimit,
+ PassableHeight = new HeightInterval(),
+ Cost = cost
+ };
+ }
+
+ ///
+ /// 创建楼梯类型的GridCell
+ ///
+ /// 世界坐标位置
+ /// 关联的楼梯ModelItem
+ /// 预计算的限速值
+ /// 遍历成本(0表示使用默认值)
+ /// 配置完整的楼梯GridCell
+ public static GridCell Stairs(Point3D worldPosition, ModelItem stairs, double speedLimit, double cost = 0)
+ {
+ return new GridCell
+ {
+ IsWalkable = true,
+ CellType = CategoryAttributeManager.LogisticsElementType.楼梯,
+ IsInChannel = false,
+ ChannelType = ChannelType.Other,
+ WorldPosition = worldPosition,
+ RelatedModelItem = stairs,
+ SpeedLimit = speedLimit,
+ PassableHeight = new HeightInterval(),
+ Cost = cost
+ };
+ }
+
+ ///
+ /// 创建空白/未知类型的GridCell
+ ///
+ /// 世界坐标位置
+ /// 配置完整的空白GridCell
+ public static GridCell Empty(Point3D worldPosition)
+ {
+ return new GridCell
+ {
+ IsWalkable = false, // Unknown类型默认不可通行
+ CellType = CategoryAttributeManager.LogisticsElementType.Unknown,
+ IsInChannel = false,
+ ChannelType = ChannelType.Other,
+ WorldPosition = worldPosition,
+ RelatedModelItem = null,
+ SpeedLimit = 0,
+ PassableHeight = new HeightInterval(),
+ Cost = double.MaxValue
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PathPlanning/GridMap.cs b/src/PathPlanning/GridMap.cs
index fefd986..52ea4a5 100644
--- a/src/PathPlanning/GridMap.cs
+++ b/src/PathPlanning/GridMap.cs
@@ -24,7 +24,7 @@ namespace NavisworksTransport.PathPlanning
public int Height { get; set; }
///
- /// 每个网格单元格的实际尺寸(米)
+ /// 每个网格单元格的实际尺寸(模型单位)
///
public double CellSize { get; set; }
@@ -244,8 +244,9 @@ namespace NavisworksTransport.PathPlanning
/// 网格坐标
/// 是否可通行
/// 遍历成本
+ /// 限速
/// 单元格类型
- public void SetCell(GridPoint2D gridPosition, bool isWalkable, double cost, CategoryAttributeManager.LogisticsElementType cellType)
+ public void SetCell(GridPoint2D gridPosition, bool isWalkable, double cost, double speedLimit, CategoryAttributeManager.LogisticsElementType cellType)
{
if (!IsValidGridPosition(gridPosition))
return;
@@ -258,12 +259,44 @@ namespace NavisworksTransport.PathPlanning
IsInChannel = cellType == CategoryAttributeManager.LogisticsElementType.通道,
PassableHeight = new HeightInterval(),
ChannelType = cellType == CategoryAttributeManager.LogisticsElementType.通道 ? ChannelType.Corridor : ChannelType.Other,
- WorldPosition = worldPos
+ WorldPosition = worldPos,
+ SpeedLimit = speedLimit
};
-
+
+ // 应用验证规则并设置成本
+ ApplyCellValidationRules(ref cell, cost);
+
+ Cells[gridPosition.X, gridPosition.Y] = cell;
+ }
+
+ ///
+ /// 将完整配置的GridCell放置到指定位置
+ /// 这是推荐的GridCell设置方式,避免了分步设置的问题
+ ///
+ /// 网格位置
+ /// 完整配置的GridCell
+ public void PlaceCell(GridPoint2D gridPosition, GridCell cell)
+ {
+ if (!IsValidGridPosition(gridPosition))
+ return;
+
+ // 应用验证规则(确保Unknown和障碍物类型的一致性)
+ ApplyCellValidationRules(ref cell, cell.Cost);
+
+ Cells[gridPosition.X, gridPosition.Y] = cell;
+ }
+
+ ///
+ /// 应用GridCell验证规则
+ /// 确保Unknown和障碍物类型永远不可通行,并设置合适的成本
+ ///
+ /// 要验证的GridCell(通过引用传递以便修改)
+ /// 原始成本值
+ private void ApplyCellValidationRules(ref GridCell cell, double originalCost)
+ {
// 🔥 关键验证:确保Unknown和障碍物类型永远不可通行
- if (cellType == CategoryAttributeManager.LogisticsElementType.Unknown ||
- cellType == CategoryAttributeManager.LogisticsElementType.障碍物)
+ if (cell.CellType == CategoryAttributeManager.LogisticsElementType.Unknown ||
+ cell.CellType == CategoryAttributeManager.LogisticsElementType.障碍物)
{
cell.IsWalkable = false;
cell.Cost = double.MaxValue;
@@ -271,10 +304,8 @@ namespace NavisworksTransport.PathPlanning
else
{
// 优先使用传入的cost参数,如果为默认值则用GetCost
- cell.Cost = (cost == double.MaxValue || cost == 0) ? cell.GetCost() : cost;
+ cell.Cost = (originalCost == double.MaxValue || originalCost == 0) ? cell.GetCost() : originalCost;
}
-
- Cells[gridPosition.X, gridPosition.Y] = cell;
}
///
@@ -576,6 +607,10 @@ namespace NavisworksTransport.PathPlanning
/// 世界坐标位置 - 用于高度计算和空间查询
///
public Point3D WorldPosition { get; set; }
+ ///
+ /// 限速(米/秒),0表示未设置限速
+ ///
+ public double SpeedLimit { get; set; }
///
/// 根据物流类型获取通行成本
@@ -627,7 +662,8 @@ namespace NavisworksTransport.PathPlanning
CellType = CategoryAttributeManager.LogisticsElementType.障碍物,
IsInChannel = false,
PassableHeight = new HeightInterval(),
- ChannelType = ChannelType.Other
+ ChannelType = ChannelType.Other,
+ SpeedLimit = 0
};
cell.Cost = cell.GetCost();
return cell;
@@ -645,7 +681,8 @@ namespace NavisworksTransport.PathPlanning
CellType = CategoryAttributeManager.LogisticsElementType.楼板,
IsInChannel = false,
PassableHeight = new HeightInterval(),
- ChannelType = ChannelType.Other
+ ChannelType = ChannelType.Other,
+ SpeedLimit = 0
};
cell.Cost = cell.GetCost();
return cell;
@@ -664,7 +701,8 @@ namespace NavisworksTransport.PathPlanning
CellType = CategoryAttributeManager.LogisticsElementType.门,
IsInChannel = isOpen,
PassableHeight = new HeightInterval(),
- ChannelType = ChannelType.Other
+ ChannelType = ChannelType.Other,
+ SpeedLimit = 0
};
cell.Cost = cell.GetCost();
return cell;
@@ -683,7 +721,8 @@ namespace NavisworksTransport.PathPlanning
CellType = CategoryAttributeManager.LogisticsElementType.通道,
IsInChannel = true,
PassableHeight = new HeightInterval(),
- ChannelType = channelType
+ ChannelType = channelType,
+ SpeedLimit = 0
};
cell.Cost = cell.GetCost();
return cell;
diff --git a/src/PathPlanning/GridMapGenerator.cs b/src/PathPlanning/GridMapGenerator.cs
index 1bfa7c9..495f785 100644
--- a/src/PathPlanning/GridMapGenerator.cs
+++ b/src/PathPlanning/GridMapGenerator.cs
@@ -281,28 +281,32 @@ namespace NavisworksTransport.PathPlanning
// 获取门底部的Z坐标
double doorBottomZ = bbox.Min.Z;
+ // 获取门的限速
+ double doorSpeedLimit = CategoryAttributeManager.GetSpeedLimit(doorItem);
+ LogManager.Info($"[门网格限速] 门物品 '{doorItem.DisplayName}' 限速: {doorSpeedLimit:F2}m/s");
+
// 设置开口区域的网格为可通行的门类型
foreach (var (x, y) in coveredCells)
{
var gridPos = new GridPoint2D(x, y);
- // 设置门网格,cost传0让SetCell自动调用GetCost()计算(门的默认cost是1.2)
- gridMap.SetCell(gridPos, true, 0, CategoryAttributeManager.LogisticsElementType.门);
- // 由于GridCell是结构体,需要重新获取、修改、再设置回去
- var cell = gridMap.Cells[x, y];
-
- // 更新门网格的Z坐标为门底部位置
+ // 计算门网格的精确世界坐标
var world2D = gridMap.GridToWorld2D(gridPos);
- cell.WorldPosition = new Point3D(world2D.X, world2D.Y, doorBottomZ);
+ var preciseWorldPosition = new Point3D(world2D.X, world2D.Y, doorBottomZ);
- // 将门的高度范围存储在PassableHeights中供后续使用(使用相对坐标)
+ // 使用GridCellBuilder创建完整配置的门GridCell
+ var cell = GridCellBuilder.Door(preciseWorldPosition, doorItem, doorSpeedLimit);
+ LogManager.Info($"[门网格创建] 位置({preciseWorldPosition.X:F2},{preciseWorldPosition.Y:F2},{preciseWorldPosition.Z:F2}) 限速: {cell.SpeedLimit:F2}m/s");
+
+ // 设置门的高度范围
if (doorHeight > 0)
{
cell.PassableHeight = new HeightInterval(0, doorHeight);
}
- // 将修改后的结构体设置回数组
- gridMap.Cells[x, y] = cell;
+ // 一次性放置完整配置的GridCell
+ gridMap.PlaceCell(gridPos, cell);
+ LogManager.Info($"[门网格放置] 网格坐标({gridPos.X},{gridPos.Y}) 最终限速: {gridMap.GetCell(gridPos)?.SpeedLimit:F2}m/s");
}
processedCount++;
@@ -489,29 +493,46 @@ namespace NavisworksTransport.PathPlanning
if (interval.MaxZ > interval.MinZ)
{
// 通道区域有足够净空高度,设置为可通行
- // 🔥 关键修复:使用SetCell方法正确更新网格统计
var cellPos = new GridPoint2D(gridX, gridY);
- gridMap.SetCell(cellPos, true, 1.0, CategoryAttributeManager.LogisticsElementType.通道);
- // 设置世界坐标和高度信息
- var updatedCell = gridMap.Cells[gridX, gridY];
- updatedCell.WorldPosition = point;
- updatedCell.PassableHeight = interval;
- updatedCell.IsInChannel = true;
- gridMap.Cells[gridX, gridY] = updatedCell;
+ // 创建更新后的通道GridCell,保持原有属性但更新位置和高度
+ var updatedCell = new GridCell
+ {
+ IsWalkable = true,
+ CellType = CategoryAttributeManager.LogisticsElementType.通道,
+ IsInChannel = true,
+ ChannelType = ChannelType.Corridor,
+ WorldPosition = point,
+ RelatedModelItem = cell.RelatedModelItem, // 保持原有关联
+ SpeedLimit = cell.SpeedLimit, // 保持原有限速
+ PassableHeight = interval,
+ Cost = 1.0
+ };
+
+ // 放置更新的GridCell
+ gridMap.PlaceCell(cellPos, updatedCell);
}
else
{
// 通道区域但净空高度不足,标记为不可通行但保持通道类型
var cellPos = new GridPoint2D(gridX, gridY);
- gridMap.SetCell(cellPos, false, double.MaxValue, CategoryAttributeManager.LogisticsElementType.通道);
- // 设置世界坐标和高度信息
- var updatedCell = gridMap.Cells[gridX, gridY];
- updatedCell.WorldPosition = point;
- updatedCell.PassableHeight = interval;
- updatedCell.IsInChannel = true;
- gridMap.Cells[gridX, gridY] = updatedCell;
+ // 创建高度不足的通道GridCell,保持原有属性但设为不可通行
+ var updatedCell = new GridCell
+ {
+ IsWalkable = false,
+ CellType = CategoryAttributeManager.LogisticsElementType.通道,
+ IsInChannel = true,
+ ChannelType = ChannelType.Corridor,
+ WorldPosition = point,
+ RelatedModelItem = cell.RelatedModelItem, // 保持原有关联
+ SpeedLimit = cell.SpeedLimit, // 保持原有限速
+ PassableHeight = interval,
+ Cost = double.MaxValue
+ };
+
+ // 放置更新的GridCell
+ gridMap.PlaceCell(cellPos, updatedCell);
channelCellsBecomingUnwalkable++;
}
@@ -771,7 +792,10 @@ namespace NavisworksTransport.PathPlanning
continue;
}
- gridMap.SetCell(gridPos, false, double.MaxValue, CategoryAttributeManager.LogisticsElementType.障碍物);
+ // 创建障碍物GridCell
+ var worldPos = gridMap.GridToWorld3D(gridPos);
+ var obstacleCell = GridCellBuilder.Obstacle(worldPos, null);
+ gridMap.PlaceCell(gridPos, obstacleCell);
inflatedCells++;
}
else if (distanceMap[x, y] == double.MaxValue || distanceMap[x, y] > inflationRadius)
@@ -820,7 +844,10 @@ namespace NavisworksTransport.PathPlanning
var gridPos = new GridPoint2D(x, layer);
if (gridMap.IsWalkable(gridPos))
{
- gridMap.SetCell(gridPos, false, double.MaxValue, CategoryAttributeManager.LogisticsElementType.障碍物);
+ // 创建障碍物GridCell
+ var worldPos = gridMap.GridToWorld3D(gridPos);
+ var obstacleCell = GridCellBuilder.Obstacle(worldPos, null);
+ gridMap.PlaceCell(gridPos, obstacleCell);
layerInflatedCount++;
}
}
@@ -834,7 +861,10 @@ namespace NavisworksTransport.PathPlanning
var gridPos = new GridPoint2D(x, gridMap.Height - 1 - layer);
if (gridMap.IsWalkable(gridPos))
{
- gridMap.SetCell(gridPos, false, double.MaxValue, CategoryAttributeManager.LogisticsElementType.障碍物);
+ // 创建障碍物GridCell
+ var worldPos = gridMap.GridToWorld3D(gridPos);
+ var obstacleCell = GridCellBuilder.Obstacle(worldPos, null);
+ gridMap.PlaceCell(gridPos, obstacleCell);
layerInflatedCount++;
}
}
@@ -848,7 +878,10 @@ namespace NavisworksTransport.PathPlanning
var gridPos = new GridPoint2D(layer, y);
if (gridMap.IsWalkable(gridPos))
{
- gridMap.SetCell(gridPos, false, double.MaxValue, CategoryAttributeManager.LogisticsElementType.障碍物);
+ // 创建障碍物GridCell
+ var worldPos = gridMap.GridToWorld3D(gridPos);
+ var obstacleCell = GridCellBuilder.Obstacle(worldPos, null);
+ gridMap.PlaceCell(gridPos, obstacleCell);
layerInflatedCount++;
}
}
@@ -862,7 +895,10 @@ namespace NavisworksTransport.PathPlanning
var gridPos = new GridPoint2D(gridMap.Width - 1 - layer, y);
if (gridMap.IsWalkable(gridPos))
{
- gridMap.SetCell(gridPos, false, double.MaxValue, CategoryAttributeManager.LogisticsElementType.障碍物);
+ // 创建障碍物GridCell
+ var worldPos = gridMap.GridToWorld3D(gridPos);
+ var obstacleCell = GridCellBuilder.Obstacle(worldPos, null);
+ gridMap.PlaceCell(gridPos, obstacleCell);
layerInflatedCount++;
}
}
diff --git a/src/PathPlanning/PathOptimizer.cs b/src/PathPlanning/PathOptimizer.cs
index 9a5e40b..645dc0f 100644
--- a/src/PathPlanning/PathOptimizer.cs
+++ b/src/PathPlanning/PathOptimizer.cs
@@ -146,26 +146,29 @@ namespace NavisworksTransport.PathPlanning
{
if (points == null || points.Count < 3)
return points;
-
+
// 转换为网格坐标
var worldPath = points.Select(p => p.Position).ToList();
var gridPath = worldPath.Select(p => gridMap.WorldToGrid(p)).ToList();
-
- // 步骤1:先去除重复点
+
+ // 步骤1:先去除重复点,但保护限速边界
var dedupedGridPath = new List();
var dedupedPoints = new List();
for (int i = 0; i < gridPath.Count; i++)
{
- if (i == 0 || !gridPath[i].Equals(gridPath[i-1]))
+ // 🔥 关键修复:即使位置相同,如果限速不同也不能去重
+ bool shouldKeep = i == 0 || !gridPath[i].Equals(gridPath[i-1]) || HasSpeedLimitChange(points[i-1], points[i]);
+
+ if (shouldKeep)
{
dedupedGridPath.Add(gridPath[i]);
dedupedPoints.Add(points[i]);
}
}
-
+
// 步骤2:基于去重后的网格路径进行方向优化
var simplified = new List { dedupedGridPath[0] };
-
+
// 获取初始方向(归一化)
int initialDx = dedupedGridPath[1].X - dedupedGridPath[0].X;
int initialDy = dedupedGridPath[1].Y - dedupedGridPath[0].Y;
@@ -173,8 +176,8 @@ namespace NavisworksTransport.PathPlanning
initialDx == 0 ? 0 : Math.Sign(initialDx),
initialDy == 0 ? 0 : Math.Sign(initialDy)
);
-
- // 简化路径:只保留转弯点和高度变化点(使用归一化方向)
+
+ // 简化路径:只保留转弯点、高度变化点和限速变化点(使用归一化方向)
for (int i = 2; i < dedupedGridPath.Count; i++)
{
// 计算当前线段的位移
@@ -194,8 +197,15 @@ namespace NavisworksTransport.PathPlanning
dedupedPoints[i]
);
- // 只有真正的方向改变或高度变化时才保留转折点
- if (!currentDirection.Equals(prevDirection) || hasHeightChange)
+ // 🔥 关键修复:检查限速变化
+ bool hasSpeedLimitChange = HasSpeedLimitChange(
+ dedupedPoints[i-2],
+ dedupedPoints[i-1],
+ dedupedPoints[i]
+ );
+
+ // 保留转折点、高度变化点或限速变化点
+ if (!currentDirection.Equals(prevDirection) || hasHeightChange || hasSpeedLimitChange)
{
simplified.Add(dedupedGridPath[i - 1]);
prevDirection = currentDirection;
@@ -204,14 +214,18 @@ namespace NavisworksTransport.PathPlanning
{
LogManager.Debug($"[网格路径优化] 保留高度变化点 {i-1}");
}
+ if (hasSpeedLimitChange)
+ {
+ LogManager.Debug($"[网格路径优化] 保留限速变化点 {i-1}");
+ }
}
}
-
+
// 添加终点
simplified.Add(dedupedGridPath[dedupedGridPath.Count - 1]);
-
+
LogManager.Info($"[网格路径优化] 最终优化完成:{gridPath.Count} -> {dedupedGridPath.Count}(去重) -> {simplified.Count}(简化) 个点");
-
+
// 步骤3:转换回PathPoint格式
var simplifiedPoints = new List();
foreach (var gridPoint in simplified)
@@ -223,7 +237,7 @@ namespace NavisworksTransport.PathPlanning
simplifiedPoints.Add(dedupedPoints[originalIndex]);
}
}
-
+
return simplifiedPoints;
}
@@ -241,7 +255,7 @@ namespace NavisworksTransport.PathPlanning
int removedCount = 0;
- // 遍历中间点,只保留转折点
+ // 遍历中间点,只保留转折点和限速边界点
for (int i = 1; i < points.Count - 1; i++)
{
var prevPoint = points[i - 1];
@@ -251,14 +265,22 @@ namespace NavisworksTransport.PathPlanning
// 检查是否在同一直线上
bool isCollinear = IsCollinear(prevPoint.Position, currPoint.Position, nextPoint.Position);
- if (!isCollinear)
+ // 🔥 关键修复:检查限速变化,即使共线也不能移除限速边界点
+ bool hasSpeedLimitChange = HasSpeedLimitChange(prevPoint, currPoint, nextPoint);
+
+ if (!isCollinear || hasSpeedLimitChange)
{
- // 这是一个转折点,需要保留
+ // 这是一个转折点或限速边界点,需要保留
simplified.Add(currPoint);
+
+ if (hasSpeedLimitChange)
+ {
+ LogManager.Debug($"[共线简化] 保留限速边界点 {i}: {prevPoint.SpeedLimit} -> {currPoint.SpeedLimit} -> {nextPoint.SpeedLimit}");
+ }
}
else
{
- // 这是直线上的冗余点,跳过
+ // 这是直线上的冗余点且无限速变化,跳过
removedCount++;
}
}
@@ -314,6 +336,39 @@ namespace NavisworksTransport.PathPlanning
return HasHeightChange(point1.Position, point2.Position, point3.Position);
}
+ ///
+ /// 检查两个路径点之间是否有限速变化
+ /// 用于路径优化时判断是否需要保留中间点
+ ///
+ /// 第一个路径点
+ /// 第二个路径点
+ /// 如果有限速变化返回true,需要保留中间点
+ private bool HasSpeedLimitChange(PathPoint point1, PathPoint point2)
+ {
+ const double speedTolerance = 1e-6; // 仅覆盖浮点数精度误差
+ return Math.Abs(point1.SpeedLimit - point2.SpeedLimit) > speedTolerance;
+ }
+
+ ///
+ /// 检查三个点之间是否有限速变化(用于判断是否可以跳过中间点)
+ ///
+ /// 第一个路径点
+ /// 中间路径点
+ /// 第三个路径点
+ /// 如果有限速变化返回true,不能跳过中间点
+ private bool HasSpeedLimitChange(PathPoint point1, PathPoint point2, PathPoint point3)
+ {
+ const double speedTolerance = 1e-6; // 仅覆盖浮点数精度误差
+
+ // 检查任意两点间是否有限速变化
+ if (Math.Abs(point1.SpeedLimit - point2.SpeedLimit) > speedTolerance ||
+ Math.Abs(point2.SpeedLimit - point3.SpeedLimit) > speedTolerance)
+ {
+ return true;
+ }
+ return false;
+ }
+
///
/// 检查三点是否真正共线(只有当三点都在同一条直线上且方向一致时才返回true)
/// 🔥 关键修复:严格检查两个线段的方向是否一致,防止在转折点处错误合并
diff --git a/src/PathPlanning/TimeMarkerCalculationService.cs b/src/PathPlanning/TimeMarkerCalculationService.cs
new file mode 100644
index 0000000..e4d8e90
--- /dev/null
+++ b/src/PathPlanning/TimeMarkerCalculationService.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Autodesk.Navisworks.Api;
+using NavisworksTransport.Utils;
+
+namespace NavisworksTransport.PathPlanning
+{
+ ///
+ /// 时标计算服务
+ /// 基于路径点的限速和距离计算各路段的时间数据
+ ///
+ public class TimeMarkerCalculationService
+ {
+ ///
+ /// 路段时间数据
+ ///
+ public class SegmentTimeData
+ {
+ ///
+ /// 起点名称
+ ///
+ public string StartPointName { get; set; }
+
+ ///
+ /// 终点名称
+ ///
+ public string EndPointName { get; set; }
+
+ ///
+ /// 路段距离(米)
+ ///
+ public double Distance { get; set; }
+
+ ///
+ /// 限速(米/秒)
+ ///
+ public double SpeedLimit { get; set; }
+
+ ///
+ /// 路段时间(秒)
+ ///
+ public double Time { get; set; }
+
+ ///
+ /// 累计时间(秒)
+ ///
+ public double CumulativeTime { get; set; }
+
+ ///
+ /// 是否有效(有限速设置)
+ ///
+ public bool IsValid { get; set; }
+ }
+
+ ///
+ /// 时标计算结果
+ ///
+ public class TimeMarkerResult
+ {
+ ///
+ /// 路段时间数据列表
+ ///
+ public List Segments { get; set; } = new List();
+
+ ///
+ /// 总距离(米)
+ ///
+ public double TotalDistance { get; set; }
+
+ ///
+ /// 总时间(秒)
+ ///
+ public double TotalTime { get; set; }
+
+ ///
+ /// 有效路段数量(有限速设置的路段)
+ ///
+ public int ValidSegmentCount { get; set; }
+
+ ///
+ /// 是否计算成功
+ ///
+ public bool IsSuccessful { get; set; }
+
+ ///
+ /// 错误信息
+ ///
+ public string ErrorMessage { get; set; }
+ }
+
+ ///
+ /// 计算路径的时标数据
+ ///
+ /// 路径
+ /// 时标计算结果
+ public TimeMarkerResult CalculateTimeMarkers(PathRoute route)
+ {
+ var result = new TimeMarkerResult();
+
+ try
+ {
+ if (route == null)
+ {
+ result.ErrorMessage = "路径不能为空";
+ return result;
+ }
+
+ if (route.Points.Count < 2)
+ {
+ result.ErrorMessage = "路径必须包含至少2个点";
+ return result;
+ }
+
+ LogManager.Info($"[时标计算] 开始计算路径 '{route.Name}' 的时标,包含 {route.Points.Count} 个点");
+
+ double cumulativeTime = 0;
+ double totalDistance = 0;
+ int validSegments = 0;
+
+ // 遍历相邻的路径点对,计算每段的时间
+ for (int i = 0; i < route.Points.Count - 1; i++)
+ {
+ var startPoint = route.Points[i];
+ var endPoint = route.Points[i + 1];
+
+ // 计算路段距离
+ var distance = CalculateDistance(startPoint.Position, endPoint.Position);
+ totalDistance += distance;
+
+ // 确定该路段的限速
+ var speedLimit = DetermineSegmentSpeedLimit(startPoint, endPoint);
+
+ var segmentData = new SegmentTimeData
+ {
+ StartPointName = startPoint.Name,
+ EndPointName = endPoint.Name,
+ Distance = distance,
+ SpeedLimit = speedLimit,
+ IsValid = speedLimit > 0
+ };
+
+ // 计算路段时间
+ if (segmentData.IsValid)
+ {
+ segmentData.Time = distance / speedLimit;
+ cumulativeTime += segmentData.Time;
+ validSegments++;
+ }
+ else
+ {
+ segmentData.Time = 0;
+ LogManager.Warning($"[时标计算] 路段 {startPoint.Name}->{endPoint.Name} 无限速设置,跳过时间计算");
+ }
+
+ segmentData.CumulativeTime = cumulativeTime;
+ result.Segments.Add(segmentData);
+
+ LogManager.Debug($"[时标计算] 路段 {i+1}: {startPoint.Name}->{endPoint.Name}, 距离:{distance:F2}m, 限速:{speedLimit:F2}m/s, 时间:{segmentData.Time:F2}s");
+ }
+
+ // 设置总计结果
+ result.TotalDistance = totalDistance;
+ result.TotalTime = cumulativeTime;
+ result.ValidSegmentCount = validSegments;
+ result.IsSuccessful = validSegments > 0;
+
+ if (result.IsSuccessful)
+ {
+ LogManager.Info($"[时标计算] 计算完成: 总距离 {totalDistance:F2}m, 总时间 {cumulativeTime:F2}s, 有效路段 {validSegments}/{result.Segments.Count}");
+ }
+ else
+ {
+ result.ErrorMessage = "没有找到任何有效的限速设置,无法计算时间";
+ LogManager.Warning($"[时标计算] {result.ErrorMessage}");
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ result.ErrorMessage = $"计算时标时发生错误: {ex.Message}";
+ LogManager.Error($"[时标计算] {result.ErrorMessage}", ex);
+ return result;
+ }
+ }
+
+ ///
+ /// 确定路段的限速
+ /// 使用起点和终点限速的最小值作为路段限速
+ ///
+ /// 起点
+ /// 终点
+ /// 路段限速(米/秒)
+ private double DetermineSegmentSpeedLimit(PathPoint startPoint, PathPoint endPoint)
+ {
+ // 如果起点或终点任一没有限速设置,返回0
+ if (startPoint.SpeedLimit <= 0 || endPoint.SpeedLimit <= 0)
+ {
+ return 0;
+ }
+
+ // 使用两点限速的最小值作为路段限速(更安全的策略)
+ return Math.Min(startPoint.SpeedLimit, endPoint.SpeedLimit);
+ }
+
+ ///
+ /// 计算两点间距离
+ ///
+ /// 第一个点
+ /// 第二个点
+ /// 距离(米)
+ private double CalculateDistance(Point3D point1, Point3D point2)
+ {
+ var dx = point1.X - point2.X;
+ var dy = point1.Y - point2.Y;
+ var dz = point1.Z - point2.Z;
+ var distanceInModelUnits = Math.Sqrt(dx * dx + dy * dy + dz * dz);
+ return UnitsConverter.ConvertToMeters(distanceInModelUnits);
+ }
+
+ ///
+ /// 格式化时间为可读字符串
+ ///
+ /// 时间(秒)
+ /// 格式化的时间字符串
+ public static string FormatTime(double timeInSeconds)
+ {
+ if (timeInSeconds <= 0)
+ return "0秒";
+
+ var timeSpan = TimeSpan.FromSeconds(timeInSeconds);
+
+ if (timeSpan.TotalHours >= 1)
+ {
+ return $"{timeSpan.Hours}小时{timeSpan.Minutes}分{timeSpan.Seconds}秒";
+ }
+ else if (timeSpan.TotalMinutes >= 1)
+ {
+ return $"{timeSpan.Minutes}分{timeSpan.Seconds}秒";
+ }
+ else
+ {
+ return $"{timeSpan.TotalSeconds:F1}秒";
+ }
+ }
+
+ ///
+ /// 生成时标计算报告
+ ///
+ /// 计算结果
+ /// 报告字符串
+ public static string GenerateReport(TimeMarkerResult result)
+ {
+ if (result == null || !result.IsSuccessful)
+ {
+ return $"时标计算失败: {result?.ErrorMessage ?? "未知错误"}";
+ }
+
+ var report = "=== 路径时标计算报告 ===\n";
+ report += $"总距离: {result.TotalDistance:F2}米\n";
+ report += $"总时间: {FormatTime(result.TotalTime)}\n";
+ report += $"有效路段: {result.ValidSegmentCount}/{result.Segments.Count}\n\n";
+
+ report += "路段详情:\n";
+ for (int i = 0; i < result.Segments.Count; i++)
+ {
+ var segment = result.Segments[i];
+ var status = segment.IsValid ? "" : " (无限速)";
+ report += $"{i + 1}. {segment.StartPointName} -> {segment.EndPointName}\n";
+ report += $" 距离: {segment.Distance:F2}m, 限速: {segment.SpeedLimit:F2}m/s, 时间: {FormatTime(segment.Time)}{status}\n";
+ report += $" 累计时间: {FormatTime(segment.CumulativeTime)}\n\n";
+ }
+
+ return report;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/UI/WPF/ViewModels/TimeTagViewModel.cs b/src/UI/WPF/ViewModels/TimeTagViewModel.cs
index 1a98f84..4b295e6 100644
--- a/src/UI/WPF/ViewModels/TimeTagViewModel.cs
+++ b/src/UI/WPF/ViewModels/TimeTagViewModel.cs
@@ -1,10 +1,9 @@
using System;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
-using NavisworksTransport.Utils;
+using NavisworksTransport.PathPlanning;
namespace NavisworksTransport.UI.WPF.ViewModels
{
@@ -15,13 +14,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public class TimeTagViewModel : INotifyPropertyChanged
{
#region 字段
-
+
private double _defaultSpeed = 0.8;
private bool _useElementSpeedLimit = true;
private bool _isCalculating = false;
private double _totalDuration = 24.8;
private string _selectedRoute = "Route_A1";
-
+ private readonly TimeMarkerCalculationService _timeCalculationService;
+ private PathPlanningManager _pathPlanningManager;
+
#endregion
#region 属性
@@ -122,8 +123,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- InitializeCollections();
- LoadDemoData();
+ _timeCalculationService = new TimeMarkerCalculationService();
+ _pathPlanningManager = PathPlanningManager.GetActivePathManager();
+
+ InitializeCollections();
+ LoadRouteData(_pathPlanningManager.CurrentRoute);
+
LogManager.Info("TimeTagViewModel初始化完成");
}
catch (Exception ex)
@@ -145,6 +150,104 @@ namespace NavisworksTransport.UI.WPF.ViewModels
TimelineItems = new ObservableCollection();
}
+ ///
+ /// 从真实路径数据加载时间分段
+ ///
+ /// 路径数据
+ private void LoadRouteData(PathRoute route)
+ {
+ try
+ {
+ if (route == null)
+ {
+ LogManager.Info("路径为空,加载演示数据");
+ LoadDemoData();
+ return;
+ }
+
+ LogManager.Info($"开始从路径 '{route.Name}' 加载真实时间数据");
+
+ // 使用TimeMarkerCalculationService计算时间分段
+ var result = _timeCalculationService.CalculateTimeMarkers(route);
+
+ if (!result.IsSuccessful)
+ {
+ LogManager.Warning($"时标计算失败: {result.ErrorMessage},回退到演示数据");
+ LoadDemoData();
+ return;
+ }
+
+ // 清空现有数据
+ TimeSegments.Clear();
+ TimelineItems.Clear();
+
+ // 添加路径分段数据
+ for (int i = 0; i < result.Segments.Count; i++)
+ {
+ var segment = result.Segments[i];
+ var segmentItem = new TimeSegmentItem
+ {
+ Index = i + 1,
+ Length = segment.Distance,
+ Speed = segment.SpeedLimit,
+ Duration = segment.Time,
+ CumulativeTime = segment.CumulativeTime,
+ Remarks = segment.IsValid ? $"{segment.StartPointName}->{segment.EndPointName}" : "无限速"
+ };
+ TimeSegments.Add(segmentItem);
+ }
+
+ // 更新总时长(使用实际计算时间,不添加额外等待)
+ TotalDuration = result.TotalTime;
+
+ // 更新选中路径名称
+ SelectedRoute = route.Name;
+
+ // 生成时间轴预览数据
+ GenerateTimelineItems();
+
+ LogManager.Info($"真实时间数据加载完成: 总距离 {result.TotalDistance:F2}m, 总时间 {result.TotalTime:F2}s");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"加载路径时间数据失败: {ex.Message}", ex);
+ // 加载失败时回退到演示数据
+ LoadDemoData();
+ }
+ }
+
+ ///
+ /// 生成时间轴预览数据
+ ///
+ private void GenerateTimelineItems()
+ {
+ try
+ {
+ TimelineItems.Clear();
+
+ if (TotalDuration <= 0) return;
+
+ // 为每个真实路径分段生成时间轴项目(只处理真实的路径段,不包括等待时间)
+ foreach (var segment in TimeSegments.Where(s => s.Index > 0))
+ {
+ var timelineItem = new TimelinePreviewItem
+ {
+ Name = segment.Remarks, // 使用真实的路径点名称(如"自动起点->自动点1")
+ Duration = segment.Duration,
+ Color = segment.Index == 1 ? "#FF2B579A" : "#FFFF9800", // 第一段蓝色,其他橙色
+ Percentage = TotalDuration > 0 ? segment.Duration / TotalDuration * 100 : 0
+ };
+ TimelineItems.Add(timelineItem);
+ }
+
+ LogManager.Info($"时间轴预览数据生成完成,包含 {TimelineItems.Count} 个项目");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"生成时间轴预览数据失败: {ex.Message}", ex);
+ }
+ }
+
///
/// 加载演示数据
///
@@ -173,40 +276,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Remarks = "拐角减速"
});
- TimeSegments.Add(new TimeSegmentItem
- {
- Index = 0, // 等待事件用0表示
- Length = 0.00,
- Speed = 0.00,
- Duration = 2.0,
- CumulativeTime = 24.8,
- Remarks = "候梯/开门"
- });
+ // 移除硬编码的等待时间,使用真实的路径数据
- // 加载时间轴预览数据
- TimelineItems.Add(new TimelinePreviewItem
- {
- Name = "段1",
- Duration = 12.1,
- Color = "#FF2B579A", // 蓝色
- Percentage = 12.1 / 24.8 * 100
- });
-
- TimelineItems.Add(new TimelinePreviewItem
- {
- Name = "段2",
- Duration = 10.7,
- Color = "#FFFF9800", // 橙色
- Percentage = 10.7 / 24.8 * 100
- });
-
- TimelineItems.Add(new TimelinePreviewItem
- {
- Name = "等待",
- Duration = 2.0,
- Color = "#FF9C27B0", // 紫色
- Percentage = 2.0 / 24.8 * 100
- });
+ // 更新总时长并生成时间轴预览数据
+ TotalDuration = 22.8; // 只计算实际路径时间
+ GenerateTimelineItems();
LogManager.Info("时间标签演示数据加载完成");
}
@@ -229,39 +303,44 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
IsCalculating = true;
- // 模拟计算过程
- System.Threading.Tasks.Task.Delay(1000).ContinueWith(t =>
+ // 检查是否有真实路径数据
+ if (_pathPlanningManager?.CurrentRoute != null)
{
- // 根据新的速度设置重新计算各段时间
- double cumulativeTime = 0;
-
- foreach (var segment in TimeSegments.Where(s => s.Index > 0))
+ // 使用真实路径数据重新计算
+ LogManager.Info("使用真实路径数据重新计算时间分段");
+
+ // 异步计算以避免UI阻塞
+ System.Threading.Tasks.Task.Run(() =>
{
- // 如果使用元素限速,保持原速度;否则使用默认速度
- if (!UseElementSpeedLimit)
+ try
{
- segment.Speed = DefaultSpeed;
+ var result = _timeCalculationService.CalculateTimeMarkers(_pathPlanningManager.CurrentRoute);
+
+ // 在UI线程上更新数据
+ System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ {
+ if (result.IsSuccessful)
+ {
+ UpdateTimeSegmentsFromCalculation(result);
+ }
+ else
+ {
+ LogManager.Warning($"重新计算失败: {result.ErrorMessage}");
+ }
+
+ IsCalculating = false;
+ });
}
-
- // 重新计算段时间
- segment.Duration = segment.Length / segment.Speed;
- cumulativeTime += segment.Duration;
- segment.CumulativeTime = cumulativeTime;
- }
-
- // 加上等待时间
- var waitingSegment = TimeSegments.FirstOrDefault(s => s.Index == 0);
- if (waitingSegment != null)
- {
- cumulativeTime += waitingSegment.Duration;
- waitingSegment.CumulativeTime = cumulativeTime;
- }
-
- TotalDuration = cumulativeTime;
- IsCalculating = false;
-
- LogManager.Info($"重新计算完成,总时长: {TotalDuration:F1}秒");
- });
+ catch (Exception ex)
+ {
+ LogManager.Error($"重新计算时间分段异常: {ex.Message}", ex);
+ System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ {
+ IsCalculating = false;
+ });
+ }
+ });
+ }
}
catch (Exception ex)
{
@@ -270,6 +349,55 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ ///
+ /// 从计算结果更新时间分段数据
+ ///
+ /// 计算结果
+ private void UpdateTimeSegmentsFromCalculation(TimeMarkerCalculationService.TimeMarkerResult result)
+ {
+ try
+ {
+ // 清空现有数据
+ TimeSegments.Clear();
+
+ // 添加路径分段数据
+ for (int i = 0; i < result.Segments.Count; i++)
+ {
+ var segment = result.Segments[i];
+ var segmentItem = new TimeSegmentItem
+ {
+ Index = i + 1,
+ Length = segment.Distance,
+ Speed = UseElementSpeedLimit ? segment.SpeedLimit : DefaultSpeed,
+ Duration = UseElementSpeedLimit ? segment.Time : (segment.Distance / DefaultSpeed),
+ CumulativeTime = 0, // 稍后重新计算累计时间
+ Remarks = segment.IsValid ? $"{segment.StartPointName}->{segment.EndPointName}" : "无限速"
+ };
+ TimeSegments.Add(segmentItem);
+ }
+
+ // 重新计算累计时间
+ double cumulativeTime = 0;
+ foreach (var segment in TimeSegments.Where(s => s.Index > 0))
+ {
+ cumulativeTime += segment.Duration;
+ segment.CumulativeTime = cumulativeTime;
+ }
+
+ // 更新总时长(使用实际计算时间,不添加额外等待)
+ TotalDuration = cumulativeTime;
+
+ // 重新生成时间轴预览
+ GenerateTimelineItems();
+
+ LogManager.Info($"时间分段数据更新完成: 总时长 {TotalDuration:F1}秒");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"更新时间分段数据失败: {ex.Message}", ex);
+ }
+ }
+
///
/// 应用时标到路径点
///
diff --git a/src/UI/WPF/Views/TimeTagDialog.xaml b/src/UI/WPF/Views/TimeTagDialog.xaml
index 39a6555..47e951d 100644
--- a/src/UI/WPF/Views/TimeTagDialog.xaml
+++ b/src/UI/WPF/Views/TimeTagDialog.xaml
@@ -175,7 +175,7 @@ NavisworksTransport 时间标签设置对话框 - 采用与主界面一致的Nav
HorizontalGridLinesBrush="#FFEEEEEE"
RowBackground="White"
AlternatingRowBackground="#FFF8FBFF"
- Height="300"
+ Height="240"
ItemsSource="{Binding TimeSegments}">
@@ -191,53 +191,31 @@ NavisworksTransport 时间标签设置对话框 - 采用与主界面一致的Nav
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+