实现内容: - 创建 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>
389 lines
15 KiB
C#
389 lines
15 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|