feat(voxel): 阶段1.2 - 创建 VoxelGrid 基础数据结构

- 创建 VoxelCell.cs: 体素单元类,包含类型、通行性、距离、成本等属性
- 创建 VoxelGrid.cs: 3D体素网格类,包含坐标转换、邻域查询、统计信息等方法
- 添加到 NavisworksTransportPlugin.csproj 编译项
- 编译成功验证

特性:
- VoxelCell: 物流属性集成,SDF距离存储,成本计算方法
- VoxelGrid: 世界坐标↔体素索引转换,6/26邻域查询,欧几里得/曼哈顿距离计算
- 完整的 XML 文档注释(中文)

下一步: 实现简单体素化原型
This commit is contained in:
tian 2025-10-12 11:25:45 +08:00
parent 0a61057476
commit c9ca6b4d32
3 changed files with 536 additions and 0 deletions

View File

@ -190,6 +190,10 @@
<Compile Include="src\PathPlanning\TimeMarkerCalculationService.cs" />
<Compile Include="src\PathPlanning\GridMapCacheKey.cs" />
<Compile Include="src\PathPlanning\GridMapCache.cs" />
<!-- PathPlanning - Voxel 3D Path Planning (Experimental) -->
<Compile Include="src\PathPlanning\VoxelCell.cs" />
<Compile Include="src\PathPlanning\VoxelGrid.cs" />
<!-- UI - WPF -->
<Compile Include="src\UI\WPF\Views\LogisticsControlPanel.xaml.cs">

View File

@ -0,0 +1,161 @@
using System;
using Autodesk.Navisworks.Api;
using static NavisworksTransport.CategoryAttributeManager;
namespace NavisworksTransport.PathPlanning
{
/// <summary>
/// 体素单元类 - 表示3D网格中的单个体素
/// 用于体素网格路径规划,存储体素的属性和状态信息
/// </summary>
public class VoxelCell
{
/// <summary>
/// 体素类型(对应物流元素分类)
/// </summary>
public LogisticsElementType Type { get; set; }
/// <summary>
/// 是否可通行
/// true: 该体素为自由空间,可以通行
/// false: 该体素为障碍物或不可通行区域
/// </summary>
public bool IsPassable { get; set; }
/// <summary>
/// 到最近障碍物的距离(模型单位)
/// 使用Signed Distance Field (SDF)计算
/// 正值:自由空间,数值表示到最近障碍物的距离
/// 负值:障碍物内部,数值表示到障碍物表面的距离
/// 零值:障碍物表面
/// </summary>
public double Distance { get; set; }
/// <summary>
/// 速度限制(米/秒)
/// 根据体素所在区域的特性设置(如通道、电梯等)
/// 0.0 表示无速度限制(使用默认速度)
/// </summary>
public double SpeedLimit { get; set; }
/// <summary>
/// 源模型元素
/// 如果该体素对应某个BIM模型元素存储其引用
/// 用于追溯体素属性的来源
/// </summary>
public ModelItem SourceItem { get; set; }
/// <summary>
/// 通行成本
/// 用于A*路径规划算法
/// 考虑因素:距离、速度限制、碰撞风险等
/// 值越大表示通行代价越高
/// </summary>
public double Cost { get; set; }
/// <summary>
/// 构造函数 - 创建默认的可通行体素
/// </summary>
public VoxelCell()
{
Type = LogisticsElementType.;
IsPassable = true;
Distance = double.MaxValue; // 初始化为无限远(远离障碍物)
SpeedLimit = 0.0; // 无速度限制
SourceItem = null;
Cost = 1.0; // 默认通行成本
}
/// <summary>
/// 构造函数 - 创建指定属性的体素
/// </summary>
/// <param name="isPassable">是否可通行</param>
/// <param name="type">体素类型</param>
/// <param name="distance">到最近障碍物的距离</param>
public VoxelCell(bool isPassable, LogisticsElementType type = LogisticsElementType., double distance = double.MaxValue)
{
Type = type;
IsPassable = isPassable;
Distance = distance;
SpeedLimit = 0.0;
SourceItem = null;
Cost = isPassable ? 1.0 : double.MaxValue; // 不可通行区域成本为无穷大
}
/// <summary>
/// 克隆体素单元
/// </summary>
/// <returns>体素单元的深拷贝</returns>
public VoxelCell Clone()
{
return new VoxelCell
{
Type = this.Type,
IsPassable = this.IsPassable,
Distance = this.Distance,
SpeedLimit = this.SpeedLimit,
SourceItem = this.SourceItem,
Cost = this.Cost
};
}
/// <summary>
/// 设置为障碍物体素
/// </summary>
/// <param name="sourceItem">源模型元素</param>
public void SetAsObstacle(ModelItem sourceItem = null)
{
IsPassable = false;
Type = LogisticsElementType.;
Distance = 0.0; // 障碍物表面距离为0
Cost = double.MaxValue;
SourceItem = sourceItem;
}
/// <summary>
/// 设置为自由空间体素
/// </summary>
/// <param name="distance">到最近障碍物的距离</param>
public void SetAsFreeSpace(double distance = double.MaxValue)
{
IsPassable = true;
Type = LogisticsElementType.;
Distance = distance;
Cost = 1.0;
}
/// <summary>
/// 根据距离更新通行成本
/// 距离障碍物越近,成本越高(避免贴边行走)
/// </summary>
/// <param name="safetyMargin">安全边距(模型单位)</param>
public void UpdateCostFromDistance(double safetyMargin)
{
if (!IsPassable)
{
Cost = double.MaxValue;
return;
}
if (Distance < safetyMargin)
{
// 在安全边距内,成本线性增加
double ratio = Distance / safetyMargin;
Cost = 1.0 + (1.0 - ratio) * 10.0; // 成本范围1.0 ~ 11.0
}
else
{
Cost = 1.0; // 安全距离外,标准成本
}
}
/// <summary>
/// 返回体素单元的字符串表示
/// </summary>
public override string ToString()
{
string passableStr = IsPassable ? "可通行" : "障碍";
return $"VoxelCell[{passableStr}, Type={Type}, Dist={Distance:F2}, Cost={Cost:F2}]";
}
}
}

View File

@ -0,0 +1,371 @@
using System;
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.PathPlanning
{
/// <summary>
/// 3D体素网格类 - 用于真3D路径规划
/// 提供完整的3D空间离散化表示支持任意高度的物体和通道
/// </summary>
public class VoxelGrid
{
/// <summary>
/// 3D体素数组 [x, y, z]
/// 注意:索引顺序为 [宽度, 深度, 高度]
/// </summary>
private VoxelCell[,,] cells;
/// <summary>
/// 网格原点(世界坐标,模型单位)
/// 对应体素索引 (0, 0, 0) 的世界坐标位置
/// </summary>
public Point3D Origin { get; private set; }
/// <summary>
/// 单个体素的尺寸(模型单位)
/// 所有三个维度使用相同的体素尺寸(立方体体素)
/// </summary>
public double VoxelSize { get; private set; }
/// <summary>
/// X方向宽度的体素数量
/// </summary>
public int SizeX { get; private set; }
/// <summary>
/// Y方向深度的体素数量
/// </summary>
public int SizeY { get; private set; }
/// <summary>
/// Z方向高度的体素数量
/// </summary>
public int SizeZ { get; private set; }
/// <summary>
/// 网格边界框(世界坐标,模型单位)
/// </summary>
public BoundingBox3D Bounds { get; private set; }
/// <summary>
/// 总体素数量
/// </summary>
public int TotalVoxels => SizeX * SizeY * SizeZ;
/// <summary>
/// 构造函数 - 创建指定尺寸的体素网格
/// </summary>
/// <param name="bounds">网格边界框(世界坐标,模型单位)</param>
/// <param name="voxelSize">体素尺寸(模型单位)</param>
public VoxelGrid(BoundingBox3D bounds, double voxelSize)
{
if (voxelSize <= 0)
throw new ArgumentException("体素尺寸必须大于0", nameof(voxelSize));
VoxelSize = voxelSize;
Bounds = bounds;
Origin = bounds.Min;
// 计算每个维度需要的体素数量(向上取整以覆盖整个边界框)
double width = bounds.Max.X - bounds.Min.X;
double depth = bounds.Max.Y - bounds.Min.Y;
double height = bounds.Max.Z - bounds.Min.Z;
SizeX = (int)Math.Ceiling(width / voxelSize);
SizeY = (int)Math.Ceiling(depth / voxelSize);
SizeZ = (int)Math.Ceiling(height / voxelSize);
// 初始化体素数组(所有体素默认为可通行)
cells = new VoxelCell[SizeX, SizeY, SizeZ];
InitializeCells();
}
/// <summary>
/// 初始化所有体素单元
/// </summary>
private void InitializeCells()
{
for (int x = 0; x < SizeX; x++)
{
for (int y = 0; y < SizeY; y++)
{
for (int z = 0; z < SizeZ; z++)
{
cells[x, y, z] = new VoxelCell();
}
}
}
}
/// <summary>
/// 获取指定索引的体素单元
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <returns>体素单元如果索引越界返回null</returns>
public VoxelCell GetCell(int x, int y, int z)
{
if (!IsValidIndex(x, y, z))
return null;
return cells[x, y, z];
}
/// <summary>
/// 设置指定索引的体素单元
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <param name="cell">体素单元</param>
/// <returns>是否设置成功</returns>
public bool SetCell(int x, int y, int z, VoxelCell cell)
{
if (!IsValidIndex(x, y, z))
return false;
cells[x, y, z] = cell;
return true;
}
/// <summary>
/// 检查体素索引是否有效(在网格范围内)
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <returns>索引是否有效</returns>
public bool IsValidIndex(int x, int y, int z)
{
return x >= 0 && x < SizeX &&
y >= 0 && y < SizeY &&
z >= 0 && z < SizeZ;
}
/// <summary>
/// 世界坐标转换为体素索引
/// 注意:体素索引代表体素的左下角,而不是中心点
/// </summary>
/// <param name="worldPos">世界坐标(模型单位)</param>
/// <returns>体素索引 (x, y, z)</returns>
public (int x, int y, int z) WorldToVoxel(Point3D worldPos)
{
int x = (int)Math.Floor((worldPos.X - Origin.X) / VoxelSize);
int y = (int)Math.Floor((worldPos.Y - Origin.Y) / VoxelSize);
int z = (int)Math.Floor((worldPos.Z - Origin.Z) / VoxelSize);
return (x, y, z);
}
/// <summary>
/// 体素索引转换为世界坐标(体素的左下角坐标)
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <returns>世界坐标(模型单位)</returns>
public Point3D VoxelToWorld(int x, int y, int z)
{
double worldX = Origin.X + x * VoxelSize;
double worldY = Origin.Y + y * VoxelSize;
double worldZ = Origin.Z + z * VoxelSize;
return new Point3D(worldX, worldY, worldZ);
}
/// <summary>
/// 体素索引转换为世界坐标(体素的中心点坐标)
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <returns>世界坐标(模型单位)</returns>
public Point3D VoxelToWorldCenter(int x, int y, int z)
{
double halfVoxel = VoxelSize / 2.0;
double worldX = Origin.X + x * VoxelSize + halfVoxel;
double worldY = Origin.Y + y * VoxelSize + halfVoxel;
double worldZ = Origin.Z + z * VoxelSize + halfVoxel;
return new Point3D(worldX, worldY, worldZ);
}
/// <summary>
/// 获取体素的6邻域上下左右前后
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <returns>邻居体素索引列表</returns>
public List<(int x, int y, int z)> GetNeighbors6(int x, int y, int z)
{
var neighbors = new List<(int, int, int)>();
// 6个方向右、左、后、前、上、下
int[,] directions = {
{ 1, 0, 0 }, // +X 右
{ -1, 0, 0 }, // -X 左
{ 0, 1, 0 }, // +Y 后
{ 0, -1, 0 }, // -Y 前
{ 0, 0, 1 }, // +Z 上
{ 0, 0, -1 } // -Z 下
};
for (int i = 0; i < 6; i++)
{
int nx = x + directions[i, 0];
int ny = y + directions[i, 1];
int nz = z + directions[i, 2];
if (IsValidIndex(nx, ny, nz))
{
neighbors.Add((nx, ny, nz));
}
}
return neighbors;
}
/// <summary>
/// 获取体素的26邻域包括对角线方向
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <returns>邻居体素索引列表</returns>
public List<(int x, int y, int z)> GetNeighbors26(int x, int y, int z)
{
var neighbors = new List<(int, int, int)>();
// 26个方向3x3x3立方体去掉中心点
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
for (int dz = -1; dz <= 1; dz++)
{
// 跳过中心点
if (dx == 0 && dy == 0 && dz == 0)
continue;
int nx = x + dx;
int ny = y + dy;
int nz = z + dz;
if (IsValidIndex(nx, ny, nz))
{
neighbors.Add((nx, ny, nz));
}
}
}
}
return neighbors;
}
/// <summary>
/// 获取两个体素之间的欧几里得距离(体素单位)
/// </summary>
/// <param name="x1">第一个体素的X索引</param>
/// <param name="y1">第一个体素的Y索引</param>
/// <param name="z1">第一个体素的Z索引</param>
/// <param name="x2">第二个体素的X索引</param>
/// <param name="y2">第二个体素的Y索引</param>
/// <param name="z2">第二个体素的Z索引</param>
/// <returns>欧几里得距离(体素单位)</returns>
public double GetDistance(int x1, int y1, int z1, int x2, int y2, int z2)
{
int dx = x2 - x1;
int dy = y2 - y1;
int dz = z2 - z1;
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
/// <summary>
/// 获取两个体素之间的曼哈顿距离(体素单位)
/// </summary>
/// <param name="x1">第一个体素的X索引</param>
/// <param name="y1">第一个体素的Y索引</param>
/// <param name="z1">第一个体素的Z索引</param>
/// <param name="x2">第二个体素的X索引</param>
/// <param name="y2">第二个体素的Y索引</param>
/// <param name="z2">第二个体素的Z索引</param>
/// <returns>曼哈顿距离(体素单位)</returns>
public int GetManhattanDistance(int x1, int y1, int z1, int x2, int y2, int z2)
{
return Math.Abs(x2 - x1) + Math.Abs(y2 - y1) + Math.Abs(z2 - z1);
}
/// <summary>
/// 检查指定索引的体素是否可通行
/// </summary>
/// <param name="x">X方向索引</param>
/// <param name="y">Y方向索引</param>
/// <param name="z">Z方向索引</param>
/// <returns>是否可通行索引越界返回false</returns>
public bool IsPassable(int x, int y, int z)
{
var cell = GetCell(x, y, z);
return cell != null && cell.IsPassable;
}
/// <summary>
/// 检查世界坐标位置是否可通行
/// </summary>
/// <param name="worldPos">世界坐标(模型单位)</param>
/// <returns>是否可通行</returns>
public bool IsPassable(Point3D worldPos)
{
var (x, y, z) = WorldToVoxel(worldPos);
return IsPassable(x, y, z);
}
/// <summary>
/// 获取网格的统计信息
/// </summary>
/// <returns>(总体素数, 可通行体素数, 障碍物体素数)</returns>
public (int total, int passable, int obstacle) GetStatistics()
{
int total = TotalVoxels;
int passable = 0;
int obstacle = 0;
for (int x = 0; x < SizeX; x++)
{
for (int y = 0; y < SizeY; y++)
{
for (int z = 0; z < SizeZ; z++)
{
if (cells[x, y, z].IsPassable)
passable++;
else
obstacle++;
}
}
}
return (total, passable, obstacle);
}
/// <summary>
/// 清除所有体素(重置为可通行状态)
/// </summary>
public void Clear()
{
InitializeCells();
}
/// <summary>
/// 返回网格的字符串表示
/// </summary>
public override string ToString()
{
var (total, passable, obstacle) = GetStatistics();
double passableRatio = (double)passable / total * 100.0;
return $"VoxelGrid[{SizeX}x{SizeY}x{SizeZ}={total}体素, 可通行:{passableRatio:F1}%, 体素尺寸:{VoxelSize:F2}模型单位]";
}
}
}