feat(voxel): 阶段1.5 - 体素网格可视化验证

实现内容:
- 创建 VoxelGridVisualizer 类,支持多种可视化模式
- VisualizeAsPoints(): 完整网格可视化(支持采样率)
- VisualizeSlice(): 单层切片可视化
- VisualizeBoundary(): 障碍物边界可视化
- ConvertToPathRoute(): 转换为 PathRoute 以便集成到现有渲染系统
- QuickVisualizationTest(): 快速测试套件
- GenerateVisualizationReport(): 文本报告生成

设计策略:
- 复用现有 PathPointRenderPlugin,而非创建新的 RenderPlugin
- 将体素转换为 PathPoint 对象进行渲染
- 使用小球体表示体素,不同颜色区分类型

完成情况:
-  编译通过,无错误
-  阶段 1 全部 5 个任务完成 (100%)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tian 2025-10-12 13:38:57 +08:00
parent f64e79d372
commit 805814616a
2 changed files with 389 additions and 0 deletions

View File

@ -196,6 +196,7 @@
<Compile Include="src\PathPlanning\VoxelGrid.cs" />
<Compile Include="src\PathPlanning\VoxelGridGenerator.cs" />
<Compile Include="src\PathPlanning\MeshSDFTester.cs" />
<Compile Include="src\PathPlanning\VoxelGridVisualizer.cs" />
<!-- UI - WPF -->
<Compile Include="src\UI\WPF\Views\LogisticsControlPanel.xaml.cs">

View File

@ -0,0 +1,388 @@
using System;
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
using static NavisworksTransport.CategoryAttributeManager;
namespace NavisworksTransport.PathPlanning
{
/// <summary>
/// 体素网格可视化器 - 在 Navisworks 3D 视图中显示体素网格
/// 阶段 1.5:体素可视化验证
/// </summary>
public class VoxelGridVisualizer
{
/// <summary>
/// 可视化体素网格(简化版本 - 使用小球表示体素)
/// </summary>
/// <param name="voxelGrid">体素网格</param>
/// <param name="samplingRate">采样率1=全部显示2=每隔一个显示,以此类推)</param>
/// <param name="showPassableOnly">是否只显示可通行体素</param>
/// <returns>生成的路径点列表可用于PathPointRenderPlugin渲染</returns>
public static List<PathPoint> VisualizeAsPoints(
VoxelGrid voxelGrid,
int samplingRate = 2,
bool showPassableOnly = false)
{
LogManager.Info("=== 开始体素网格可视化 ===");
LogManager.Info($"网格尺寸: {voxelGrid.SizeX} × {voxelGrid.SizeY} × {voxelGrid.SizeZ}");
LogManager.Info($"采样率: {samplingRate} (每{samplingRate}个体素显示1个)");
LogManager.Info($"仅显示可通行: {showPassableOnly}");
var points = new List<PathPoint>();
int totalVoxels = 0;
int visualizedVoxels = 0;
try
{
for (int x = 0; x < voxelGrid.SizeX; x += samplingRate)
{
for (int y = 0; y < voxelGrid.SizeY; y += samplingRate)
{
for (int z = 0; z < voxelGrid.SizeZ; z += samplingRate)
{
totalVoxels++;
var cell = voxelGrid.GetCell(x, y, z);
if (cell == null) continue;
// 过滤:如果只显示可通行体素,跳过不可通行的
if (showPassableOnly && !cell.IsPassable)
continue;
// 获取体素中心点的世界坐标
var worldPos = voxelGrid.VoxelToWorldCenter(x, y, z);
// 创建路径点表示体素
var point = new PathPoint
{
Index = visualizedVoxels,
Position = worldPos,
Name = GetVoxelDisplayName(cell, x, y, z),
Type = PathPointType.WayPoint,
Notes = $"VoxelType:{cell.Type}"
};
points.Add(point);
visualizedVoxels++;
}
}
}
LogManager.Info($"总体素数: {voxelGrid.TotalVoxels:N0}");
LogManager.Info($"采样后体素数: {totalVoxels:N0}");
LogManager.Info($"可视化体素数: {visualizedVoxels:N0}");
LogManager.Info("=== 体素网格可视化完成 ===");
}
catch (Exception ex)
{
LogManager.Error($"体素网格可视化失败: {ex.Message}");
LogManager.Error($"堆栈跟踪: {ex.StackTrace}");
}
return points;
}
/// <summary>
/// 可视化体素网格的切片指定Z高度
/// </summary>
/// <param name="voxelGrid">体素网格</param>
/// <param name="zIndex">Z方向索引高度层</param>
/// <returns>生成的路径点列表</returns>
public static List<PathPoint> VisualizeSlice(VoxelGrid voxelGrid, int zIndex)
{
LogManager.Info($"=== 可视化体素网格切片 (Z={zIndex}) ===");
var points = new List<PathPoint>();
int visualizedCount = 0;
try
{
if (!voxelGrid.IsValidIndex(0, 0, zIndex))
{
LogManager.Warning($"无效的 Z 索引: {zIndex},有效范围: 0 ~ {voxelGrid.SizeZ - 1}");
return points;
}
for (int x = 0; x < voxelGrid.SizeX; x++)
{
for (int y = 0; y < voxelGrid.SizeY; y++)
{
var cell = voxelGrid.GetCell(x, y, zIndex);
if (cell == null) continue;
// 获取体素中心点的世界坐标
var worldPos = voxelGrid.VoxelToWorldCenter(x, y, zIndex);
// 创建路径点表示体素
var point = new PathPoint
{
Index = visualizedCount,
Position = worldPos,
Name = GetVoxelDisplayName(cell, x, y, zIndex),
Type = PathPointType.WayPoint,
Notes = $"VoxelType:{cell.Type}"
};
points.Add(point);
visualizedCount++;
}
}
LogManager.Info($"切片 Z={zIndex} 可视化体素数: {visualizedCount:N0}");
LogManager.Info("=== 切片可视化完成 ===");
}
catch (Exception ex)
{
LogManager.Error($"切片可视化失败: {ex.Message}");
LogManager.Error($"堆栈跟踪: {ex.StackTrace}");
}
return points;
}
/// <summary>
/// 可视化体素网格的边界(只显示障碍物表面)
/// </summary>
/// <param name="voxelGrid">体素网格</param>
/// <returns>生成的路径点列表</returns>
public static List<PathPoint> VisualizeBoundary(VoxelGrid voxelGrid)
{
LogManager.Info("=== 可视化体素网格边界(障碍物表面) ===");
var points = new List<PathPoint>();
int visualizedCount = 0;
try
{
for (int x = 0; x < voxelGrid.SizeX; x++)
{
for (int y = 0; y < voxelGrid.SizeY; y++)
{
for (int z = 0; z < voxelGrid.SizeZ; z++)
{
var cell = voxelGrid.GetCell(x, y, z);
if (cell == null || cell.IsPassable) continue;
// 检查是否是边界体素(至少有一个相邻体素是可通行的)
if (IsBoundaryVoxel(voxelGrid, x, y, z))
{
var worldPos = voxelGrid.VoxelToWorldCenter(x, y, z);
var point = new PathPoint
{
Index = visualizedCount,
Position = worldPos,
Name = GetVoxelDisplayName(cell, x, y, z),
Type = PathPointType.WayPoint,
Notes = $"VoxelType:{cell.Type},Boundary:true"
};
points.Add(point);
visualizedCount++;
}
}
}
}
LogManager.Info($"边界体素数: {visualizedCount:N0}");
LogManager.Info("=== 边界可视化完成 ===");
}
catch (Exception ex)
{
LogManager.Error($"边界可视化失败: {ex.Message}");
LogManager.Error($"堆栈跟踪: {ex.StackTrace}");
}
return points;
}
/// <summary>
/// 判断体素是否为边界体素
/// </summary>
private static bool IsBoundaryVoxel(VoxelGrid voxelGrid, int x, int y, int z)
{
// 获取 6 邻域
var neighbors = voxelGrid.GetNeighbors6(x, y, z);
// 如果至少有一个邻居是可通行的,则当前体素是边界
foreach (var (nx, ny, nz) in neighbors)
{
if (voxelGrid.IsPassable(nx, ny, nz))
{
return true;
}
}
return false;
}
/// <summary>
/// 获取体素显示名称
/// </summary>
private static string GetVoxelDisplayName(VoxelCell cell, int x, int y, int z)
{
string typeStr = cell.IsPassable ? "通行" : "障碍";
return $"体素({x},{y},{z})-{typeStr}-{cell.Type}";
}
/// <summary>
/// 将体素列表转换为 PathRoute以便使用 PathPointRenderPlugin 渲染
/// </summary>
/// <param name="voxelPoints">体素点列表</param>
/// <param name="routeName">路径名称</param>
/// <returns>PathRoute 对象</returns>
public static PathRoute ConvertToPathRoute(List<PathPoint> voxelPoints, string routeName = "体素网格可视化")
{
var route = new PathRoute
{
Id = $"voxel_visualization_{Guid.NewGuid():N}",
Name = routeName,
Points = voxelPoints,
IsComplete = true,
CreatedTime = DateTime.Now
};
return route;
}
/// <summary>
/// 快速可视化测试
/// </summary>
/// <param name="voxelGrid">体素网格</param>
public static void QuickVisualizationTest(VoxelGrid voxelGrid)
{
LogManager.Info("=== 开始快速可视化测试 ===");
try
{
// 统计信息
var (total, passable, obstacle) = voxelGrid.GetStatistics();
LogManager.Info($"体素统计: 总数={total:N0}, 可通行={passable:N0}, 障碍={obstacle:N0}");
// 测试1可视化底层切片Z=0
LogManager.Info("[测试 1] 可视化底层切片 (Z=0)");
var slicePoints = VisualizeSlice(voxelGrid, 0);
LogManager.Info($"底层切片点数: {slicePoints.Count}");
// 测试2可视化中间层切片Z=中间)
int midZ = voxelGrid.SizeZ / 2;
LogManager.Info($"[测试 2] 可视化中间层切片 (Z={midZ})");
var midSlicePoints = VisualizeSlice(voxelGrid, midZ);
LogManager.Info($"中间层切片点数: {midSlicePoints.Count}");
// 测试3可视化边界
LogManager.Info("[测试 3] 可视化边界(障碍物表面)");
var boundaryPoints = VisualizeBoundary(voxelGrid);
LogManager.Info($"边界点数: {boundaryPoints.Count}");
// 测试4采样可视化每隔2个体素显示1个
LogManager.Info("[测试 4] 采样可视化(采样率=2");
var sampledPoints = VisualizeAsPoints(voxelGrid, 2, false);
LogManager.Info($"采样点数: {sampledPoints.Count}");
LogManager.Info("=== 快速可视化测试完成 ===");
LogManager.Info("提示:使用 PathPointRenderPlugin 渲染这些点");
}
catch (Exception ex)
{
LogManager.Error($"快速可视化测试失败: {ex.Message}");
LogManager.Error($"堆栈跟踪: {ex.StackTrace}");
}
}
/// <summary>
/// 生成可视化报告(文本格式)
/// </summary>
/// <param name="voxelGrid">体素网格</param>
/// <returns>报告文本</returns>
public static string GenerateVisualizationReport(VoxelGrid voxelGrid)
{
var report = new System.Text.StringBuilder();
report.AppendLine("================================");
report.AppendLine(" 体素网格可视化报告");
report.AppendLine("================================");
report.AppendLine();
// 基本信息
report.AppendLine("【基本信息】");
report.AppendLine($"网格尺寸: {voxelGrid.SizeX} × {voxelGrid.SizeY} × {voxelGrid.SizeZ}");
report.AppendLine($"体素尺寸: {voxelGrid.VoxelSize:F4} 模型单位");
report.AppendLine($"总体素数: {voxelGrid.TotalVoxels:N0}");
report.AppendLine($"网格边界: {voxelGrid.Bounds.Min} ~ {voxelGrid.Bounds.Max}");
report.AppendLine();
// 统计信息
var (total, passable, obstacle) = voxelGrid.GetStatistics();
double passableRatio = (double)passable / total * 100.0;
double obstacleRatio = (double)obstacle / total * 100.0;
report.AppendLine("【统计信息】");
report.AppendLine($"总体素数: {total:N0}");
report.AppendLine($"可通行体素: {passable:N0} ({passableRatio:F1}%)");
report.AppendLine($"障碍物体素: {obstacle:N0} ({obstacleRatio:F1}%)");
report.AppendLine();
// 类型分布
report.AppendLine("【体素类型分布】");
var typeDistribution = AnalyzeTypeDistribution(voxelGrid);
foreach (var kvp in typeDistribution)
{
double ratio = (double)kvp.Value / total * 100.0;
report.AppendLine($" {kvp.Key}: {kvp.Value:N0} ({ratio:F1}%)");
}
report.AppendLine();
// 可视化建议
report.AppendLine("【可视化建议】");
if (total > 1000000)
{
report.AppendLine(" ⚠️ 体素数量很大,建议使用采样率 >= 5");
report.AppendLine(" 或者只可视化边界/切片");
}
else if (total > 100000)
{
report.AppendLine(" ⚠️ 体素数量较大,建议使用采样率 >= 3");
}
else
{
report.AppendLine(" ✅ 体素数量适中,可以使用采样率 1-2");
}
report.AppendLine();
report.AppendLine("================================");
return report.ToString();
}
/// <summary>
/// 分析体素类型分布
/// </summary>
private static Dictionary<LogisticsElementType, int> AnalyzeTypeDistribution(VoxelGrid voxelGrid)
{
var distribution = new Dictionary<LogisticsElementType, int>();
for (int x = 0; x < voxelGrid.SizeX; x++)
{
for (int y = 0; y < voxelGrid.SizeY; y++)
{
for (int z = 0; z < voxelGrid.SizeZ; z++)
{
var cell = voxelGrid.GetCell(x, y, z);
if (cell == null) continue;
if (!distribution.ContainsKey(cell.Type))
{
distribution[cell.Type] = 0;
}
distribution[cell.Type]++;
}
}
}
return distribution;
}
}
}