测试和修改A*返回的坐标转换的问题
This commit is contained in:
parent
95bf6e839b
commit
3f2d66c255
@ -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
57
AStarTestRunner.csproj
Normal 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>
|
||||
20
CLAUDE.md
20
CLAUDE.md
@ -211,3 +211,23 @@ public class PathClickToolPlugin : ToolPlugin { }
|
||||
- 程序的日志在:C:\ProgramData\Autodesk\Navisworks Manage 2026\NavisworksTransport\logs\debug.log
|
||||
- 使用agent完成任务前,一定要先用Plan模式设计好方案和任务清单,并征得我同意。
|
||||
- 网格坐标代表的是网格单元的左下角,而不是中心点!
|
||||
|
||||
## 运行路径测试
|
||||
|
||||
需要先编译单元测试DLL,然后再编译TestRunner:
|
||||
|
||||
● Bash(powershell -Command "& 'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe'
|
||||
NavisworksTransport.UnitTests.csproj /p:Configu…)
|
||||
⎿ 适用于 .NET Framework MSBuild 版本 17.14.10+8b8e13593
|
||||
|
||||
NavisworksTransport.UnitTests -> C:\Users\Tellme\apps\NavisworksTransport\bin\Debug\NavisworksTransport.UnitTests.dll
|
||||
|
||||
● Bash(powershell -Command "& 'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe'
|
||||
AStarTestRunner.csproj /p:Configuration=Debug /…)
|
||||
⎿ 适用于 .NET Framework MSBuild 版本 17.14.10+8b8e13593
|
||||
|
||||
AStarTestRunner -> C:\Users\Tellme\apps\NavisworksTransport\bin\Debug\AStarTestRunner.exe
|
||||
|
||||
● 现在运行新的测试程序:
|
||||
|
||||
● Bash("bin\Debug\AStarTestRunner.exe")
|
||||
|
||||
@ -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>
|
||||
|
||||
867
NavisworksTransport.UnitTests/AStarDebuggingTest.cs
Normal file
867
NavisworksTransport.UnitTests/AStarDebuggingTest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
86
TestRunner.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
462
doc/working/video_export_ffmpegcore_solution_20250929.md
Normal file
462
doc/working/video_export_ffmpegcore_solution_20250929.md
Normal 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.dll(NuGet自动处理)
|
||||
├── Instances.dll(依赖项)
|
||||
├── System.Text.Json.dll(依赖项)
|
||||
└── ffmpeg/
|
||||
├── ffmpeg.exe(Windows x64版本,~50MB)
|
||||
└── ffprobe.exe(探测工具,~20MB)
|
||||
```
|
||||
|
||||
### FFmpeg二进制文件获取
|
||||
1. **官方下载:** https://ffmpeg.org/download.html#build-windows
|
||||
2. **推荐版本:** FFmpeg 4.4.0+ Windows x64 static builds
|
||||
3. **文件来源:** Gyan.dev或BtbN builds(预编译静态版本)
|
||||
|
||||
### 配置代码
|
||||
```csharp
|
||||
// 在应用程序启动时配置FFmpeg路径
|
||||
public static void ConfigureFFmpeg()
|
||||
{
|
||||
var ffmpegPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg");
|
||||
|
||||
if (!Directory.Exists(ffmpegPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"FFmpeg二进制目录不存在: {ffmpegPath}");
|
||||
}
|
||||
|
||||
GlobalFFOptions.Configure(new FFOptions
|
||||
{
|
||||
BinaryFolder = ffmpegPath,
|
||||
TemporaryFilesFolder = Path.GetTempPath()
|
||||
});
|
||||
|
||||
LogManager.Info($"FFmpeg已配置: {ffmpegPath}");
|
||||
}
|
||||
```
|
||||
|
||||
## 支持的视频格式
|
||||
|
||||
### 输出格式
|
||||
- **MP4**(推荐):H.264编码,兼容性最好
|
||||
- **AVI**:MPEG-4编码,文件较大但兼容性好
|
||||
- **WebM**:VP9编码,适合web播放
|
||||
- **MOV**:QuickTime格式,适合Mac用户
|
||||
|
||||
### 编码参数建议
|
||||
```csharp
|
||||
// 高质量设置
|
||||
.WithVideoCodec(VideoCodec.LibX264)
|
||||
.WithConstantRateFactor(23) // CRF值,18-28范围,数值越小质量越高
|
||||
.WithFramerate(30)
|
||||
|
||||
// 压缩优化设置
|
||||
.WithVideoCodec(VideoCodec.LibX264)
|
||||
.WithVideoBitrate(2000) // 2Mbps码率
|
||||
.WithAudioCodec(AudioCodec.Aac) // 如果需要音频
|
||||
```
|
||||
|
||||
## 性能优化策略
|
||||
|
||||
### 内存管理
|
||||
```csharp
|
||||
// 避免内存泄漏的Bitmap处理
|
||||
public void CaptureCurrentFrame()
|
||||
{
|
||||
using (var originalBitmap = Document.GenerateImage(...))
|
||||
{
|
||||
// 创建副本避免引用原始对象
|
||||
var frameCopy = new Bitmap(originalBitmap.Width, originalBitmap.Height, originalBitmap.PixelFormat);
|
||||
using (var graphics = Graphics.FromImage(frameCopy))
|
||||
{
|
||||
graphics.DrawImage(originalBitmap, 0, 0);
|
||||
}
|
||||
_capturedFrames.Add(frameCopy);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步处理
|
||||
```csharp
|
||||
// UI不会冻结的异步导出
|
||||
public async Task ExportVideoAsync(VideoExportSettings settings)
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
// 在后台线程执行视频编码
|
||||
await _videoExportManager.FinishVideoCapture();
|
||||
});
|
||||
|
||||
// 回到UI线程更新状态
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateStatus("视频导出完成");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 进度报告
|
||||
```csharp
|
||||
public class VideoExportManager
|
||||
{
|
||||
public event EventHandler<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天)
|
||||
- **阶段4:UI优化**(0.5天)
|
||||
- **总计:3.5-4.5天**
|
||||
|
||||
## 总结
|
||||
|
||||
FFMpegCore方案虽然需要分发额外的二进制文件,但提供了最完整和专业的视频导出功能。通过合理的架构设计和错误处理,可以为用户提供高质量的导航视频导出体验。
|
||||
|
||||
**推荐实施顺序:**
|
||||
1. 先实现基础的帧捕获功能
|
||||
2. 集成FFMpegCore进行视频编码测试
|
||||
3. 完善UI和用户体验
|
||||
4. 优化性能和错误处理
|
||||
|
||||
这个方案能够满足专业用户对视频质量和格式的要求,是长期来看最具价值的技术选择。
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
151
src/PathPlanning/GridCellBuilder.cs
Normal file
151
src/PathPlanning/GridCellBuilder.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
/// 🔥 关键修复:严格检查两个线段的方向是否一致,防止在转折点处错误合并
|
||||
|
||||
278
src/PathPlanning/TimeMarkerCalculationService.cs
Normal file
278
src/PathPlanning/TimeMarkerCalculationService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user