测试和修改A*返回的坐标转换的问题

This commit is contained in:
tian 2025-09-29 23:25:21 +08:00
parent 95bf6e839b
commit 3f2d66c255
21 changed files with 2657 additions and 262 deletions

View File

@ -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": [

57
AStarTestRunner.csproj Normal file
View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{F2B4A8C1-2DEF-4765-8392-E67AD8372FG2}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TestRunner</RootNamespace>
<AssemblyName>AStarTestRunner</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<!-- Roy_T.AStar for A* algorithm testing -->
<Reference Include="Roy-T.AStar">
<HintPath>packages\RoyT.AStar.2.1.0\lib\netstandard2.0\Roy-T.AStar.dll</HintPath>
<Private>True</Private>
</Reference>
<!-- 引用单元测试DLL -->
<Reference Include="NavisworksTransport.UnitTests">
<HintPath>bin\Debug\NavisworksTransport.UnitTests.dll</HintPath>
<Private>True</Private>
</Reference>
<!-- 引用主项目DLL -->
<Reference Include="NavisworksTransportPlugin">
<HintPath>bin\Debug\NavisworksTransportPlugin.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="TestRunner.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -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")

View File

@ -73,25 +73,24 @@
<HintPath>bin\Debug\NavisworksTransportPlugin.dll</HintPath>
<Private>True</Private>
</Reference>
<!-- Roy_T.AStar for A* algorithm testing -->
<Reference Include="Roy-T.AStar">
<HintPath>packages\RoyT.AStar.2.1.0\lib\netstandard2.0\Roy-T.AStar.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<!-- 原有核心测试类 - 纯逻辑测试可脱离Navisworks和UI环境运行 -->
<Compile Include="UnitTests\Core\UIStateManagerBasicTests.cs" />
<Compile Include="UnitTests\Collections\ThreadSafeObservableCollectionBasicTests.cs" />
<!-- 新增命令框架测试 - 纯逻辑测试 -->
<Compile Include="tests\Commands\CommandBaseTests.cs" />
<Compile Include="tests\Commands\PathPlanningResultTests.cs" />
<Compile Include="tests\Commands\IPathPlanningCommandTests.cs" />
<!-- 新增核心数据模型测试 - 纯逻辑测试 -->
<Compile Include="tests\Core\MapPoint2DTests.cs" />
<Compile Include="tests\Core\EnumValidationTests.cs" />
<!-- A*算法问题检测测试 -->
<Compile Include="NavisworksTransport.UnitTests\AStarDebuggingTest.cs" />
<!-- 测试辅助类 -->
<Compile Include="UnitTests\TestHelpers\TestViewModel.cs" />
<!-- Assembly Info -->
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" />
</ItemGroup>

View File

@ -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
{
/// <summary>
/// 简化的3D点结构体用于测试
/// </summary>
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})";
}
/// <summary>
/// 简化的2D点结构体用于测试
/// </summary>
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})";
}
/// <summary>
/// 简化的网格点结构体(用于测试)
/// </summary>
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})";
}
/// <summary>
/// 简化的边界框结构体(用于测试)
/// </summary>
public struct BoundingBox3D
{
public Point3D Min { get; set; }
public Point3D Max { get; set; }
public BoundingBox3D(Point3D min, Point3D max)
{
Min = min;
Max = max;
}
}
/// <summary>
/// A*算法问题检测测试用例
/// 专门用于调试A*路径穿越障碍物网格的问题
/// </summary>
[TestClass]
public class AStarDebuggingTest
{
/// <summary>
/// 基础障碍物避让测试
/// 创建一个3x3网格中间设置障碍物测试A*是否能正确绕过
/// </summary>
[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<GridPosition> { 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})");
}
/// <summary>
/// 坐标映射验证测试
/// 验证GridPosition和Node.Position之间的对应关系
/// </summary>
[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}");
}
}
/// <summary>
/// 实际问题场景重现测试
/// 重现日志中出现的49,25到62,25障碍物路径问题
/// </summary>
[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<GridPosition>();
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<GridPosition> { 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<GridPosition>();
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类型的路径");
}
}
/// <summary>
/// 网格连接状态详细检查测试
/// 验证断开节点后,相邻节点的连接状态
/// </summary>
[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})不应该连接到已断开的中心节点");
}
}
/// <summary>
/// 使用真实坐标系统的A*算法测试
/// 集成实际系统的坐标转换方法,以复现真实环境中的问题
/// </summary>
[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);
}
/// <summary>
/// 通道路径查找测试
/// 创建一个有明确通道的场景验证A*返回的路径坐标是否正确
/// </summary>
[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<GridPosition>();
// 上下墙
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<GridPosition> { 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<GridPosition>();
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*算法返回的坐标转换正确");
}
}
/// <summary>
/// 创建模拟的GridMap使用实际系统的参数
/// </summary>
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;
}
/// <summary>
/// 从GridMap创建对应的Roy_T.AStar网格
/// </summary>
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);
}
/// <summary>
/// 在网格中设置障碍物
/// </summary>
private void SetupObstaclesInMockGridMap(MockGridMap gridMap, Grid astarGrid)
{
// 模拟实际问题场景:在路径中间设置障碍物
var obstaclePositions = new List<GridPoint2D>();
// 设置从网格(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}");
}
}
}
/// <summary>
/// 执行真实的坐标转换和路径查找
/// </summary>
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<Point3D>(),
OriginalEndPoint = endWorld,
IsSuccessful = false
};
}
/// <summary>
/// 转换A*路径为世界坐标模拟AutoPathFinder的逻辑
/// </summary>
private List<Point3D> ConvertAStarPathToWorldCoordinates(Path astarPath, MockGridMap gridMap)
{
var worldPath = new List<Point3D>();
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;
}
/// <summary>
/// 验证真实坐标系统下的路径结果
/// </summary>
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<GridPoint2D>();
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<GridPoint2D>();
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("✅ 路径正确避开了所有障碍物");
}
}
/// <summary>
/// 模拟路径查找结果类
/// </summary>
private class PathFindingResult
{
public List<Point3D> PathPoints { get; set; } = new List<Point3D>();
public Point3D OriginalEndPoint { get; set; }
public bool IsSuccessful { get; set; }
}
/// <summary>
/// 模拟UnitsConverter类用于测试
/// </summary>
private static class MockUnitsConverter
{
/// <summary>
/// 模拟:将距离从文档单位转换为米
/// 假设文档单位就是米转换因子为1.0
/// </summary>
public static double ConvertToMeters(double distance)
{
return distance * 1.0; // 假设文档单位就是米
}
}
/// <summary>
/// 模拟GridMap类用于测试
/// </summary>
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 };
}
}
}
/// <summary>
/// 世界坐标转换为网格坐标
/// </summary>
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));
}
/// <summary>
/// 网格坐标转换为世界2D坐标网格左下角
/// </summary>
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);
}
/// <summary>
/// 检查网格坐标是否有效
/// </summary>
public bool IsValidGridPosition(GridPoint2D gridPosition)
{
return gridPosition.X >= 0 && gridPosition.X < Width &&
gridPosition.Y >= 0 && gridPosition.Y < Height;
}
/// <summary>
/// 设置网格单元
/// </summary>
public void SetCell(GridPoint2D gridPosition, bool isWalkable)
{
if (IsValidGridPosition(gridPosition))
{
cells[gridPosition.X, gridPosition.Y].IsWalkable = isWalkable;
}
}
/// <summary>
/// 获取网格单元
/// </summary>
public MockGridCell GetCell(GridPoint2D gridPosition)
{
if (IsValidGridPosition(gridPosition))
{
return cells[gridPosition.X, gridPosition.Y];
}
return new MockGridCell { IsWalkable = false };
}
}
/// <summary>
/// 模拟GridCell类用于测试
/// </summary>
private class MockGridCell
{
public bool IsWalkable { get; set; }
}
}
}

View File

@ -155,6 +155,7 @@
<!-- PathPlanning - Auto Path Planning -->
<Compile Include="src\PathPlanning\GridMap.cs" />
<Compile Include="src\PathPlanning\GridMapGenerator.cs" />
<Compile Include="src\PathPlanning\GridCellBuilder.cs" />
<Compile Include="src\PathPlanning\AutoPathFinder.cs" />
<Compile Include="src\PathPlanning\GridPoint2D.cs" />
<Compile Include="src\PathPlanning\AutoPathPlanningValidationResult.cs" />
@ -164,6 +165,7 @@
<Compile Include="src\PathPlanning\ChannelBasedGridBuilder.cs" />
<Compile Include="src\PathPlanning\VerticalScanProcessor.cs" />
<Compile Include="src\PathPlanning\PathOptimizer.cs" />
<Compile Include="src\PathPlanning\TimeMarkerCalculationService.cs" />
<Compile Include="src\PathPlanning\GridMapCacheKey.cs" />
<Compile Include="src\PathPlanning\GridMapCache.cs" />

86
TestRunner.cs Normal file
View File

@ -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();
}
}
}

View File

@ -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
<package id="FFMpegCore" version="5.2.0" targetFramework="net47" />
<package id="Instances" version="3.0.1" targetFramework="net47" />
<package id="System.Text.Json" version="9.0.2" targetFramework="net47" />
```
## 核心代码实现
### 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<Bitmap> _capturedFrames;
private bool _isCapturing = false;
private VideoExportSettings _currentSettings;
public void StartVideoCapture(VideoExportSettings settings)
{
_currentSettings = settings;
_capturedFrames = new List<Bitmap>();
_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<IVideoFrame> 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.dllNuGet自动处理
├── Instances.dll依赖项
├── System.Text.Json.dll依赖项
└── ffmpeg/
├── ffmpeg.exeWindows 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<ProgressEventArgs> 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天
- **阶段4UI优化**0.5天)
- **总计3.5-4.5天**
## 总结
FFMpegCore方案虽然需要分发额外的二进制文件但提供了最完整和专业的视频导出功能。通过合理的架构设计和错误处理可以为用户提供高质量的导航视频导出体验。
**推荐实施顺序:**
1. 先实现基础的帧捕获功能
2. 集成FFMpegCore进行视频编码测试
3. 完善UI和用户体验
4. 优化性能和错误处理
这个方案能够满足专业用户对视频质量和格式的要求,是长期来看最具价值的技术选择。

View File

@ -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
}
}
/// <summary>
/// 根据世界坐标获取该位置的限速
/// </summary>
/// <param name="worldPosition">世界坐标</param>
/// <returns>限速值(米/秒0表示未设置限速</returns>
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;
}
}
/// <summary>
/// 生成自动路径点名称
/// </summary>

View File

@ -263,6 +263,10 @@ namespace NavisworksTransport
/// 备注信息
/// </summary>
public string Notes { get; set; }
/// <summary>
/// 限速(米/秒0表示未设置限速
/// </summary>
public double SpeedLimit { get; set; }
/// <summary>
/// 构造函数
@ -276,6 +280,7 @@ namespace NavisworksTransport
CreatedTime = DateTime.Now;
Index = 0;
Notes = string.Empty;
SpeedLimit = 0;
}
/// <summary>
@ -293,6 +298,7 @@ namespace NavisworksTransport
CreatedTime = DateTime.Now;
Index = 0;
Notes = string.Empty;
SpeedLimit = 0;
}
/// <summary>

View File

@ -592,11 +592,38 @@ namespace NavisworksTransport
public static string GetLogisticsPropertyValueViaCom(ModelItem item, string propertyName)
{
return NavisworksComPropertyManager.GetPropertyValue(
item,
LogisticsCategories.LOGISTICS,
item,
LogisticsCategories.LOGISTICS,
propertyName);
}
/// <summary>
/// 获取模型项的速度限制
/// </summary>
/// <param name="item">模型项</param>
/// <returns>速度限制(米/秒如果未设置返回0</returns>
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;
}
}
/// <summary>
/// 通用方法:获取模型项的指定属性值(不限于物流属性)
/// </summary>
@ -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)
{

View File

@ -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;
}
}

View File

@ -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;
}
/// <summary>
/// 将A*返回的米坐标转换为网格坐标(统一方法)
/// </summary>
/// <param name="astarPosition">A*位置(米坐标)</param>
/// <param name="gridMap">网格地图</param>
/// <returns>网格坐标</returns>
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);
}
/// <summary>
/// 执行A*算法的核心逻辑
/// </summary>
@ -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++;
}
}

View File

@ -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
/// </summary>
/// <param name="gridMap">网格地图</param>
/// <param name="triangle">三角形</param>
private void RasterizeTriangleToGrid(GridMap gridMap, Triangle3D triangle)
/// <param name="channel">通道模型项</param>
/// <param name="channelSpeedLimit">预计算的通道限速</param>
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);
}
}
}

View File

@ -0,0 +1,151 @@
using System;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.PathPlanning
{
/// <summary>
/// GridCell工厂类提供常用GridCell类型的便捷创建方法
/// 避免手动设置大量属性,减少错误,提高代码可读性
/// </summary>
public static class GridCellBuilder
{
/// <summary>
/// 创建通道类型的GridCell
/// </summary>
/// <param name="worldPosition">世界坐标位置</param>
/// <param name="channel">关联的通道ModelItem</param>
/// <param name="speedLimit">预计算的限速值</param>
/// <param name="cost">遍历成本0表示使用默认值</param>
/// <returns>配置完整的通道GridCell</returns>
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处理
};
}
/// <summary>
/// 创建门类型的GridCell
/// </summary>
/// <param name="worldPosition">世界坐标位置</param>
/// <param name="door">关联的门ModelItem</param>
/// <param name="speedLimit">预计算的限速值</param>
/// <param name="cost">遍历成本0表示使用默认值</param>
/// <returns>配置完整的门GridCell</returns>
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
};
}
/// <summary>
/// 创建障碍物类型的GridCell
/// </summary>
/// <param name="worldPosition">世界坐标位置</param>
/// <param name="obstacle">关联的障碍物ModelItem</param>
/// <returns>配置完整的障碍物GridCell</returns>
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 // 障碍物成本最大
};
}
/// <summary>
/// 创建电梯类型的GridCell
/// </summary>
/// <param name="worldPosition">世界坐标位置</param>
/// <param name="elevator">关联的电梯ModelItem</param>
/// <param name="speedLimit">预计算的限速值</param>
/// <param name="cost">遍历成本0表示使用默认值</param>
/// <returns>配置完整的电梯GridCell</returns>
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
};
}
/// <summary>
/// 创建楼梯类型的GridCell
/// </summary>
/// <param name="worldPosition">世界坐标位置</param>
/// <param name="stairs">关联的楼梯ModelItem</param>
/// <param name="speedLimit">预计算的限速值</param>
/// <param name="cost">遍历成本0表示使用默认值</param>
/// <returns>配置完整的楼梯GridCell</returns>
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
};
}
/// <summary>
/// 创建空白/未知类型的GridCell
/// </summary>
/// <param name="worldPosition">世界坐标位置</param>
/// <returns>配置完整的空白GridCell</returns>
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
};
}
}
}

View File

@ -24,7 +24,7 @@ namespace NavisworksTransport.PathPlanning
public int Height { get; set; }
/// <summary>
/// 每个网格单元格的实际尺寸(
/// 每个网格单元格的实际尺寸(模型单位
/// </summary>
public double CellSize { get; set; }
@ -244,8 +244,9 @@ namespace NavisworksTransport.PathPlanning
/// <param name="gridPosition">网格坐标</param>
/// <param name="isWalkable">是否可通行</param>
/// <param name="cost">遍历成本</param>
/// <param name="speedLimit">限速</param>
/// <param name="cellType">单元格类型</param>
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;
}
/// <summary>
/// 将完整配置的GridCell放置到指定位置
/// 这是推荐的GridCell设置方式避免了分步设置的问题
/// </summary>
/// <param name="gridPosition">网格位置</param>
/// <param name="cell">完整配置的GridCell</param>
public void PlaceCell(GridPoint2D gridPosition, GridCell cell)
{
if (!IsValidGridPosition(gridPosition))
return;
// 应用验证规则确保Unknown和障碍物类型的一致性
ApplyCellValidationRules(ref cell, cell.Cost);
Cells[gridPosition.X, gridPosition.Y] = cell;
}
/// <summary>
/// 应用GridCell验证规则
/// 确保Unknown和障碍物类型永远不可通行并设置合适的成本
/// </summary>
/// <param name="cell">要验证的GridCell通过引用传递以便修改</param>
/// <param name="originalCost">原始成本值</param>
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;
}
/// <summary>
@ -576,6 +607,10 @@ namespace NavisworksTransport.PathPlanning
/// 世界坐标位置 - 用于高度计算和空间查询
/// </summary>
public Point3D WorldPosition { get; set; }
/// <summary>
/// 限速(米/秒0表示未设置限速
/// </summary>
public double SpeedLimit { get; set; }
/// <summary>
/// 根据物流类型获取通行成本
@ -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;

View File

@ -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++;
}
}

View File

@ -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<GridPoint2D>();
var dedupedPoints = new List<PathPoint>();
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<GridPoint2D> { 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<PathPoint>();
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);
}
/// <summary>
/// 检查两个路径点之间是否有限速变化
/// 用于路径优化时判断是否需要保留中间点
/// </summary>
/// <param name="point1">第一个路径点</param>
/// <param name="point2">第二个路径点</param>
/// <returns>如果有限速变化返回true需要保留中间点</returns>
private bool HasSpeedLimitChange(PathPoint point1, PathPoint point2)
{
const double speedTolerance = 1e-6; // 仅覆盖浮点数精度误差
return Math.Abs(point1.SpeedLimit - point2.SpeedLimit) > speedTolerance;
}
/// <summary>
/// 检查三个点之间是否有限速变化(用于判断是否可以跳过中间点)
/// </summary>
/// <param name="point1">第一个路径点</param>
/// <param name="point2">中间路径点</param>
/// <param name="point3">第三个路径点</param>
/// <returns>如果有限速变化返回true不能跳过中间点</returns>
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;
}
/// <summary>
/// 检查三点是否真正共线只有当三点都在同一条直线上且方向一致时才返回true
/// 🔥 关键修复:严格检查两个线段的方向是否一致,防止在转折点处错误合并

View File

@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
namespace NavisworksTransport.PathPlanning
{
/// <summary>
/// 时标计算服务
/// 基于路径点的限速和距离计算各路段的时间数据
/// </summary>
public class TimeMarkerCalculationService
{
/// <summary>
/// 路段时间数据
/// </summary>
public class SegmentTimeData
{
/// <summary>
/// 起点名称
/// </summary>
public string StartPointName { get; set; }
/// <summary>
/// 终点名称
/// </summary>
public string EndPointName { get; set; }
/// <summary>
/// 路段距离(米)
/// </summary>
public double Distance { get; set; }
/// <summary>
/// 限速(米/秒)
/// </summary>
public double SpeedLimit { get; set; }
/// <summary>
/// 路段时间(秒)
/// </summary>
public double Time { get; set; }
/// <summary>
/// 累计时间(秒)
/// </summary>
public double CumulativeTime { get; set; }
/// <summary>
/// 是否有效(有限速设置)
/// </summary>
public bool IsValid { get; set; }
}
/// <summary>
/// 时标计算结果
/// </summary>
public class TimeMarkerResult
{
/// <summary>
/// 路段时间数据列表
/// </summary>
public List<SegmentTimeData> Segments { get; set; } = new List<SegmentTimeData>();
/// <summary>
/// 总距离(米)
/// </summary>
public double TotalDistance { get; set; }
/// <summary>
/// 总时间(秒)
/// </summary>
public double TotalTime { get; set; }
/// <summary>
/// 有效路段数量(有限速设置的路段)
/// </summary>
public int ValidSegmentCount { get; set; }
/// <summary>
/// 是否计算成功
/// </summary>
public bool IsSuccessful { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; }
}
/// <summary>
/// 计算路径的时标数据
/// </summary>
/// <param name="route">路径</param>
/// <returns>时标计算结果</returns>
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;
}
}
/// <summary>
/// 确定路段的限速
/// 使用起点和终点限速的最小值作为路段限速
/// </summary>
/// <param name="startPoint">起点</param>
/// <param name="endPoint">终点</param>
/// <returns>路段限速(米/秒)</returns>
private double DetermineSegmentSpeedLimit(PathPoint startPoint, PathPoint endPoint)
{
// 如果起点或终点任一没有限速设置返回0
if (startPoint.SpeedLimit <= 0 || endPoint.SpeedLimit <= 0)
{
return 0;
}
// 使用两点限速的最小值作为路段限速(更安全的策略)
return Math.Min(startPoint.SpeedLimit, endPoint.SpeedLimit);
}
/// <summary>
/// 计算两点间距离
/// </summary>
/// <param name="point1">第一个点</param>
/// <param name="point2">第二个点</param>
/// <returns>距离(米)</returns>
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);
}
/// <summary>
/// 格式化时间为可读字符串
/// </summary>
/// <param name="timeInSeconds">时间(秒)</param>
/// <returns>格式化的时间字符串</returns>
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}秒";
}
}
/// <summary>
/// 生成时标计算报告
/// </summary>
/// <param name="result">计算结果</param>
/// <returns>报告字符串</returns>
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;
}
}
}

View File

@ -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<TimelinePreviewItem>();
}
/// <summary>
/// 从真实路径数据加载时间分段
/// </summary>
/// <param name="route">路径数据</param>
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();
}
}
/// <summary>
/// 生成时间轴预览数据
/// </summary>
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);
}
}
/// <summary>
/// 加载演示数据
/// </summary>
@ -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
}
}
/// <summary>
/// 从计算结果更新时间分段数据
/// </summary>
/// <param name="result">计算结果</param>
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);
}
}
/// <summary>
/// 应用时标到路径点
/// </summary>

View File

@ -175,7 +175,7 @@ NavisworksTransport 时间标签设置对话框 - 采用与主界面一致的Nav
HorizontalGridLinesBrush="#FFEEEEEE"
RowBackground="White"
AlternatingRowBackground="#FFF8FBFF"
Height="300"
Height="240"
ItemsSource="{Binding TimeSegments}">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="40" IsReadOnly="True" Binding="{Binding Index}"/>
@ -191,53 +191,31 @@ NavisworksTransport 时间标签设置对话框 - 采用与主界面一致的Nav
<TextBlock Text="📈 时间轴预览" Style="{StaticResource SectionHeaderStyle}" Margin="0,20,0,10"/>
<StackPanel>
<!-- 时间轴段1 -->
<Border Background="#FFF0F8FF" BorderBrush="#FFB0D4F1" BorderThickness="1" CornerRadius="3" Padding="8" Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="段1" FontWeight="SemiBold" FontSize="10" VerticalAlignment="Center"/>
<Rectangle Grid.Column="1" Height="8" Fill="{StaticResource NavisworksPrimaryBrush}" Margin="5,0" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="12.1s" FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- 时间轴段2 -->
<Border Background="#FFF0F8FF" BorderBrush="#FFB0D4F1" BorderThickness="1" CornerRadius="3" Padding="8" Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="段2" FontWeight="SemiBold" FontSize="10" VerticalAlignment="Center"/>
<Rectangle Grid.Column="1" Height="8" Fill="#FFFF9800" Margin="5,0" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="10.7s" FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- 时间轴段3 -->
<Border Background="#FFFFEAA7" BorderBrush="#FFDDA0DD" BorderThickness="1" CornerRadius="3" Padding="8" Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="等待" FontWeight="SemiBold" FontSize="10" VerticalAlignment="Center"/>
<Rectangle Grid.Column="1" Height="8" Fill="#FF9C27B0" Margin="5,0" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="2.0s" FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- 总计 -->
<!-- 动态时间轴段绑定到TimelineItems集合 -->
<ItemsControl ItemsSource="{Binding TimelineItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#FFF0F8FF" BorderBrush="#FFB0D4F1" BorderThickness="1" CornerRadius="3" Padding="8" Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Name}" FontWeight="SemiBold" FontSize="10" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<Rectangle Grid.Column="1" Height="8" Fill="{Binding Color}" Margin="5,0" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="{Binding Duration, StringFormat='{}{0:F1}s'}" FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 总计绑定到ViewModel -->
<Border Background="#FFE8F5E8" BorderBrush="#FF4CAF50" BorderThickness="2" CornerRadius="4" Padding="10" Margin="0,10,0,0">
<Grid>
<TextBlock Text="总计用时" FontWeight="Bold" FontSize="12" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Text="24.8 秒" FontWeight="Bold" FontSize="14" Foreground="#FF4CAF50" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<TextBlock Text="{Binding TotalDurationText}" FontWeight="Bold" FontSize="14" Foreground="#FF4CAF50" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
</StackPanel>