diff --git a/NavisworksTransportPlugin.csproj b/NavisworksTransportPlugin.csproj index b7c524f..45e9265 100644 --- a/NavisworksTransportPlugin.csproj +++ b/NavisworksTransportPlugin.csproj @@ -190,6 +190,10 @@ + + + + diff --git a/src/PathPlanning/VoxelCell.cs b/src/PathPlanning/VoxelCell.cs new file mode 100644 index 0000000..37e404e --- /dev/null +++ b/src/PathPlanning/VoxelCell.cs @@ -0,0 +1,161 @@ +using System; +using Autodesk.Navisworks.Api; +using static NavisworksTransport.CategoryAttributeManager; + +namespace NavisworksTransport.PathPlanning +{ + /// + /// 体素单元类 - 表示3D网格中的单个体素 + /// 用于体素网格路径规划,存储体素的属性和状态信息 + /// + public class VoxelCell + { + /// + /// 体素类型(对应物流元素分类) + /// + public LogisticsElementType Type { get; set; } + + /// + /// 是否可通行 + /// true: 该体素为自由空间,可以通行 + /// false: 该体素为障碍物或不可通行区域 + /// + public bool IsPassable { get; set; } + + /// + /// 到最近障碍物的距离(模型单位) + /// 使用Signed Distance Field (SDF)计算 + /// 正值:自由空间,数值表示到最近障碍物的距离 + /// 负值:障碍物内部,数值表示到障碍物表面的距离 + /// 零值:障碍物表面 + /// + public double Distance { get; set; } + + /// + /// 速度限制(米/秒) + /// 根据体素所在区域的特性设置(如通道、电梯等) + /// 0.0 表示无速度限制(使用默认速度) + /// + public double SpeedLimit { get; set; } + + /// + /// 源模型元素 + /// 如果该体素对应某个BIM模型元素,存储其引用 + /// 用于追溯体素属性的来源 + /// + public ModelItem SourceItem { get; set; } + + /// + /// 通行成本 + /// 用于A*路径规划算法 + /// 考虑因素:距离、速度限制、碰撞风险等 + /// 值越大表示通行代价越高 + /// + public double Cost { get; set; } + + /// + /// 构造函数 - 创建默认的可通行体素 + /// + public VoxelCell() + { + Type = LogisticsElementType.通道; + IsPassable = true; + Distance = double.MaxValue; // 初始化为无限远(远离障碍物) + SpeedLimit = 0.0; // 无速度限制 + SourceItem = null; + Cost = 1.0; // 默认通行成本 + } + + /// + /// 构造函数 - 创建指定属性的体素 + /// + /// 是否可通行 + /// 体素类型 + /// 到最近障碍物的距离 + 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; // 不可通行区域成本为无穷大 + } + + /// + /// 克隆体素单元 + /// + /// 体素单元的深拷贝 + public VoxelCell Clone() + { + return new VoxelCell + { + Type = this.Type, + IsPassable = this.IsPassable, + Distance = this.Distance, + SpeedLimit = this.SpeedLimit, + SourceItem = this.SourceItem, + Cost = this.Cost + }; + } + + /// + /// 设置为障碍物体素 + /// + /// 源模型元素 + public void SetAsObstacle(ModelItem sourceItem = null) + { + IsPassable = false; + Type = LogisticsElementType.障碍物; + Distance = 0.0; // 障碍物表面距离为0 + Cost = double.MaxValue; + SourceItem = sourceItem; + } + + /// + /// 设置为自由空间体素 + /// + /// 到最近障碍物的距离 + public void SetAsFreeSpace(double distance = double.MaxValue) + { + IsPassable = true; + Type = LogisticsElementType.通道; + Distance = distance; + Cost = 1.0; + } + + /// + /// 根据距离更新通行成本 + /// 距离障碍物越近,成本越高(避免贴边行走) + /// + /// 安全边距(模型单位) + 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; // 安全距离外,标准成本 + } + } + + /// + /// 返回体素单元的字符串表示 + /// + public override string ToString() + { + string passableStr = IsPassable ? "可通行" : "障碍"; + return $"VoxelCell[{passableStr}, Type={Type}, Dist={Distance:F2}, Cost={Cost:F2}]"; + } + } +} diff --git a/src/PathPlanning/VoxelGrid.cs b/src/PathPlanning/VoxelGrid.cs new file mode 100644 index 0000000..0a1fe0b --- /dev/null +++ b/src/PathPlanning/VoxelGrid.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.PathPlanning +{ + /// + /// 3D体素网格类 - 用于真3D路径规划 + /// 提供完整的3D空间离散化表示,支持任意高度的物体和通道 + /// + public class VoxelGrid + { + /// + /// 3D体素数组 [x, y, z] + /// 注意:索引顺序为 [宽度, 深度, 高度] + /// + private VoxelCell[,,] cells; + + /// + /// 网格原点(世界坐标,模型单位) + /// 对应体素索引 (0, 0, 0) 的世界坐标位置 + /// + public Point3D Origin { get; private set; } + + /// + /// 单个体素的尺寸(模型单位) + /// 所有三个维度使用相同的体素尺寸(立方体体素) + /// + public double VoxelSize { get; private set; } + + /// + /// X方向(宽度)的体素数量 + /// + public int SizeX { get; private set; } + + /// + /// Y方向(深度)的体素数量 + /// + public int SizeY { get; private set; } + + /// + /// Z方向(高度)的体素数量 + /// + public int SizeZ { get; private set; } + + /// + /// 网格边界框(世界坐标,模型单位) + /// + public BoundingBox3D Bounds { get; private set; } + + /// + /// 总体素数量 + /// + public int TotalVoxels => SizeX * SizeY * SizeZ; + + /// + /// 构造函数 - 创建指定尺寸的体素网格 + /// + /// 网格边界框(世界坐标,模型单位) + /// 体素尺寸(模型单位) + 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(); + } + + /// + /// 初始化所有体素单元 + /// + 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(); + } + } + } + } + + /// + /// 获取指定索引的体素单元 + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 体素单元,如果索引越界返回null + public VoxelCell GetCell(int x, int y, int z) + { + if (!IsValidIndex(x, y, z)) + return null; + + return cells[x, y, z]; + } + + /// + /// 设置指定索引的体素单元 + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 体素单元 + /// 是否设置成功 + 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; + } + + /// + /// 检查体素索引是否有效(在网格范围内) + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 索引是否有效 + public bool IsValidIndex(int x, int y, int z) + { + return x >= 0 && x < SizeX && + y >= 0 && y < SizeY && + z >= 0 && z < SizeZ; + } + + /// + /// 世界坐标转换为体素索引 + /// 注意:体素索引代表体素的左下角,而不是中心点 + /// + /// 世界坐标(模型单位) + /// 体素索引 (x, y, z) + 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); + } + + /// + /// 体素索引转换为世界坐标(体素的左下角坐标) + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 世界坐标(模型单位) + 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); + } + + /// + /// 体素索引转换为世界坐标(体素的中心点坐标) + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 世界坐标(模型单位) + 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); + } + + /// + /// 获取体素的6邻域(上下左右前后) + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 邻居体素索引列表 + 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; + } + + /// + /// 获取体素的26邻域(包括对角线方向) + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 邻居体素索引列表 + 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; + } + + /// + /// 获取两个体素之间的欧几里得距离(体素单位) + /// + /// 第一个体素的X索引 + /// 第一个体素的Y索引 + /// 第一个体素的Z索引 + /// 第二个体素的X索引 + /// 第二个体素的Y索引 + /// 第二个体素的Z索引 + /// 欧几里得距离(体素单位) + 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); + } + + /// + /// 获取两个体素之间的曼哈顿距离(体素单位) + /// + /// 第一个体素的X索引 + /// 第一个体素的Y索引 + /// 第一个体素的Z索引 + /// 第二个体素的X索引 + /// 第二个体素的Y索引 + /// 第二个体素的Z索引 + /// 曼哈顿距离(体素单位) + 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); + } + + /// + /// 检查指定索引的体素是否可通行 + /// + /// X方向索引 + /// Y方向索引 + /// Z方向索引 + /// 是否可通行(索引越界返回false) + public bool IsPassable(int x, int y, int z) + { + var cell = GetCell(x, y, z); + return cell != null && cell.IsPassable; + } + + /// + /// 检查世界坐标位置是否可通行 + /// + /// 世界坐标(模型单位) + /// 是否可通行 + public bool IsPassable(Point3D worldPos) + { + var (x, y, z) = WorldToVoxel(worldPos); + return IsPassable(x, y, z); + } + + /// + /// 获取网格的统计信息 + /// + /// (总体素数, 可通行体素数, 障碍物体素数) + 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); + } + + /// + /// 清除所有体素(重置为可通行状态) + /// + public void Clear() + { + InitializeCells(); + } + + /// + /// 返回网格的字符串表示 + /// + 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}模型单位]"; + } + } +}