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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - +