NavisworksTransport/src/PathPlanning/VoxelGridGenerator.cs

379 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
using NavisworksTransport.Core.Config;
using static NavisworksTransport.CategoryAttributeManager;
using g4;
namespace NavisworksTransport.PathPlanning
{
/// <summary>
/// 体素网格生成器 - 从 Navisworks BIM 模型生成 3D 体素网格
/// 使用 SDF (Signed Distance Field) 方法进行精确体素化
/// </summary>
public class VoxelGridGenerator
{
/// <summary>
/// 获取模型元素的物流类型
/// </summary>
private LogisticsElementType GetLogisticsElementType(ModelItem item)
{
// 尝试从自定义属性读取
try
{
var categories = item.PropertyCategories;
foreach (var category in categories)
{
if (category.DisplayName == LogisticsCategories.LOGISTICS ||
category.Name == LogisticsCategories.CATEGORY_INTERNAL_NAME)
{
foreach (var prop in category.Properties)
{
if (prop.DisplayName == LogisticsProperties.TYPE)
{
string typeStr = prop.Value.ToDisplayString();
if (Enum.TryParse<LogisticsElementType>(typeStr, out var parsedType))
{
return parsedType;
}
}
}
}
}
}
catch (Exception ex)
{
LogManager.Debug($"读取物流属性失败: {ex.Message}");
}
// 默认:根据元素名称推断类型
string displayName = item.DisplayName?.ToLower() ?? "";
if (displayName.Contains("门") || displayName.Contains("door"))
return LogisticsElementType.;
else if (displayName.Contains("楼梯") || displayName.Contains("stair"))
return LogisticsElementType.;
else if (displayName.Contains("电梯") || displayName.Contains("elevator"))
return LogisticsElementType.;
else if (displayName.Contains("通道") || displayName.Contains("corridor"))
return LogisticsElementType.;
else
return LogisticsElementType.; // 默认为障碍物
}
/// <summary>
/// 判断物流类型是否为障碍物
/// </summary>
private bool IsObstacleType(LogisticsElementType type)
{
switch (type)
{
case LogisticsElementType.:
return true;
case LogisticsElementType.:
case LogisticsElementType.:
case LogisticsElementType.:
case LogisticsElementType.:
case LogisticsElementType.:
case LogisticsElementType.:
return false;
default:
return true; // 未知类型默认为障碍物
}
}
/// <summary>
/// 使用 MeshSignedDistanceGrid 从 BIM 模型生成体素网格(精确几何体方法)
/// 阶段 1.4 版本:使用 geometry4Sharp 的签名距离场进行精确体素化
/// </summary>
/// <param name="bounds">网格边界(世界坐标,模型单位)</param>
/// <param name="voxelSizeMeters">体素尺寸(米)</param>
/// <param name="objectRadiusMeters">物体半径(米),用于安全间隙</param>
/// <param name="objectHeightMeters">物体高度(米)</param>
/// <param name="obstacleItems">障碍物模型元素列表</param>
/// <returns>生成的体素网格</returns>
public VoxelGrid GenerateFromBIMWithSDF(
BoundingBox3D bounds,
double voxelSizeMeters,
double objectRadiusMeters,
double objectHeightMeters,
IEnumerable<ModelItem> obstacleItems)
{
var stopwatch = Stopwatch.StartNew();
LogManager.Info("=== 开始体素网格生成MeshSignedDistanceGrid 方法) ===");
// 第一步:单位转换(米 → 模型单位)
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor(
Autodesk.Navisworks.Api.Application.ActiveDocument.Units);
double voxelSizeInModelUnits = voxelSizeMeters * metersToModelUnits;
double objectRadiusInModelUnits = objectRadiusMeters * metersToModelUnits;
double objectHeightInModelUnits = objectHeightMeters * metersToModelUnits;
double safetyMarginInModelUnits = objectRadiusInModelUnits; // 安全间隙 = 物体半径
LogManager.Info($"单位转换系数: {metersToModelUnits:F4}");
LogManager.Info($"体素尺寸: {voxelSizeMeters}米 = {voxelSizeInModelUnits:F2}模型单位");
LogManager.Info($"安全间隙: {objectRadiusMeters}米 = {safetyMarginInModelUnits:F2}模型单位");
// 第二步:创建体素网格
var voxelGrid = new VoxelGrid(bounds, voxelSizeInModelUnits);
LogManager.Info($"创建体素网格: {voxelGrid.SizeX} × {voxelGrid.SizeY} × {voxelGrid.SizeZ} = {voxelGrid.TotalVoxels:N0} 个体素");
// 第三步:过滤模型元素 - 排除门,只保留真正的障碍物
LogManager.Info("开始过滤模型元素(排除门)...");
var filterStopwatch = Stopwatch.StartNew();
int totalItemsCount = obstacleItems.Count();
var filteredItems = obstacleItems.Where(item =>
{
var elementType = GetLogisticsElementType(item);
// 排除门:门留出空洞,不作为障碍物
// 保留真正的障碍物
return elementType != LogisticsElementType. && IsObstacleType(elementType);
}).ToList();
filterStopwatch.Stop();
LogManager.Info($"元素过滤完成,耗时: {filterStopwatch.ElapsedMilliseconds} ms");
LogManager.Info($"元素统计: 总数={totalItemsCount}, 过滤后={filteredItems.Count}, 排除的门={totalItemsCount - filteredItems.Count}");
// 第四步:提取障碍物几何体并转换为 DMesh3
LogManager.Info("开始提取障碍物几何体...");
var geometryStopwatch = Stopwatch.StartNew();
// 使用几何体缓存
DMesh3 obstacleMesh = null;
var cacheManager = new GeometryCacheManager(Application.ActiveDocument.FileName);
try
{
LogManager.Info($"[体素网格] 从缓存中提取 {filteredItems.Count} 个过滤后项的几何体");
var allTriangles = new List<Triangle3D>();
int processedItems = 0;
foreach (var item in filteredItems)
{
var itemTriangles = cacheManager.GetTrianglesForModelItem(item);
allTriangles.AddRange(itemTriangles);
processedItems++;
}
LogManager.Info($"[体素网格] 从缓存中提取完成,总三角形数: {allTriangles.Count}");
obstacleMesh = NavisworksToDMesh3Converter.Convert(allTriangles);
}
catch (Exception ex)
{
LogManager.Warning($"[体素网格] 使用几何体缓存失败: {ex.Message},回退到传统方法");
// 如果缓存不可用,则使用传统方法提取几何体
obstacleMesh = NavisworksToDMesh3Converter.ConvertFromModelItems(filteredItems);
}
geometryStopwatch.Stop();
LogManager.Info($"几何体提取完成,耗时: {geometryStopwatch.ElapsedMilliseconds} ms");
if (obstacleMesh.TriangleCount == 0)
{
LogManager.Warning("警告:没有提取到任何障碍物几何体,所有体素将标记为可通行");
return voxelGrid;
}
// 第四步:使用 MeshSignedDistanceGrid 计算签名距离场
LogManager.Info("开始计算签名距离场Signed Distance Field...");
var sdfStopwatch = Stopwatch.StartNew();
try
{
// 计算需要的网格尺寸(基于包围盒和体素大小)
var meshBounds = obstacleMesh.GetBounds();
int numCellsX = (int)Math.Ceiling(meshBounds.Width / voxelSizeInModelUnits);
int numCellsY = (int)Math.Ceiling(meshBounds.Height / voxelSizeInModelUnits);
int numCellsZ = (int)Math.Ceiling(meshBounds.Depth / voxelSizeInModelUnits);
LogManager.Info($"SDF 网格尺寸: {numCellsX} × {numCellsY} × {numCellsZ}");
// 创建签名距离场
MeshSignedDistanceGrid sdf = new MeshSignedDistanceGrid(
obstacleMesh,
voxelSizeInModelUnits);
// 计算距离场(这可能耗时较长)
sdf.Compute();
sdfStopwatch.Stop();
LogManager.Info($"距离场计算完成,耗时: {sdfStopwatch.ElapsedMilliseconds} ms ({sdfStopwatch.Elapsed.TotalSeconds:F2} 秒)");
// 第五步:使用距离场标记体素
LogManager.Info("开始根据距离场标记体素...");
var markingStopwatch = Stopwatch.StartNew();
int obstacleCount = 0;
int passableCount = 0;
int boundaryCount = 0;
// SDF阶段不使用安全间隙只标记几何体内部为障碍物
// 安全间隙在后续的XY平面膨胀阶段应用
// 获取 SDF 网格的原点和单元尺寸
Vector3f sdfOrigin = sdf.GridOrigin;
float sdfCellSize = sdf.CellSize;
var sdfDimensions = sdf.Dimensions;
LogManager.Info($"SDF 网格信息: 原点=({sdfOrigin.x:F2}, {sdfOrigin.y:F2}, {sdfOrigin.z:F2}), " +
$"单元尺寸={sdfCellSize:F4}, 维度={sdfDimensions.x}×{sdfDimensions.y}×{sdfDimensions.z}");
for (int x = 0; x < voxelGrid.SizeX; x++)
{
for (int y = 0; y < voxelGrid.SizeY; y++)
{
for (int z = 0; z < voxelGrid.SizeZ; z++)
{
// 体素中心的世界坐标
Point3D worldPos = voxelGrid.VoxelToWorldCenter(x, y, z);
// 将世界坐标转换为 SDF 网格索引
int sdfX = (int)Math.Floor((worldPos.X - sdfOrigin.x) / sdfCellSize);
int sdfY = (int)Math.Floor((worldPos.Y - sdfOrigin.y) / sdfCellSize);
int sdfZ = (int)Math.Floor((worldPos.Z - sdfOrigin.z) / sdfCellSize);
// 查询距离场(使用整数索引)
double distance = 0.0;
if (sdfX >= 0 && sdfX < sdfDimensions.x &&
sdfY >= 0 && sdfY < sdfDimensions.y &&
sdfZ >= 0 && sdfZ < sdfDimensions.z)
{
distance = sdf[sdfX, sdfY, sdfZ];
}
else
{
// 超出 SDF 范围,认为是远离障碍物
distance = double.MaxValue;
}
var cell = voxelGrid.GetCell(x, y, z);
cell.Distance = distance;
// 判断可通行性 - SDF阶段只标记几何体内部为障碍物
// 距离 < 0: 在障碍物内部 → 不可通行
// 距离 >= 0: 在障碍物外部 → 可通行
if (distance < 0)
{
cell.SetAsObstacle();
cell.Type = LogisticsElementType.;
obstacleCount++;
// 统计边界体素距离接近0的体素
if (Math.Abs(distance) < voxelSizeInModelUnits * 0.5)
{
boundaryCount++;
}
}
else
{
cell.IsPassable = true;
cell.Type = LogisticsElementType.;
passableCount++;
}
}
}
}
markingStopwatch.Stop();
LogManager.Info($"体素标记完成,耗时: {markingStopwatch.ElapsedMilliseconds} ms");
LogManager.Info($"标记统计: 障碍物={obstacleCount:N0}, 可通行={passableCount:N0}, 边界={boundaryCount:N0}");
// 第六步:障碍物膨胀(可选)
if (objectRadiusMeters > 0)
{
LogManager.Info("开始障碍物膨胀...");
var inflationStopwatch = Stopwatch.StartNew();
int inflatedCount = voxelGrid.InflateObstacles(objectRadiusMeters, metersToModelUnits);
inflationStopwatch.Stop();
LogManager.Info($"障碍物膨胀完成,耗时: {inflationStopwatch.ElapsedMilliseconds} ms");
LogManager.Info($"膨胀统计: 新增障碍物体素={inflatedCount:N0}");
}
else
{
LogManager.Info("跳过障碍物膨胀物体半径为0");
}
}
catch (Exception ex)
{
LogManager.Error($"MeshSignedDistanceGrid 计算失败: {ex.Message}");
LogManager.Error($"堆栈跟踪: {ex.StackTrace}");
// 不使用 fallback直接抛出异常让问题暴露
throw new InvalidOperationException("SDF 体素网格生成失败,无法继续", ex);
}
// 第七步:统计信息
var (total, passable, obstacle) = voxelGrid.GetStatistics();
double passableRatio = (double)passable / total * 100.0;
stopwatch.Stop();
LogManager.Info($"=== 体素网格生成完成SDF 方法) ===");
LogManager.Info($"总体素数: {total:N0}");
LogManager.Info($"可通行体素: {passable:N0} ({passableRatio:F1}%)");
LogManager.Info($"障碍物体素: {obstacle:N0} ({100 - passableRatio:F1}%)");
LogManager.Info($"总耗时: {stopwatch.ElapsedMilliseconds} ms ({stopwatch.Elapsed.TotalSeconds:F2} 秒)");
return voxelGrid;
}
/// <summary>
/// 快速测试方法 - 生成简单的测试场景体素网格
/// </summary>
/// <returns>测试用体素网格</returns>
public static VoxelGrid CreateTestGrid()
{
// 创建 10m × 10m × 3m 的测试房间
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor(
Autodesk.Navisworks.Api.Application.ActiveDocument.Units);
var bounds = new BoundingBox3D(
new Point3D(0, 0, 0),
new Point3D(10 * metersToModelUnits, 10 * metersToModelUnits, 3 * metersToModelUnits)
);
double voxelSize = ConfigManager.Instance.Current.PathEditing.CellSizeMeters * metersToModelUnits; // 使用配置的网格大小作为体素大小
var grid = new VoxelGrid(bounds, voxelSize);
LogManager.Info($"创建测试网格: {grid}");
// 添加一个简单的障碍物(中心位置的柱子)
int centerX = grid.SizeX / 2;
int centerY = grid.SizeY / 2;
for (int z = 0; z < grid.SizeZ; z++)
{
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
int x = centerX + dx;
int y = centerY + dy;
if (grid.IsValidIndex(x, y, z))
{
var cell = grid.GetCell(x, y, z);
cell.SetAsObstacle();
}
}
}
}
var (total, passable, obstacle) = grid.GetStatistics();
LogManager.Info($"测试网格统计: 总={total}, 可通行={passable}, 障碍={obstacle}");
return grid;
}
}
}