feat(voxel): 阶段1.2 - 创建 VoxelGrid 基础数据结构
- 创建 VoxelCell.cs: 体素单元类,包含类型、通行性、距离、成本等属性 - 创建 VoxelGrid.cs: 3D体素网格类,包含坐标转换、邻域查询、统计信息等方法 - 添加到 NavisworksTransportPlugin.csproj 编译项 - 编译成功验证 特性: - VoxelCell: 物流属性集成,SDF距离存储,成本计算方法 - VoxelGrid: 世界坐标↔体素索引转换,6/26邻域查询,欧几里得/曼哈顿距离计算 - 完整的 XML 文档注释(中文) 下一步: 实现简单体素化原型
This commit is contained in:
parent
0a61057476
commit
c9ca6b4d32
@ -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">
|
||||
|
||||
161
src/PathPlanning/VoxelCell.cs
Normal file
161
src/PathPlanning/VoxelCell.cs
Normal 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}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
371
src/PathPlanning/VoxelGrid.cs
Normal file
371
src/PathPlanning/VoxelGrid.cs
Normal 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}模型单位]";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user