diff --git a/NavisworksTransportPlugin.csproj b/NavisworksTransportPlugin.csproj index b39b774..ab979e2 100644 --- a/NavisworksTransportPlugin.csproj +++ b/NavisworksTransportPlugin.csproj @@ -181,6 +181,7 @@ + LogisticsControlPanel.xaml @@ -254,6 +255,7 @@ + diff --git a/doc/working/空轨支持方案.md b/doc/working/空轨支持方案.md new file mode 100644 index 0000000..4188036 --- /dev/null +++ b/doc/working/空轨支持方案.md @@ -0,0 +1,522 @@ +# 空轨支持方案 + +## 一、需求分析 + +### 1.1 核心需求 + +1. **新增空轨属性**:在物流属性系统中增加"空轨"类型 +2. **空轨路径生成**: + - 空轨走向固定,只需设置起点和终点 + - 自动提取空轨几何体的下表面中心线作为路径 +3. **路径类型区分**:路径需要标记为"地面路径"或"空轨路径" +4. **动画运行模式**: + - 地面路径:车辆中心点在路径点上 + - 空轨路径:车辆悬挂在空轨下方运行(车辆中心 = 路径点 - 车辆高度/2) +5. **碰撞检测**: + - 统一使用3D碰撞检测 + - 空轨路径需要排除空轨本身 +6. **限制条件**:空轨暂时只支持人工路径,不支持自动路径规划 + +### 1.2 使用场景 + +- **地面路径**:转运车在楼面、走廊、通道上运行 +- **空轨路径**:悬挂式输送设备在空轨上运行 + +### 1.3 已确认信息 + +1. ✅ **空轨模型**:已存在,不需要建模规范 +2. ✅ **空轨类型**:不需要分类型,只有一个"空轨"类型 +3. ✅ **车辆悬挂**:车辆悬挂在空轨下方 +4. ✅ **碰撞检测**:需要排除空轨本身 +5. ✅ **数据库迁移**:不需要,新字段使用默认值 +6. ✅ **向后兼容**:不需要向后兼容性 +7. ✅ **碰撞检测统一**:地面路径和空轨路径都使用3D碰撞检测 +8. ✅ **轨道几何**:轨道不是水平的,需要精确计算几何体数据 + +--- + +## 二、架构影响分析 + +### 2.1 涉及的核心模块 + +| 模块 | 影响程度 | 改动内容 | +|------|---------|---------| +| **物流属性系统** | 🟢 低 | 新增"空轨"枚举值 | +| **路径数据模型** | 🟡 中 | 新增路径类型字段 | +| **路径规划系统** | 🟡 中 | 新增空轨路径生成逻辑 | +| **动画系统** | 🟡 中 | 根据路径类型调整车辆位置 | +| **碰撞检测系统** | 🟢 低 | 空轨路径排除空轨本身 | +| **UI界面** | 🟡 中 | 路径类型选择、空轨路径生成 | + +--- + +## 三、详细实施方案 + +### 3.1 第一阶段:基础数据结构(1天) + +#### 3.1.1 新增空轨属性 + +**文件**:`CategoryAttributeManager.cs` + +```csharp +public enum LogisticsElementType +{ + // ... 现有类型 ... + + /// + /// 空轨 - 空中运输路径,车辆悬挂运行,权重0.7 + /// + 空轨 = 9, +} +``` + +#### 3.1.2 路径类型枚举 + +**文件**:`PathPlanningModels.cs` + +```csharp +/// +/// 路径类型 +/// +public enum PathType +{ + /// + /// 地面路径 - 车辆在地面运行 + /// + Ground = 0, + + /// + /// 空轨路径 - 车辆悬挂在空轨下方运行 + /// + Rail = 1, +} +``` + +#### 3.1.3 路径数据模型扩展 + +**文件**:`PathRouteViewModel.cs` + +```csharp +private PathType _pathType = PathType.Ground; + +public PathType PathType +{ + get => _pathType; + set => SetProperty(ref _pathType, value); +} +``` + +#### 3.1.4 路径数据库适配 + +**文件**:`PathDatabase.cs` + +```csharp +// 保存路径时,保存路径类型 +public void SavePath(PathRouteViewModel path) +{ + // ... 现有逻辑 ... + // 新增:保存路径类型 +} + +// 加载路径时,读取路径类型 +public PathRouteViewModel LoadPath(string pathId) +{ + // ... 现有逻辑 ... + // 新增:读取路径类型 +} +``` + +--- + +### 3.2 第二阶段:空轨路径生成(2天) + +#### 3.2.1 复用现有几何计算 + +**现有代码复用**: + +- `ChannelHeightDetector.AnalyzeChannelGeometry()` - 分析通道几何 +- `ChannelHeightDetector.CalculatePreciseHeightAtPosition()` - 计算精确高度 +- `GeometryHelper.ExtractTriangles()` - 提取三角形 +- `GeometryHelper.RayTriangleIntersect()` - 射线-三角形交点检测 + +#### 3.2.2 新增底部高度检测方法 + +**文件**:`ChannelHeightDetector.cs` + +新增方法: + +- `GetChannelBottomHeight(Point3D position, IEnumerable channelItems)` - 获取通道底面高度 +- `CalculateBottomPreciseHeightAtPosition(Point3D position, ChannelHeightInfo heightInfo, ModelItem channel)` - 计算底面精确高度 +- `TryRaycastFromBelow(Point3D position, ModelItem channel, double floorHeight, out double height)` - 从下方射线投射 +- `PerformRayTriangleIntersectionFromBelow(Point3D position, List triangles)` - 从下往上射线-三角形交点检测 +- `TryGetGeometricBottomHeight(Point3D position, ModelItem channel, out double height)` - View API获取底面高度 +- `AnalyzeBottomSurfaceHeightAtPosition(Point3D position, ModelItem channel, double floorHeight, double ceilingHeight)` - 分析底面表面高度 +- `TryMultiPointBottomSampling(Point3D position, ModelItem channel)` - 多点采样获取底面高度 + +#### 3.2.3 空轨中心线提取工具 + +**文件**:`RailGeometryHelper.cs`(新建) + +```csharp +/// +/// 空轨几何体辅助工具 +/// 负责提取空轨的下表面中心线 +/// +public static class RailGeometryHelper +{ + /// + /// 从空轨几何体提取下表面中心线 + /// + /// 空轨模型项 + /// 起点(2D坐标) + /// 终点(2D坐标) + /// 路径点列表(包含精确的Z坐标) + public static List ExtractRailCenterLine( + ModelItem railModel, + Point3D startPoint, + Point3D endPoint) + { + var pathPoints = new List(); + + try + { + LogManager.Info($"[空轨] 开始提取空轨中心线: {railModel.DisplayName}"); + + // 1. 创建高度检测器实例 + var heightDetector = new ChannelHeightDetector(); + var railItems = new List { railModel }; + + // 2. 计算起点和终点在X-Y平面上的投影距离 + double distance2D = Math.Sqrt( + Math.Pow(endPoint.X - startPoint.X, 2) + + Math.Pow(endPoint.Y - startPoint.Y, 2) + ); + + if (distance2D < 0.01) + { + LogManager.Warning("[空轨] 起点和终点重合"); + return pathPoints; + } + + // 3. 在起点和终点之间采样,计算每个采样点的精确Z坐标 + int sampleCount = Math.Max(2, (int)Math.Ceiling(distance2D / 0.5)); // 每0.5米一个采样点 + + for (int i = 0; i < sampleCount; i++) + { + double t = (double)i / (sampleCount - 1); + + // 线性插值计算X、Y坐标 + double x = startPoint.X + t * (endPoint.X - startPoint.X); + double y = startPoint.Y + t * (endPoint.Y - startPoint.Y); + double z = startPoint.Z; // 初始Z坐标 + + // 调用现有的GetChannelBottomHeight方法获取精确底面高度 + var samplePoint = new Point3D(x, y, z); // Z坐标会被方法更新 + var bottomHeight = heightDetector.GetChannelBottomHeight(samplePoint, railItems); + + pathPoints.Add(new Point3D(x, y, bottomHeight)); + } + + LogManager.Info($"[空轨] 生成 {pathPoints.Count} 个路径点,总长度: {distance2D:F2}m"); + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 提取空轨中心线失败: {ex.Message}"); + } + + return pathPoints; + } +} +``` + +#### 3.2.4 路径编辑器扩展 + +**文件**:`PathEditingViewModel.cs` + +新增方法: + +- `GeneratePathFromRail(ModelItem railModel, Point3D startPoint, Point3D endPoint)` - 从空轨生成路径 +- `SelectRailModelCommand` - 选择空轨命令 +- `SetPathStartPointCommand` - 设置起点命令 +- `SetPathEndPointCommand` - 设置终点命令 +- `GeneratePathFromRailCommand` - 生成路径命令 + +新增属性: + +- `SelectedPathType` - 选中的路径类型 +- `PathTypes` - 路径类型数组 +- `IsRailPathType` - 是否为空轨路径类型 +- `SelectedRailModel` - 选中的空轨模型 +- `SelectedRailModelName` - 选中的空轨模型名称 +- `PathStartPoint` - 路径起点 +- `PathStartPointDisplay` - 路径起点显示 +- `PathEndPoint` - 路径终点 +- `PathEndPointDisplay` - 路径终点显示 + +--- + +### 3.3 第三阶段:动画系统适配(1天) + +#### 3.3.1 车辆位置计算 + +**文件**:`PathAnimationManager.cs` + +```csharp +/// +/// 计算车辆在路径点上的位置 +/// +private Point3D CalculateVehiclePosition(Point3D pathPoint, PathType pathType) +{ + if (pathType == PathType.Rail) + { + // 空轨路径:车辆悬挂在空轨下方 + // 车辆中心点 = 空轨点 - 车辆高度的一半 + double vehicleCenterZ = pathPoint.Z - _virtualVehicleHeight / 2; + return new Point3D(pathPoint.X, pathPoint.Y, vehicleCenterZ); + } + else + { + // 地面路径:车辆中心点在路径点上 + return pathPoint; + } +} +``` + +#### 3.3.2 传递路径类型 + +**文件**:`PathAnimationManager.cs` + +修改方法: + +- `GenerateAnimationFrames(List pathPoints, PathType pathType)` - 传递路径类型参数 +- `PrecomputeCollisions(List pathPoints, PathType pathType, List railModels)` - 传递路径类型和空轨模型列表 +- `DetectFrameCollisions(AnimationFrame frame, PathType pathType, List collisionExclusions)` - 传递路径类型和排除列表 + +--- + +### 3.4 第四阶段:碰撞检测适配(1天) + +#### 3.4.1 空轨排除逻辑 + +**文件**:`PathAnimationManager.cs` + +修改方法: + +- `PrecomputeCollisions(List pathPoints, PathType pathType, List railModels)` - 新增空轨排除列表 +- `DetectFrameCollisions(AnimationFrame frame, PathType pathType, List collisionExclusions)` - 使用排除列表过滤 + +**核心逻辑**: + +```csharp +// 创建碰撞排除列表 +var collisionExclusions = new List(); + +// 如果是空轨路径,将空轨模型加入排除列表 +if (pathType == PathType.Rail && railModels != null) +{ + collisionExclusions.AddRange(railModels); + LogManager.Info($"[碰撞检测] 已排除 {railModels.Count} 个空轨模型"); +} + +// 在碰撞检测循环中跳过排除列表中的项目 +foreach (var modelItem in allModelItems) +{ + // 跳过排除列表中的项目(包括空轨本身) + if (collisionExclusions.Contains(modelItem)) + { + continue; + } + + // 统一使用3D碰撞检测 + double distance = BoundingBoxGeometryUtils.CalculateDistance(vehicleBoundingBox, modelBoundingBox); + + if (distance <= 0) // 相交或接触 + { + collisions.Add(new CollisionResult + { + Item1 = _animatedObject, + Item2 = modelItem, + Distance = distance + }); + } +} +``` + +--- + +### 3.5 第五阶段:UI界面适配(1天) + +#### 3.5.1 路径类型选择 + +**文件**:`PathEditingViewModel.cs` + +新增属性: + +- `PathTypes` - 路径类型数组 +- `SelectedPathType` - 选中的路径类型 +- `IsRailPathType` - 是否为空轨路径类型 + +#### 3.5.2 命令 + +**文件**:`PathEditingViewModel.cs` + +新增命令: + +- `GeneratePathFromRailCommand` - 从空轨生成路径命令 +- `SelectRailModelCommand` - 选择空轨命令 +- `SetPathStartPointCommand` - 设置起点命令 +- `SetPathEndPointCommand` - 设置终点命令 + +#### 3.5.3 UI布局 + +**文件**:`LogisticsControlPanel.xaml` + +新增控件: + +- 路径类型选择 ComboBox +- 空轨选择 StackPanel(仅在选择空轨类型时显示) + - 选择空轨按钮 + - 设置起点按钮 + - 设置终点按钮 + - 生成路径按钮 + +--- + +## 四、实施步骤 + +### 第一阶段:基础数据结构(1天) + +1. ✅ 新增"空轨"物流属性类型 +2. ✅ 新增路径类型枚举 +3. ✅ 扩展路径数据模型 +4. ✅ 适配数据库读写 + +### 第二阶段:空轨路径生成(2天) + +1. ✅ 在 `ChannelHeightDetector` 中新增底部高度检测方法 +2. ✅ 复用现有的几何计算代码 +3. ✅ 实现 `RailGeometryHelper`(调用现有方法) +4. ✅ 扩展路径编辑器 +5. ✅ UI界面适配 +6. ✅ 集成测试 + +### 第三阶段:动画系统适配(1天) + +1. ✅ 车辆位置计算适配 +2. ✅ 传递路径类型参数 +3. ✅ 动画播放测试 + +### 第四阶段:碰撞检测适配(1天) + +1. ✅ 实现空轨排除逻辑 +2. ✅ 统一使用3D碰撞检测 +3. ✅ 碰撞报告适配 +4. ✅ 集成测试 + +### 第五阶段:测试和优化(1天) + +1. ✅ 功能测试 +2. ✅ 性能测试 +3. ✅ 用户验收测试 + +**总计**:6天 + +--- + +## 五、风险评估 + +| 风险项 | 风险等级 | 缓解措施 | +|--------|---------|---------| +| 空轨几何体提取失败 | 🟢 低 | 空轨模型已存在,直接使用 | +| 车辆悬挂位置计算错误 | 🟡 中 | 充分测试,提供可视化验证 | +| 碰撞检测误报 | 🟡 中 | 排除空轨本身,调整检测参数 | +| 性能下降 | 🟢 低 | 空轨路径数量少,影响有限 | +| 几何计算性能 | 🟡 中 | 复用现有代码,优化采样策略 | + +--- + +## 六、总结 + +### 6.1 方案优势 + +- ✅ **代码复用最大化**:充分利用现有的几何计算代码 +- ✅ **架构统一**:空轨和地面路径使用相同的高度检测框架 +- ✅ **改动最小化**:只需新增底部检测方法,其他全部复用 +- ✅ **易于维护**:几何计算逻辑集中管理 +- ✅ **向后兼容**:不需要迁移,新字段使用默认值 +- ✅ **碰撞检测统一**:统一使用3D检测,简化逻辑 + +### 6.2 关键技术点 + +1. **复用现有代码**:`ChannelHeightDetector` 的几何计算方法 +2. **新增底部检测**:`GetChannelBottomHeight()` 方法 +3. **车辆悬挂计算**:车辆中心 = 空轨点 - 车辆高度/2 +4. **碰撞检测统一**:统一使用3D检测 +5. **排除空轨本身**:通过排除列表过滤空轨模型 +6. **精确几何计算**:支持倾斜、弯曲的轨道 + +### 6.3 技术难点 + +- 需要处理大量三角形面片(性能优化) +- 需要处理重叠三角形(选择最高点) +- 需要处理采样点不在任何三角形内的情况(使用默认值) + +### 6.4 预期成果 + +- ✅ 支持空轨路径生成 +- ✅ 支持车辆悬挂运行 +- ✅ 支持空轨碰撞检测(排除空轨本身) +- ✅ 保持与现有地面路径功能的兼容性 + +--- + +## 七、附录 + +### 7.1 文件清单 + +#### 新增文件 + +- `RailGeometryHelper.cs` - 空轨几何体辅助工具 + +#### 修改文件 + +- `CategoryAttributeManager.cs` - 新增空轨枚举 +- `PathPlanningModels.cs` - 新增路径类型枚举 +- `PathRouteViewModel.cs` - 新增路径类型字段 +- `PathDatabase.cs` - 适配路径类型读写 +- `ChannelHeightDetector.cs` - 新增底部高度检测方法 +- `PathEditingViewModel.cs` - 扩展路径编辑功能 +- `PathAnimationManager.cs` - 适配车辆位置和碰撞检测 +- `LogisticsControlPanel.xaml` - UI界面适配 + +### 7.2 方法清单 + +#### 新增方法(ChannelHeightDetector.cs) + +- `GetChannelBottomHeight(Point3D position, IEnumerable channelItems)` - 获取通道底面高度 +- `CalculateBottomPreciseHeightAtPosition(Point3D position, ChannelHeightInfo heightInfo, ModelItem channel)` - 计算底面精确高度 +- `TryRaycastFromBelow(Point3D position, ModelItem channel, double floorHeight, out double height)` - 从下方射线投射 +- `PerformRayTriangleIntersectionFromBelow(Point3D position, List triangles)` - 从下往上射线-三角形交点检测 +- `TryGetGeometricBottomHeight(Point3D position, ModelItem channel, out double height)` - View API获取底面高度 +- `AnalyzeBottomSurfaceHeightAtPosition(Point3D position, ModelItem channel, double floorHeight, double ceilingHeight)` - 分析底面表面高度 +- `TryMultiPointBottomSampling(Point3D position, ModelItem channel)` - 多点采样获取底面高度 + +#### 新增方法(RailGeometryHelper.cs) + +- `ExtractRailCenterLine(ModelItem railModel, Point3D startPoint, Point3D endPoint)` - 提取空轨中心线 + +#### 新增方法(PathEditingViewModel.cs) + +- `GeneratePathFromRail(ModelItem railModel, Point3D startPoint, Point3D endPoint)` - 从空轨生成路径 + +#### 新增方法(PathAnimationManager.cs) + +- `CalculateVehiclePosition(Point3D pathPoint, PathType pathType)` - 计算车辆位置 + +#### 修改方法(PathAnimationManager.cs) + +- `GenerateAnimationFrames(List pathPoints, PathType pathType)` - 传递路径类型参数 +- `PrecomputeCollisions(List pathPoints, PathType pathType, List railModels)` - 传递路径类型和空轨模型列表 +- `DetectFrameCollisions(AnimationFrame frame, PathType pathType, List collisionExclusions)` - 传递路径类型和排除列表 + +--- \ No newline at end of file diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index 3032d73..8f343e5 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -82,6 +82,7 @@ namespace NavisworksTransport MaxVehicleHeight REAL, SafetyMargin REAL, GridSize REAL, + PathType INTEGER, CreatedTime DATETIME, LastModified DATETIME ) @@ -221,8 +222,8 @@ namespace NavisworksTransport // 保存路径基本信息 var sql = @" INSERT OR REPLACE INTO PathRoutes - (Id, Name, TotalLength, EstimatedTime, TurnRadius, IsCurved, MaxVehicleLength, MaxVehicleWidth, MaxVehicleHeight, SafetyMargin, GridSize, CreatedTime, LastModified) - VALUES (@id, @name, @length, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @created, @modified) + (Id, Name, TotalLength, EstimatedTime, TurnRadius, IsCurved, MaxVehicleLength, MaxVehicleWidth, MaxVehicleHeight, SafetyMargin, GridSize, PathType, CreatedTime, LastModified) + VALUES (@id, @name, @length, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @created, @modified) "; using (var cmd = new SQLiteCommand(sql, _connection)) @@ -238,6 +239,7 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@maxHeight", route.MaxVehicleHeight); cmd.Parameters.AddWithValue("@safetyMargin", route.SafetyMargin); cmd.Parameters.AddWithValue("@gridSize", route.GridSize); + cmd.Parameters.AddWithValue("@pathType", (int)route.PathType); cmd.Parameters.AddWithValue("@created", route.CreatedTime); cmd.Parameters.AddWithValue("@modified", DateTime.Now); cmd.ExecuteNonQuery(); @@ -822,6 +824,7 @@ namespace NavisworksTransport MaxVehicleHeight = reader.IsDBNull(reader.GetOrdinal("MaxVehicleHeight")) ? 2.0 : Convert.ToDouble(reader["MaxVehicleHeight"]), SafetyMargin = reader.IsDBNull(reader.GetOrdinal("SafetyMargin")) ? 0.5 : Convert.ToDouble(reader["SafetyMargin"]), GridSize = reader.IsDBNull(reader.GetOrdinal("GridSize")) ? 0.5 : Convert.ToDouble(reader["GridSize"]), + PathType = reader.IsDBNull(reader.GetOrdinal("PathType")) ? PathType.Ground : (PathType)Convert.ToInt32(reader["PathType"]), CreatedTime = Convert.ToDateTime(reader["CreatedTime"]), LastModified = Convert.ToDateTime(reader["LastModified"]) }; diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index 4577017..2197f62 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -47,6 +47,9 @@ namespace NavisworksTransport // 自动路径规划模式标志 private bool _isInAutoPathMode = false; + // 空轨吸附模式标志 + private bool _enableRailSnapping = false; + // 路径点3D标记管理 private List _pathPointMarkers; @@ -848,18 +851,29 @@ namespace NavisworksTransport /// /// 点击类型 public void StartClickTool(PathPointType pointType) + { + StartClickTool(pointType, enableRailSnapping: false); + } + + /// + /// 启动点击工具(支持空轨吸附) + /// + /// 点击类型 + /// 是否启用空轨吸附 + public void StartClickTool(PathPointType pointType, bool enableRailSnapping) { try { CurrentPointType = pointType; + _enableRailSnapping = enableRailSnapping; // 检查是否在自动路径模式 - 如果是,则不订阅PathPlanningManager的事件 bool shouldSubscribeToEvents = !IsInAutoPathMode; - LogManager.Info($"StartClickTool - 自动路径模式: {IsInAutoPathMode}, 订阅事件: {shouldSubscribeToEvents}"); + LogManager.Info($"StartClickTool - 自动路径模式: {IsInAutoPathMode}, 订阅事件: {shouldSubscribeToEvents}, 空轨吸附: {enableRailSnapping}"); ActivateToolPlugin(shouldSubscribeToEvents); PathEditState = PathEditState.AddingPoints; - LogManager.Info($"点击工具已启动,类型: {pointType},事件订阅: {shouldSubscribeToEvents}"); + LogManager.Info($"点击工具已启动,类型: {pointType},事件订阅: {shouldSubscribeToEvents},空轨吸附: {enableRailSnapping}"); } catch (Exception ex) { @@ -1466,20 +1480,25 @@ namespace NavisworksTransport /// 开始新建路径 /// /// 路径名称 + /// 是否为空轨路径 /// 创建的新路径 - public PathRoute StartCreatingNewRoute(string routeName = null) + public PathRoute StartCreatingNewRoute(string routeName = null, bool isRailPath = false) { try { - // 自动选择所有可通行的物流模型(先检查是否有可通行的模型) - AutoSelectLogisticsChannels(); - - // 检查是否有可通行的物流模型 - if (_walkableAreas == null || _walkableAreas.Count == 0) + // 空轨路径不需要自动选择可通行物流模型 + if (!isRailPath) { - RaiseErrorOccurred("没有找到任何可通行的物流模型,请先为模型设置可通行的物流属性"); - // 不需要重置状态,因为还没有进入创建状态 - return null; + // 自动选择所有可通行的物流模型(先检查是否有可通行的模型) + AutoSelectLogisticsChannels(); + + // 检查是否有可通行的物流模型 + if (_walkableAreas == null || _walkableAreas.Count == 0) + { + RaiseErrorOccurred("没有找到任何可通行的物流模型,请先为模型设置可通行的物流属性"); + // 不需要重置状态,因为还没有进入创建状态 + return null; + } } // 设置为创建状态 @@ -1493,7 +1512,11 @@ namespace NavisworksTransport // 智能管理ToolPlugin状态 ManageToolPluginForEditState(); - RaiseStatusChanged($"正在新建路径: {newRoute.Name} - 请在3D视图中可通行的物流模型上点击设置路径点", PathPlanningStatusType.Info); + string statusMessage = isRailPath + ? $"正在新建空轨路径: {newRoute.Name} - 请在3D视图中点击空轨模型设置路径点(将自动吸附到基准路径)" + : $"正在新建路径: {newRoute.Name} - 请在3D视图中可通行的物流模型上点击设置路径点"; + + RaiseStatusChanged(statusMessage, PathPlanningStatusType.Info); return newRoute; } @@ -1537,8 +1560,8 @@ namespace NavisworksTransport } } - // 如果是创建模式,将当前路径添加到路径集合 - if (_pathEditState == PathEditState.Creating && CurrentRoute != null) + // 如果是创建模式或添加点模式,将当前路径添加到路径集合 + if ((PathEditState == PathEditState.Creating || PathEditState == PathEditState.AddingPoints) && CurrentRoute != null) { if (!_routes.Contains(CurrentRoute)) { @@ -2669,78 +2692,169 @@ namespace NavisworksTransport /// private void ProcessManualPathEditing(PickItemResult pickResult) { - // 检查当前选中的通道状态 - LogManager.Debug($"[手动编辑] 当前_selectedChannels状态: {(_walkableAreas == null ? "NULL" : $"包含{_walkableAreas.Count}个项目")}"); - - // 如果没有选中的通道,尝试实时搜索 - if (_walkableAreas == null || _walkableAreas.Count == 0) + try { - LogManager.Debug("[手动编辑] 没有预选通道,开始实时搜索可通行的物流模型"); - SearchAndSetTraversableChannels(); - } - - // 检查是否在可通行的物流模型内并处理点击 - if (_walkableAreas != null && _walkableAreas.Any()) - { - bool isInTraversableLogisticsModel = IsItemInSelectedChannels(pickResult.ModelItem) || - IsItemChildOfSelectedChannels(pickResult.ModelItem); - - LogManager.Debug($"[手动编辑] 在可通行的物流模型内: {isInTraversableLogisticsModel}"); - - if (isInTraversableLogisticsModel) + // 如果启用了空轨吸附,先吸附到基准路径 + Point3D clickedPoint = pickResult.Point; + if (_enableRailSnapping) { - // 手动路径编辑 - 根据当前模式处理点击 - if (PathEditState == PathEditState.AddingPoints) - { - // 添加路径点模式 - 使用预览点 - LogManager.Debug("[手动编辑] 设置预览点位置"); - var previewPoint = SetPreviewPoint(pickResult.Point); + clickedPoint = SnapToRailBaseline(clickedPoint); + } - if (previewPoint != null) - { - LogManager.Debug($"[手动编辑] ✓ 预览点已设置: {previewPoint.Name}"); - } - else - { - LogManager.Warning("[手动编辑] ✗ 预览点设置失败"); - } - } - else if (PathEditState == PathEditState.EditingPoint) + // 检查当前选中的通道状态 + LogManager.Debug($"[手动编辑] 当前_selectedChannels状态: {(_walkableAreas == null ? "NULL" : $"包含{_walkableAreas.Count}个项目")}"); + + // 如果没有选中的通道,尝试实时搜索 + if (_walkableAreas == null || _walkableAreas.Count == 0) + { + LogManager.Debug("[手动编辑] 没有预选通道,开始实时搜索可通行的物流模型"); + SearchAndSetTraversableChannels(); + } + + // 空轨路径直接添加路径点,不检查可通行物流模型 + if (_enableRailSnapping) + { + LogManager.Debug("[手动编辑] 空轨路径模式,直接添加路径点"); + var pathPoint = AddPathPointIn3D(clickedPoint); + if (pathPoint != null) { - // 修改路径点模式 - 设置预览位置 - LogManager.Debug("[手动编辑] 设置修改路径点预览位置"); - SetEditingPreviewPoint(pickResult.Point); - LogManager.Debug($"[手动编辑] ✓ 修改路径点预览位置已设置: ({pickResult.Point.X:F3}, {pickResult.Point.Y:F3}, {pickResult.Point.Z:F3})"); + LogManager.Debug($"[手动编辑] ✓ 空轨路径点添加成功: {pathPoint.Name}"); } else { - // 其他编辑模式 - 保持原有逻辑 - LogManager.Debug("[手动编辑] 调用AddPathPointIn3D添加路径点"); - var pathPoint = AddPathPointIn3D(pickResult.Point); + LogManager.Warning("[手动编辑] ✗ 空轨路径点添加失败"); + } + } + // 地面路径检查是否在可通行的物流模型内并处理点击 + else if (_walkableAreas != null && _walkableAreas.Any()) + { + bool isInTraversableLogisticsModel = IsItemInSelectedChannels(pickResult.ModelItem) || + IsItemChildOfSelectedChannels(pickResult.ModelItem); - if (pathPoint != null) + LogManager.Debug($"[手动编辑] 在可通行的物流模型内: {isInTraversableLogisticsModel}"); + + if (isInTraversableLogisticsModel) + { + // 手动路径编辑 - 根据当前模式处理点击 + if (PathEditState == PathEditState.AddingPoints) { - LogManager.Debug($"[手动编辑] ✓ 路径点添加成功: {pathPoint.Name}"); + // 添加路径点模式 - 使用预览点 + LogManager.Debug("[手动编辑] 设置预览点位置"); + var previewPoint = SetPreviewPoint(clickedPoint); + + if (previewPoint != null) + { + LogManager.Debug($"[手动编辑] ✓ 预览点已设置: {previewPoint.Name}"); + } + else + { + LogManager.Warning("[手动编辑] ✗ 预览点设置失败"); + } + } + else if (PathEditState == PathEditState.EditingPoint) + { + // 修改路径点模式 - 设置预览位置 + LogManager.Debug("[手动编辑] 设置修改路径点预览位置"); + SetEditingPreviewPoint(clickedPoint); + LogManager.Debug($"[手动编辑] ✓ 修改路径点预览位置已设置: ({clickedPoint.X:F3}, {clickedPoint.Y:F3}, {clickedPoint.Z:F3})"); } else { - LogManager.Warning("[手动编辑] ✗ 路径点添加失败"); + // 其他编辑模式 - 保持原有逻辑 + LogManager.Debug("[手动编辑] 调用AddPathPointIn3D添加路径点"); + var pathPoint = AddPathPointIn3D(clickedPoint); + + if (pathPoint != null) + { + LogManager.Debug($"[手动编辑] ✓ 路径点添加成功: {pathPoint.Name}"); + } + else + { + LogManager.Warning("[手动编辑] ✗ 路径点添加失败"); + } } } + else + { + LogManager.Debug("[手动编辑] ✗ 点击位置不在可通行的物流模型内"); + RaiseErrorOccurred("点击位置不在物流通道内,请选择有效的物流路径位置"); + } } else { - LogManager.Debug("[手动编辑] ✗ 点击位置不在可通行的物流模型内"); - RaiseErrorOccurred("点击位置不在物流通道内,请选择有效的物流路径位置"); + LogManager.Warning("[手动编辑] ✗ 未找到可通行的物流模型"); + RaiseErrorOccurred("未找到可通行的物流通道,请先选择或配置物流通道"); } } - else + catch (Exception ex) { - LogManager.Warning("[手动编辑] ✗ 未找到可通行的物流模型"); - RaiseErrorOccurred("未找到可通行的物流通道,请先选择或配置物流通道"); + LogManager.Error($"[手动编辑] 处理异常: {ex.Message}"); + LogManager.Error($"[手动编辑] 堆栈: {ex.StackTrace}"); } } + /// + /// 将点击点吸附到空轨基准路径 + /// + private Point3D SnapToRailBaseline(Point3D clickedPoint) + { + try + { + // 获取所有空轨基准路径 + var baselinePaths = PathPointRenderPlugin.Instance?.GetAllRailBaselinePaths(); + if (baselinePaths == null || baselinePaths.Count == 0) + { + LogManager.Warning("[空轨吸附] 没有找到空轨基准路径,不进行吸附"); + return clickedPoint; + } + + // 找到最近的基准路径点 + Point3D nearestPoint = null; + double minDistance = double.MaxValue; + double maxSnapDistance = 2.0; // 最大吸附距离(模型单位) + + foreach (var pathPoints in baselinePaths.Values) + { + foreach (var point in pathPoints) + { + double distance = CalculateDistance3D(clickedPoint, point); + if (distance < minDistance) + { + minDistance = distance; + nearestPoint = point; + } + } + } + + if (nearestPoint != null && minDistance <= maxSnapDistance) + { + LogManager.Info($"[空轨吸附] 点击点 ({clickedPoint.X:F2}, {clickedPoint.Y:F2}, {clickedPoint.Z:F2}) 吸附到基准路径点 ({nearestPoint.X:F2}, {nearestPoint.Y:F2}, {nearestPoint.Z:F2}),距离 {minDistance:F2}"); + return nearestPoint; + } + else + { + LogManager.Warning($"[空轨吸附] 未找到合适的基准路径点(最近距离: {minDistance:F2},最大吸附距离: {maxSnapDistance})"); + return clickedPoint; + } + } + catch (Exception ex) + { + LogManager.Error($"[空轨吸附] 吸附失败: {ex.Message}"); + return clickedPoint; + } + } + + /// + /// 计算3D距离 + /// + private double CalculateDistance3D(Point3D a, Point3D b) + { + double dx = a.X - b.X; + double dy = a.Y - b.Y; + double dz = a.Z - b.Z; + return Math.Sqrt(dx * dx + dy * dy + dz * dz); + } + /// /// 搜索并设置可通行的通道 /// @@ -2963,6 +3077,16 @@ namespace NavisworksTransport try { + // 空轨路径不进行曲线化,直接保存 + if (route.PathType == PathType.Rail) + { + LogManager.Info($"空轨路径跳过曲线化,直接保存: {route.Name}"); + SavePathToDatabase(route); + LogManager.Info($"空轨路径已保存到数据库: {route.Name}"); + return; + } + + // 地面路径进行曲线化 double samplingStep = ConfigManager.Instance.Current.PathEditing.ArcSamplingStep; route.TurnRadius = ConfigManager.Instance.Current.PathEditing.DefaultPathTurnRadius; List warnings; diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index 9202059..d404370 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -44,7 +44,23 @@ namespace NavisworksTransport /// EditingPoint } - + + /// + /// 路径类型枚举 + /// + public enum PathType + { + /// + /// 地面路径 - 车辆在地面运行 + /// + Ground = 0, + + /// + /// 空轨路径 - 车辆悬挂在空轨下方运行 + /// + Rail = 1 + } + /// /// 通道检测结果 /// @@ -546,6 +562,11 @@ namespace NavisworksTransport /// public bool IsCurved { get; set; } = false; + /// + /// 路径类型 + /// + public PathType PathType { get; set; } = PathType.Ground; + // 数据库分析相关属性 /// /// 碰撞数量(从数据库加载) diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index c3cba31..bfe4918 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -118,7 +118,12 @@ namespace NavisworksTransport /// /// 安全警告样式(红色) /// - SafetyWarning + SafetyWarning, + + /// + /// 空轨基准路径样式(浅红色) + /// + RailBaseline } /// @@ -367,6 +372,9 @@ namespace NavisworksTransport // 预览连线标记 private List _previewLines = new List(); + // 空轨基准路径可视化 + private Dictionary _railBaselineVisualizations = new Dictionary(); + // 当前网格大小(米),用于自适应点大小计算 private double _currentGridSizeInMeters; @@ -495,14 +503,16 @@ namespace NavisworksTransport // 检查是否有路径或预览点需要渲染 int pathCount; + int railBaselineCount; bool hasPreviewPoint; lock (_lockObject) { pathCount = _pathVisualizations.Count; + railBaselineCount = _railBaselineVisualizations.Count; hasPreviewPoint = _previewMarker != null; } - if (pathCount == 0 && !hasPreviewPoint) + if (pathCount == 0 && railBaselineCount == 0 && !hasPreviewPoint) { return; } @@ -591,6 +601,19 @@ namespace NavisworksTransport graphics.Cylinder(previewLine.StartPoint, previewLine.EndPoint, previewLine.Radius); } } + + // 渲染空轨基准路径(浅红色) + if (_railBaselineVisualizations.Count > 0) + { + foreach (var visualization in _railBaselineVisualizations.Values) + { + foreach (var pathLineMarker in visualization.PathLineMarkers) + { + graphics.Color(pathLineMarker.Color, 0.7); // 使用70%透明度 + graphics.Cylinder(pathLineMarker.StartPoint, pathLineMarker.EndPoint, pathLineMarker.Radius); + } + } + } } graphics.EndModelContext(); @@ -854,6 +877,129 @@ namespace NavisworksTransport } } + /// + /// 渲染空轨基准路径 + /// + /// 空轨模型ID + /// 基准路径点列表 + public void RenderRailBaseline(string railModelId, List baselinePoints) + { + if (string.IsNullOrEmpty(railModelId) || baselinePoints == null || baselinePoints.Count < 2) + { + LogManager.Warning($"[路径渲染] RenderRailBaseline参数无效: railModelId={railModelId}, points={(baselinePoints?.Count ?? 0)}"); + return; + } + + try + { + LogManager.Info($"[路径渲染] 开始渲染空轨基准路径: {railModelId}, {baselinePoints.Count} 个点"); + + // 先清除该模型的旧基准路径可视化 + ClearRailBaseline(railModelId); + + // 创建PathVisualization对象 + var visualization = new PathVisualization + { + PathId = railModelId, + ShowControlVisualization = false, // 不显示控制点 + LastUpdated = DateTime.Now + }; + + var renderStyle = GetRenderStyle(RenderStyleName.RailBaseline); + LogManager.Info($"[路径渲染] RailBaseline样式: R={renderStyle.Color.R}, G={renderStyle.Color.G}, B={renderStyle.Color.B}, Alpha={renderStyle.Alpha}"); + + // 创建连线标记 + for (int i = 0; i < baselinePoints.Count - 1; i++) + { + var start = baselinePoints[i]; + var end = baselinePoints[i + 1]; + + var lineMarker = new LineMarker + { + StartPoint = start, + EndPoint = end, + Color = renderStyle.Color, + Radius = GetLineRadius() * 0.2, + SegmentType = PathSegmentType.Straight, + FromIndex = i, + ToIndex = i + 1 + }; + + visualization.PathLineMarkers.Add(lineMarker); + } + + LogManager.Info($"[路径渲染] 创建了 {visualization.PathLineMarkers.Count} 个连线标记"); + + lock (_lockObject) + { + _railBaselineVisualizations[railModelId] = visualization; + LogManager.Info($"[路径渲染] 已添加到字典,当前共有 {_railBaselineVisualizations.Count} 个基准路径"); + } + + RequestViewRefresh(); + LogManager.Info($"[路径渲染] 已请求视图刷新"); + } + catch (Exception ex) + { + LogManager.Error($"[路径渲染] 渲染空轨基准路径失败: {ex.Message}", ex); + } + } + + /// + /// 清除指定空轨的基准路径可视化 + /// + /// 空轨模型ID + public void ClearRailBaseline(string railModelId) + { + if (string.IsNullOrEmpty(railModelId)) + { + return; + } + + lock (_lockObject) + { + if (_railBaselineVisualizations.Remove(railModelId)) + { + RequestViewRefresh(); + LogManager.Info($"[路径渲染] 已清除空轨基准路径: {railModelId}"); + } + } + } + + /// + /// 获取所有空轨基准路径 + /// + /// 所有空轨基准路径的字典 + public Dictionary> GetAllRailBaselinePaths() + { + lock (_lockObject) + { + var result = new Dictionary>(); + foreach (var kvp in _railBaselineVisualizations) + { + result[kvp.Key] = kvp.Value.PathLineMarkers.Select(m => m.StartPoint).ToList(); + } + return result; + } + } + + /// + /// 清除所有空轨基准路径可视化 + /// + public void ClearAllRailBaselines() + { + lock (_lockObject) + { + int count = _railBaselineVisualizations.Count; + _railBaselineVisualizations.Clear(); + if (count > 0) + { + RequestViewRefresh(); + LogManager.Info($"[路径渲染] 已清除所有空轨基准路径, 共 {count} 个"); + } + } + } + /// /// 清空所有路径 /// @@ -942,9 +1088,30 @@ namespace NavisworksTransport // 构建控制点连线(用户意图,半透明) BuildControlLines(visualization, sortedPoints); - // 所有模式都使用曲线化后的路径(Edges) - if (visualization.PathRoute.Edges != null && visualization.PathRoute.Edges.Count > 0) + // 地面路径使用曲线化后的路径(Edges) + // 空轨路径直接使用控制点连线作为路径连线 + if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) { + // 空轨路径:将控制点连线复制到路径连线,使用不透明样式 + foreach (var controlLine in visualization.ControlLineMarkers) + { + var pathLineMarker = new LineMarker + { + StartPoint = controlLine.StartPoint, + EndPoint = controlLine.EndPoint, + Color = GetRenderStyle(RenderStyleName.Line).Color, + Radius = GetLineRadius(), + SegmentType = PathSegmentType.Straight, + FromIndex = controlLine.FromIndex, + ToIndex = controlLine.ToIndex + }; + visualization.PathLineMarkers.Add(pathLineMarker); + } + LogManager.Debug($"[路径渲染] 空轨路径使用控制点连线,共 {visualization.PathLineMarkers.Count} 条"); + } + else if (visualization.PathRoute.Edges != null && visualization.PathRoute.Edges.Count > 0) + { + // 地面路径:使用曲线化后的路径 BuildPathLines(visualization, sortedPoints); } @@ -1371,6 +1538,9 @@ namespace NavisworksTransport case RenderStyleName.SafetyWarning: return new RenderStyle(Color.FromByteRGB(244, 67, 54), 0.85); // Material Red安全警告,15%透明 + case RenderStyleName.RailBaseline: + return new RenderStyle(Color.FromByteRGB(255, 138, 128), 0.7); // 浅红色,30%透明 + default: return new RenderStyle(Color.White, 1.0); // 默认白色,完全不透明 } diff --git a/src/Core/Properties/CategoryAttributeManager.cs b/src/Core/Properties/CategoryAttributeManager.cs index c85e276..d810de3 100644 --- a/src/Core/Properties/CategoryAttributeManager.cs +++ b/src/Core/Properties/CategoryAttributeManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Autodesk.Navisworks.Api; using NavisworksTransport.Utils; +using NavisworksTransport.PathPlanning; namespace NavisworksTransport { @@ -88,10 +89,15 @@ namespace NavisworksTransport /// 停车位 = 9, + /// + /// 空轨 - 空中运输路径,车辆悬挂运行,权重0.7 + /// + 空轨 = 10, + /// /// 无关项 - 不参与网格生成的大型构件(如地基、结构基础等) /// - 无关项 = 10 + 无关项 = 11 } /// @@ -170,6 +176,13 @@ namespace NavisworksTransport propertyInternalNames); LogManager.Info($"[属性添加] 添加操作完成,成功添加 {successCount} 个模型的属性"); + + // 如果是空轨类型,预计算基准路径 + if (elementType == LogisticsElementType.空轨 && successCount > 0) + { + PreCalculateRailBaselinePaths(items); + } + return successCount; } @@ -524,6 +537,52 @@ namespace NavisworksTransport return LogisticsElementType.障碍物; } + /// + /// 预计算空轨基准路径 + /// + /// 已设置为空轨的模型项集合 + private static void PreCalculateRailBaselinePaths(ModelItemCollection items) + { + if (items == null || items.Count == 0) + return; + + LogManager.Info($"[空轨] 开始预计算 {items.Count} 个空轨模型的基准路径"); + + int successCount = 0; + int failCount = 0; + + foreach (ModelItem item in items) + { + try + { + // 调用RailGeometryHelper提取基准路径 + var baselinePath = PathPlanning.RailGeometryHelper.ExtractRailBaselinePath(item); + + if (baselinePath != null && baselinePath.PathPoints != null && baselinePath.PathPoints.Count > 0) + { + successCount++; + LogManager.Info($"[空轨] 成功提取模型 {item.DisplayName} 的基准路径,共 {baselinePath.PathPoints.Count} 个点,总长度 {baselinePath.TotalLength:F2} 模型单位"); + + // 自动可视化基准路径 + var railModelId = $"{item.DisplayName}_{item.GetHashCode()}"; + PathPointRenderPlugin.Instance.RenderRailBaseline(railModelId, baselinePath.PathPoints); + } + else + { + failCount++; + LogManager.Warning($"[空轨] 模型 {item.DisplayName} 的基准路径提取失败或路径为空"); + } + } + catch (Exception ex) + { + failCount++; + LogManager.Error($"[空轨] 预计算模型 {item.DisplayName} 的基准路径时发生错误: {ex.Message}"); + } + } + + LogManager.Info($"[空轨] 基准路径预计算完成,成功 {successCount} 个,失败 {failCount} 个"); + } + /// /// 删除选定模型项的物流属性 /// diff --git a/src/PathPlanning/AutoPathFinder.cs b/src/PathPlanning/AutoPathFinder.cs index 4d5dd4d..cb57679 100644 --- a/src/PathPlanning/AutoPathFinder.cs +++ b/src/PathPlanning/AutoPathFinder.cs @@ -2362,7 +2362,7 @@ namespace NavisworksTransport.PathPlanning }; // 检查路径类型 - if (astarPath.Type == PathType.ClosestApproach) + if (astarPath.Type == Roy_T.AStar.Paths.PathType.ClosestApproach) { LogManager.Info($"[A*执行] 找到部分路径(最近接近),包含 {astarPath.Edges.Count + 1} 个网格点"); LogManager.Warning($"[A*执行] 无法完全到达目标点,已找到最接近的可达点"); diff --git a/src/PathPlanning/RailGeometryHelper.cs b/src/PathPlanning/RailGeometryHelper.cs new file mode 100644 index 0000000..9864504 --- /dev/null +++ b/src/PathPlanning/RailGeometryHelper.cs @@ -0,0 +1,1153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autodesk.Navisworks.Api; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.PathPlanning +{ + /// + /// 空轨几何体辅助工具 + /// 负责从空轨几何体提取下表面中心线,支持拐弯和倾斜 + /// + public static class RailGeometryHelper + { + /// + /// 空轨下表面信息 + /// + public class RailBottomSurfaceInfo { + /// + /// 下表面三角形列表 + /// + public List BottomTriangles { get; set; } + + /// + /// 包围盒 + /// + public BoundingBox3D Bounds { get; set; } + + /// + /// 最低Z坐标 + /// + public double MinZ { get; set; } + + /// + /// 最高Z坐标 + /// + public double MaxZ { get; set; } + + /// + /// 缓存时间 + /// + public DateTime CacheTime { get; set; } + } + + /// + /// 空轨基准路径信息(完整路径) + /// + public class RailBaselinePath + { + /// + /// 完整的基准路径点列表(下表面中心线) + /// + public List PathPoints { get; set; } + + /// + /// 路径总长度(模型单位) + /// + public double TotalLength { get; set; } + + /// + /// 包围盒 + /// + public BoundingBox3D Bounds { get; set; } + + /// + /// 起点 + /// + public Point3D StartPoint { get; set; } + + /// + /// 终点 + /// + public Point3D EndPoint { get; set; } + + /// + /// 关联的空轨模型项 + /// + public ModelItem RailModel { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } + } + + /// + /// 缓存字典(模型ID -> 下表面信息) + /// + private static readonly Dictionary _railCache = + new Dictionary(); + + /// + /// 基准路径缓存(模型ID -> 基准路径) + /// + private static readonly Dictionary _baselinePathCache = + new Dictionary(); + + /// + /// 缓存有效期(30分钟) + /// + private static readonly TimeSpan _cacheValidDuration = TimeSpan.FromMinutes(30); + + /// + /// 步骤1: 提取空轨完整基准路径(用于可视化和吸附) + /// + /// 空轨模型项 + /// 采样间隔(模型单位),默认0.5 + /// 空轨基准路径信息 + public static RailBaselinePath ExtractRailBaselinePath( + ModelItem railModel, + double samplingInterval = 0.5) + { + try + { + LogManager.Info($"[空轨] 步骤1: 开始提取完整基准路径: {railModel.DisplayName}"); + + // 检查缓存 + var cacheKey = $"baseline_{railModel.DisplayName}_{railModel.GetHashCode()}"; + if (_baselinePathCache.TryGetValue(cacheKey, out var cachedPath)) + { + // 检查缓存是否过期 + if (DateTime.Now - cachedPath.CreatedTime < _cacheValidDuration) + { + LogManager.Info($"[空轨] 使用缓存的基准路径"); + return cachedPath; + } + else + { + LogManager.Info($"[空轨] 基准路径缓存已过期,重新提取"); + _baselinePathCache.Remove(cacheKey); + } + } + + // 提取下表面信息 + var surfaceInfo = ExtractRailBottomSurface(railModel); + if (surfaceInfo == null || surfaceInfo.BottomTriangles.Count == 0) + { + throw new InvalidOperationException($"无法从空轨模型提取下表面三角形: {railModel.DisplayName}"); + } + + LogManager.Info($"[空轨] 提取到 {surfaceInfo.BottomTriangles.Count} 个下表面三角形"); + + // 提取骨架线 + var skeleton = ExtractSkeletonFromTriangles(surfaceInfo.BottomTriangles); + LogManager.Info($"[空轨] 提取到 {skeleton.Count} 个骨架点"); + + // 沿骨架线生成采样点(完整路径) + var pathPoints = GenerateSamplePointsAlongSkeleton(skeleton, samplingInterval); + LogManager.Info($"[空轨] 生成 {pathPoints.Count} 个采样点"); + + // 计算路径长度 + double totalLength = CalculatePathLength(pathPoints); + + // 创建基准路径对象 + var baselinePath = new RailBaselinePath + { + PathPoints = pathPoints, + TotalLength = totalLength, + Bounds = surfaceInfo.Bounds, + StartPoint = pathPoints.First(), + EndPoint = pathPoints.Last(), + RailModel = railModel, + CreatedTime = DateTime.Now + }; + + // 加入缓存 + _baselinePathCache[cacheKey] = baselinePath; + + LogManager.Info($"[空轨] 步骤1完成: 基准路径总长度 {totalLength:F2},{pathPoints.Count}个采样点"); + return baselinePath; + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 步骤1失败: {ex.Message}"); + throw; + } + } + + /// + /// 步骤3: 将点吸附到基准路径上(找到最近的路径点) + /// + /// 基准路径 + /// 输入点(用户点击的位置) + /// 最大吸附距离(模型单位),默认1.0 + /// 输出参数:吸附后的点 + /// 是否成功吸附 + public static bool SnapToBaselinePath( + RailBaselinePath baselinePath, + Point3D inputPoint, + double maxSnapDistance, + out Point3D snappedPoint) + { + snappedPoint = new Point3D(); + + try + { + if (baselinePath == null || baselinePath.PathPoints.Count == 0) + { + LogManager.Warning("[空轨] 基准路径为空,无法吸附"); + return false; + } + + LogManager.Info($"[空轨] 步骤3: 吸附点到基准路径,输入点=({inputPoint.X:F2}, {inputPoint.Y:F2}, {inputPoint.Z:F2})"); + + Point3D nearestPoint = baselinePath.PathPoints[0]; + double minDistance = double.MaxValue; + + // 找到最近的路径点 + foreach (var pathPoint in baselinePath.PathPoints) + { + double distance = CalculateDistance2D(inputPoint, pathPoint); + if (distance < minDistance) + { + minDistance = distance; + nearestPoint = pathPoint; + } + } + + // 检查是否在最大吸附距离内 + if (minDistance <= maxSnapDistance) + { + snappedPoint = nearestPoint; + LogManager.Info($"[空轨] 步骤3完成: 吸附到 ({nearestPoint.X:F2}, {nearestPoint.Y:F2}, {nearestPoint.Z:F2}),距离 {minDistance:F2}"); + return true; + } + else + { + LogManager.Warning($"[空轨] 步骤3失败: 超出最大吸附距离 {maxSnapDistance:F2}(实际距离 {minDistance:F2})"); + return false; + } + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 步骤3失败: {ex.Message}"); + return false; + } + } + + /// + /// 步骤4: 从基准路径提取指定区段的路径点 + /// + /// 基准路径 + /// 吸附后的起点 + /// 吸附后的终点 + /// 采样间隔(模型单位),默认0.5 + /// 路径点列表(包含精确的Z坐标) + public static List ExtractPathSegment( + RailBaselinePath baselinePath, + Point3D snappedStartPoint, + Point3D snappedEndPoint, + double samplingInterval = 0.5) + { + try + { + LogManager.Info($"[空轨] 步骤4: 提取路径段"); + LogManager.Info($"[空轨] 起点: ({snappedStartPoint.X:F2}, {snappedStartPoint.Y:F2}, {snappedStartPoint.Z:F2})"); + LogManager.Info($"[空轨] 终点: ({snappedEndPoint.X:F2}, {snappedEndPoint.Y:F2}, {snappedEndPoint.Z:F2})"); + + // 在基准路径中找到起点和终点的索引 + int startIndex = FindNearestPointIndex(baselinePath.PathPoints, snappedStartPoint); + int endIndex = FindNearestPointIndex(baselinePath.PathPoints, snappedEndPoint); + + if (startIndex == -1 || endIndex == -1) + { + throw new InvalidOperationException("无法在基准路径中找到起点或终点"); + } + + LogManager.Info($"[空轨] 起点索引: {startIndex}, 终点索引: {endIndex}"); + + // 确保起点在终点之前 + if (startIndex > endIndex) + { + int temp = startIndex; + startIndex = endIndex; + endIndex = temp; + LogManager.Info("[空轨] 交换起点和终点索引"); + } + + // 提取路径段 + var segmentPoints = new List(); + for (int i = startIndex; i <= endIndex; i++) + { + segmentPoints.Add(baselinePath.PathPoints[i]); + } + + // 如果采样间隔比基准路径的采样间隔大,进行重采样 + double baselineInterval = baselinePath.TotalLength / (baselinePath.PathPoints.Count - 1); + if (samplingInterval > baselineInterval * 1.5) + { + LogManager.Info($"[空轨] 需要重采样: 基准间隔{baselineInterval:F3},目标间隔{samplingInterval:F3}"); + segmentPoints = ResamplePath(segmentPoints, samplingInterval); + } + + LogManager.Info($"[空轨] 步骤4完成: 提取了 {segmentPoints.Count} 个路径点"); + return segmentPoints; + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 步骤4失败: {ex.Message}"); + throw; + } + } + + /// + /// 找到最近的路径点索引 + /// + private static int FindNearestPointIndex(List pathPoints, Point3D targetPoint) + { + int nearestIndex = -1; + double minDistance = double.MaxValue; + + for (int i = 0; i < pathPoints.Count; i++) + { + double distance = CalculateDistance2D(pathPoints[i], targetPoint); + if (distance < minDistance) + { + minDistance = distance; + nearestIndex = i; + } + } + + return nearestIndex; + } + + /// + /// 重采样路径 + /// + private static List ResamplePath(List path, double newInterval) + { + double totalLength = CalculatePathLength(path); + int sampleCount = Math.Max(2, (int)Math.Ceiling(totalLength / newInterval)); + + var resampled = new List(); + for (int i = 0; i < sampleCount; i++) + { + double progress = (double)i / (sampleCount - 1); + var point = InterpolateOnPath(path, progress); + resampled.Add(point); + } + + return resampled; + } + + /// + /// 清除缓存 + /// + public static void ClearCache() + { + _railCache.Clear(); + _baselinePathCache.Clear(); + LogManager.Info("[空轨] 缓存已清除"); + } + + /// + /// 清除指定模型的基准路径缓存 + /// + public static void ClearBaselinePathCache(ModelItem railModel) + { + var cacheKey = $"baseline_{railModel.DisplayName}_{railModel.GetHashCode()}"; + _baselinePathCache.Remove(cacheKey); + LogManager.Info($"[空轨] 已清除模型 {railModel.DisplayName} 的基准路径缓存"); + } + + /// + /// 兼容方法:从空轨几何体提取下表面中心线(一步完成) + /// + /// 空轨模型项 + /// 起点(3D坐标) + /// 终点(3D坐标) + /// 采样间隔(模型单位),默认0.5 + /// 路径点列表(包含精确的Z坐标) + public static List ExtractRailBottomCenterLine( + ModelItem railModel, + Point3D startPoint, + Point3D endPoint, + double samplingInterval = 0.5) + { + try + { + LogManager.Info($"[空轨] 开始提取下表面中心线: {railModel.DisplayName}"); + LogManager.Info($"[空轨] 起点: ({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2})"); + LogManager.Info($"[空轨] 终点: ({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})"); + LogManager.Info($"[空轨] 采样间隔: {samplingInterval:F2}"); + + // 1. 提取空轨下表面信息(带缓存) + var surfaceInfo = ExtractRailBottomSurface(railModel); + if (surfaceInfo == null || surfaceInfo.BottomTriangles.Count == 0) + { + throw new InvalidOperationException($"无法从空轨模型提取下表面三角形: {railModel.DisplayName}"); + } + + LogManager.Info($"[空轨] 提取到 {surfaceInfo.BottomTriangles.Count} 个下表面三角形"); + + // 2. 提取骨架线 + var skeleton = ExtractSkeletonFromTriangles(surfaceInfo.BottomTriangles); + LogManager.Info($"[空轨] 提取到 {skeleton.Count} 个骨架点"); + + // 3. 沿骨架线生成采样点 + var samplePoints = GenerateSamplePointsAlongSkeleton(skeleton, samplingInterval); + LogManager.Info($"[空轨] 生成 {samplePoints.Count} 个采样点"); + + // 4. 对每个采样点,从下方射线投射获取精确高度 + var pathPoints = new List(); + int successCount = 0; + int failCount = 0; + + foreach (var samplePoint in samplePoints) + { + try + { + double bottomZ = GetBottomHeightByRaycast( + samplePoint, + surfaceInfo.BottomTriangles, + surfaceInfo.MinZ); + + if (bottomZ > surfaceInfo.MinZ - 1000) // 检查是否有效 + { + pathPoints.Add(new Point3D( + samplePoint.X, + samplePoint.Y, + bottomZ)); + successCount++; + } + else + { + LogManager.Warning($"[空轨] 采样点 ({samplePoint.X:F2}, {samplePoint.Y:F2}) 未找到有效交点"); + failCount++; + } + } + catch (Exception ex) + { + LogManager.Warning($"[空轨] 采样点 ({samplePoint.X:F2}, {samplePoint.Y:F2}) 射线投射失败: {ex.Message}"); + failCount++; + } + } + + LogManager.Info($"[空轨] 提取完成: 成功 {successCount} 个点, 失败 {failCount} 个点"); + + if (pathPoints.Count < 2) + { + throw new InvalidOperationException($"提取的中心线点数不足: {pathPoints.Count} (最少需要2个)"); + } + + return pathPoints; + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 提取下表面中心线失败: {ex.Message}"); + throw; + } + } + + /// + /// 提取空轨下表面信息(带缓存) + /// + private static RailBottomSurfaceInfo ExtractRailBottomSurface(ModelItem railModel) + { + try + { + // 生成缓存键(使用DisplayName作为唯一标识) + var cacheKey = $"rail_{railModel.DisplayName}_{railModel.GetHashCode()}"; + + // 检查缓存 + if (_railCache.TryGetValue(cacheKey, out var cachedInfo)) + { + // 检查缓存是否过期 + if (DateTime.Now - cachedInfo.CacheTime < _cacheValidDuration) + { + LogManager.Debug($"[空轨] 使用缓存的下表面信息"); + return cachedInfo; + } + else + { + LogManager.Debug($"[空轨] 缓存已过期,重新提取"); + _railCache.Remove(cacheKey); + } + } + + // 提取所有三角形 + var allTriangles = GeometryHelper.ExtractTriangles(new[] { railModel }); + if (allTriangles.Count == 0) + { + LogManager.Warning($"[空轨] 模型 {railModel.DisplayName} 没有三角形数据"); + return null; + } + + LogManager.Info($"[空轨] 提取到 {allTriangles.Count} 个三角形"); + + // 筛选下表面三角形 + var bottomTriangles = FilterBottomTriangles(allTriangles); + LogManager.Info($"[空轨] 筛选出 {bottomTriangles.Count} 个下表面三角形"); + + if (bottomTriangles.Count == 0) + { + LogManager.Warning($"[空轨] 没有找到下表面三角形"); + return null; + } + + // 计算包围盒 + var bounds = CalculateBoundingBox(bottomTriangles); + + // 创建下表面信息 + var surfaceInfo = new RailBottomSurfaceInfo + { + BottomTriangles = bottomTriangles, + Bounds = bounds, + MinZ = bounds.Min.Z, + MaxZ = bounds.Max.Z, + CacheTime = DateTime.Now + }; + + // 加入缓存 + _railCache[cacheKey] = surfaceInfo; + + return surfaceInfo; + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 提取下表面信息失败: {ex.Message}"); + return null; + } + } + + /// + /// 筛选下表面三角形(法线向下,Z < 0) + /// + private static List FilterBottomTriangles(List allTriangles) + { + var bottomTriangles = new List(); + const double Z_THRESHOLD = -0.1; // 法线Z分量阈值(容差约5.7度) + + foreach (var triangle in allTriangles) + { + try + { + // 计算三角形法线 + var normal = CalculateTriangleNormal(triangle); + + // 判断是否为下表面(法线向下) + if (normal.Z < Z_THRESHOLD) + { + bottomTriangles.Add(triangle); + } + } + catch (Exception ex) + { + LogManager.Warning($"[空轨] 计算三角形法线失败: {ex.Message}"); + } + } + + return bottomTriangles; + } + + /// + /// 计算三角形法线 + /// + private static Point3D CalculateTriangleNormal(Triangle3D triangle) + { + // 计算两条边向量 + var edge1 = new Point3D( + triangle.Point2.X - triangle.Point1.X, + triangle.Point2.Y - triangle.Point1.Y, + triangle.Point2.Z - triangle.Point1.Z); + + var edge2 = new Point3D( + triangle.Point3.X - triangle.Point1.X, + triangle.Point3.Y - triangle.Point1.Y, + triangle.Point3.Z - triangle.Point1.Z); + + // 计算叉积(法线) + var normal = new Point3D( + edge1.Y * edge2.Z - edge1.Z * edge2.Y, + edge1.Z * edge2.X - edge1.X * edge2.Z, + edge1.X * edge2.Y - edge1.Y * edge2.X); + + // 归一化 + double length = Math.Sqrt(normal.X * normal.X + normal.Y * normal.Y + normal.Z * normal.Z); + if (length > 0.000001) + { + normal = new Point3D(normal.X / length, normal.Y / length, normal.Z / length); + } + + return normal; + } + + /// + /// 从下表面三角形提取骨架线(支持拐弯) + /// + private static List ExtractSkeletonFromTriangles(List bottomTriangles) + { + try + { + // 1. 计算每个三角形的中心点 + var triangleCenters = new List(); + foreach (var triangle in bottomTriangles) + { + var center = new Point3D( + (triangle.Point1.X + triangle.Point2.X + triangle.Point3.X) / 3, + (triangle.Point1.Y + triangle.Point2.Y + triangle.Point3.Y) / 3, + (triangle.Point1.Z + triangle.Point2.Z + triangle.Point3.Z) / 3 + ); + triangleCenters.Add(center); + } + + LogManager.Info($"[空轨] 计算了 {triangleCenters.Count} 个三角形中心点"); + + // 2. 按邻近度排序,形成路径 + var sortedPoints = SortPointsByProximity(triangleCenters); + LogManager.Info($"[空轨] 排序后得到 {sortedPoints.Count} 个骨架点"); + + // 2.5 使用全局PCA压缩对称点(适用于直线路径) + var compressedPath = CollapseSymmetricPointsByPCA(sortedPoints); + LogManager.Info($"[空轨] PCA压缩后得到 {compressedPath.Count} 个骨架点"); + + // 3. 简化路径(移除过近的点) + var simplifiedPath = SimplifyPath(compressedPath, 0.1); // 0.1米阈值 + LogManager.Info($"[空轨] 简化后得到 {simplifiedPath.Count} 个骨架点"); + + // 4. 平滑路径(可选) + var smoothedPath = SmoothPath(simplifiedPath); + LogManager.Info($"[空轨] 平滑后得到 {smoothedPath.Count} 个骨架点"); + + return smoothedPath; + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 提取骨架线失败: {ex.Message}"); + throw; + } + } + + /// + /// 按邻近度排序点(形成路径) + /// + private static List SortPointsByProximity(List points) + { + if (points.Count == 0) return new List(); + + var sorted = new List(); + var remaining = new List(points); + + // 从边界点开始(选择X最小的点) + var current = remaining.OrderBy(p => p.X).First(); + sorted.Add(current); + remaining.Remove(current); + + // 依次查找最近的点 + while (remaining.Count > 0) + { + Point3D nearest = null; + double minDistance = double.MaxValue; + + foreach (var point in remaining) + { + double distance = CalculateDistance2D(current, point); + if (distance < minDistance) + { + minDistance = distance; + nearest = point; + } + } + + if (nearest != null && minDistance < 10.0) // 10米阈值,避免跳跃 + { + sorted.Add(nearest); + remaining.Remove(nearest); + current = nearest; + } + else + { + // 没有找到合适的邻近点,结束 + break; + } + } + + return sorted; + } + + /// + /// 使用全局PCA压缩对称点(适用于直线路径) + /// + private static List CollapseSymmetricPointsByPCA(List points) + { + try + { + if (points.Count < 3) + { + return new List(points); + } + + LogManager.Info("[空轨] 开始PCA压缩对称点"); + + // 1. 计算质心 + var centroid = new Point3D( + points.Average(p => p.X), + points.Average(p => p.Y), + points.Average(p => p.Z) + ); + + // 2. 计算协方差矩阵 + double covXX = 0, covYY = 0, covZZ = 0; + double covXY = 0, covXZ = 0, covYZ = 0; + + foreach (var point in points) + { + double dx = point.X - centroid.X; + double dy = point.Y - centroid.Y; + double dz = point.Z - centroid.Z; + + covXX += dx * dx; + covYY += dy * dy; + covZZ += dz * dz; + covXY += dx * dy; + covXZ += dx * dz; + covYZ += dy * dz; + } + + covXX /= points.Count; + covYY /= points.Count; + covZZ /= points.Count; + covXY /= points.Count; + covXZ /= points.Count; + covYZ /= points.Count; + + // 3. 计算特征值和特征向量(3x3对称矩阵) + // 使用数值方法计算特征值 + var eigenvalues = new double[3]; + var eigenvectors = new Point3D[3]; + + ComputeEigen3x3(covXX, covYY, covZZ, covXY, covXZ, covYZ, eigenvalues, eigenvectors); + + // 4. 找到最小特征值对应的特征向量(压缩方向) + int minIndex = 0; + if (eigenvalues[1] < eigenvalues[minIndex]) minIndex = 1; + if (eigenvalues[2] < eigenvalues[minIndex]) minIndex = 2; + + var compressAxis = eigenvectors[minIndex]; + + LogManager.Info($"[空轨] PCA特征值: {eigenvalues[0]:F4}, {eigenvalues[1]:F4}, {eigenvalues[2]:F4}"); + LogManager.Info($"[空轨] 压缩方向: ({compressAxis.X:F3}, {compressAxis.Y:F3}, {compressAxis.Z:F3})"); + + // 5. 沿路径方向分段 + // 找到最大特征值对应的特征向量(路径方向) + int maxIndex = 0; + if (eigenvalues[1] > eigenvalues[maxIndex]) maxIndex = 1; + if (eigenvalues[2] > eigenvalues[maxIndex]) maxIndex = 2; + + var pathAxis = eigenvectors[maxIndex]; + + // 将点投影到路径方向 + var projected = points.Select(p => new + { + Point = p, + Position = Dot(new Point3D((p - centroid).X, (p - centroid).Y, (p - centroid).Z), pathAxis) + }).OrderBy(x => x.Position).ToList(); + + // 6. 分段压缩 + var segmentLength = 0.5; // 0.5米分段 + var segments = new List>(); + var currentSegment = new List(); + double currentStart = projected[0].Position; + + foreach (var item in projected) + { + if (item.Position - currentStart < segmentLength) + { + currentSegment.Add(item.Point); + } + else + { + segments.Add(currentSegment); + currentSegment = new List { item.Point }; + currentStart = item.Position; + } + } + if (currentSegment.Count > 0) + segments.Add(currentSegment); + + // 7. 每段压缩到中心 + var compressed = new List(); + foreach (var segment in segments) + { + var segmentCentroid = new Point3D( + segment.Average(p => p.X), + segment.Average(p => p.Y), + segment.Average(p => p.Z) + ); + compressed.Add(segmentCentroid); + } + + LogManager.Info($"[空轨] PCA压缩完成: {points.Count}个点 -> {compressed.Count}个点"); + + return compressed; + } + catch (Exception ex) + { + LogManager.Warning($"[空轨] PCA压缩失败: {ex.Message},返回原始点"); + return new List(points); + } + } + + /// + /// 计算3x3对称矩阵的特征值和特征向量 + /// + private static void ComputeEigen3x3(double xx, double yy, double zz, + double xy, double xz, double yz, + double[] eigenvalues, Point3D[] eigenvectors) + { + // 使用雅可比迭代法计算特征值和特征向量 + // 初始化为单位矩阵 + double[,] V = new double[3, 3] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } }; + double[,] A = new double[3, 3] { { xx, xy, xz }, { xy, yy, yz }, { xz, yz, zz } }; + + const int maxIterations = 100; + const double tolerance = 1e-10; + + for (int iter = 0; iter < maxIterations; iter++) + { + // 找到最大的非对角元素 + int p = 0, q = 1; + double maxOffDiag = Math.Abs(A[0, 1]); + + if (Math.Abs(A[0, 2]) > maxOffDiag) + { + maxOffDiag = Math.Abs(A[0, 2]); + p = 0; q = 2; + } + if (Math.Abs(A[1, 2]) > maxOffDiag) + { + maxOffDiag = Math.Abs(A[1, 2]); + p = 1; q = 2; + } + + if (maxOffDiag < tolerance) + break; + + // 计算旋转角度 + double theta; + if (A[p, p] == A[q, q]) + { + theta = Math.PI / 4; + } + else + { + theta = 0.5 * Math.Atan2(2 * A[p, q], A[q, q] - A[p, p]); + } + + double c = Math.Cos(theta); + double s = Math.Sin(theta); + + // 更新矩阵A + for (int i = 0; i < 3; i++) + { + if (i != p && i != q) + { + double Api = A[p, i]; + double Aqi = A[q, i]; + A[p, i] = c * Api - s * Aqi; + A[q, i] = s * Api + c * Aqi; + A[i, p] = A[p, i]; + A[i, q] = A[q, i]; + } + } + + double App = A[p, p]; + double Aqq = A[q, q]; + double Apq = A[p, q]; + + A[p, p] = c * c * App - 2 * s * c * Apq + s * s * Aqq; + A[q, q] = s * s * App + 2 * s * c * Apq + c * c * Aqq; + A[p, q] = 0; + A[q, p] = 0; + + // 更新特征向量矩阵V + for (int i = 0; i < 3; i++) + { + double Vip = V[i, p]; + double Viq = V[i, q]; + V[i, p] = c * Vip - s * Viq; + V[i, q] = s * Vip + c * Viq; + } + } + + // 特征值是对角元素 + eigenvalues[0] = A[0, 0]; + eigenvalues[1] = A[1, 1]; + eigenvalues[2] = A[2, 2]; + + // 特征向量是V的列 + eigenvectors[0] = new Point3D(V[0, 0], V[1, 0], V[2, 0]); + eigenvectors[1] = new Point3D(V[0, 1], V[1, 1], V[2, 1]); + eigenvectors[2] = new Point3D(V[0, 2], V[1, 2], V[2, 2]); + } + + /// + /// 点积 + /// + private static double Dot(Point3D a, Point3D b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z; + } + + /// + /// 点积(Vector3D版本) + /// + private static double Dot(Vector3D a, Point3D b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z; + } + + /// + /// 简化路径(移除过近的点) + /// + private static List SimplifyPath(List path, double threshold) + { + if (path.Count <= 2) return new List(path); + + var simplified = new List { path[0] }; + + for (int i = 1; i < path.Count - 1; i++) + { + double distanceToPrev = CalculateDistance2D(path[i], simplified.Last()); + double distanceToNext = CalculateDistance2D(path[i], path[i + 1]); + + if (distanceToPrev >= threshold || distanceToNext >= threshold) + { + simplified.Add(path[i]); + } + } + + simplified.Add(path.Last()); + return simplified; + } + + /// + /// 平滑路径(简单移动平均) + /// + private static List SmoothPath(List path) + { + if (path.Count <= 2) return new List(path); + + var smoothed = new List(); + + // 第一个点保持不变 + smoothed.Add(path[0]); + + // 中间点使用移动平均 + for (int i = 1; i < path.Count - 1; i++) + { + var prev = path[i - 1]; + var curr = path[i]; + var next = path[i + 1]; + + var smoothedPoint = new Point3D( + (prev.X + curr.X + next.X) / 3, + (prev.Y + curr.Y + next.Y) / 3, + (prev.Z + curr.Z + next.Z) / 3 + ); + + smoothed.Add(smoothedPoint); + } + + // 最后一个点保持不变 + smoothed.Add(path.Last()); + + return smoothed; + } + + /// + /// 沿骨架线生成采样点 + /// + private static List GenerateSamplePointsAlongSkeleton( + List skeleton, + double samplingInterval) + { + if (skeleton.Count < 2) + { + throw new ArgumentException("骨架线至少需要2个点"); + } + + var samplePoints = new List(); + + // 计算骨架线总长度 + double totalLength = CalculatePathLength(skeleton); + + // 计算采样点数 + int sampleCount = Math.Max(2, (int)Math.Ceiling(totalLength / samplingInterval)); + + LogManager.Info($"[空轨] 骨架线总长度: {totalLength:F2}, 采样点数: {sampleCount}"); + + // 沿骨架线均匀采样 + for (int i = 0; i < sampleCount; i++) + { + double progress = (double)i / (sampleCount - 1); + var point = InterpolateOnPath(skeleton, progress); + samplePoints.Add(point); + } + + return samplePoints; + } + + /// + /// 在路径上插值(支持拐弯) + /// + private static Point3D InterpolateOnPath(List path, double progress) + { + if (path.Count == 0) + throw new ArgumentException("路径不能为空"); + + if (path.Count == 1) + return path[0]; + + if (progress <= 0) return path[0]; + if (progress >= 1) return path.Last(); + + // 计算路径总长度 + double totalLength = CalculatePathLength(path); + double targetDistance = totalLength * progress; + + // 找到目标距离所在的线段 + double accumulatedLength = 0; + for (int i = 0; i < path.Count - 1; i++) + { + double dx = path[i + 1].X - path[i].X; + double dy = path[i + 1].Y - path[i].Y; + double dz = path[i + 1].Z - path[i].Z; + double segmentLength = Math.Sqrt(dx * dx + dy * dy + dz * dz); + + if (accumulatedLength + segmentLength >= targetDistance) + { + // 在当前线段内插值 + double segmentProgress = (targetDistance - accumulatedLength) / segmentLength; + return new Point3D( + path[i].X + segmentProgress * (path[i + 1].X - path[i].X), + path[i].Y + segmentProgress * (path[i + 1].Y - path[i].Y), + path[i].Z + segmentProgress * (path[i + 1].Z - path[i].Z) + ); + } + + accumulatedLength += segmentLength; + } + + // 如果超出范围,返回最后一个点 + return path.Last(); + } + + /// + /// 计算路径总长度 + /// + private static double CalculatePathLength(List path) + { + if (path.Count < 2) return 0; + + double totalLength = 0; + for (int i = 0; i < path.Count - 1; i++) + { + double dx = path[i + 1].X - path[i].X; + double dy = path[i + 1].Y - path[i].Y; + double dz = path[i + 1].Z - path[i].Z; + totalLength += Math.Sqrt(dx * dx + dy * dy + dz * dz); + } + + return totalLength; + } + + /// + /// 计算两点之间的距离(2D,忽略Z) + /// + private static double CalculateDistance2D(Point3D p1, Point3D p2) + { + double dx = p2.X - p1.X; + double dy = p2.Y - p1.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + /// + /// 从下方射线投射获取下表面高度 + /// + private static double GetBottomHeightByRaycast( + Point3D position, + List bottomTriangles, + double minZ) + { + try + { + // 创建垂直向上的射线(从下方足够远开始) + var rayOrigin = new Point3D( + position.X, + position.Y, + minZ - 1000.0); // 从最低点下方1km开始 + + var rayDirection = new Point3D(0, 0, 1); // 向上 + + // 执行射线与三角形的交点检测 + var intersectionPoints = new List(); + + foreach (var triangle in bottomTriangles) + { + if (GeometryHelper.RayTriangleIntersect(rayOrigin, rayDirection, triangle, out double intersectionZ)) + { + intersectionPoints.Add(intersectionZ); + } + } + + if (intersectionPoints.Count > 0) + { + // 选择最高的交点作为下表面高度 + return intersectionPoints.Max(); + } + else + { + // 没有交点,返回无效值 + return double.MinValue; + } + } + catch (Exception ex) + { + LogManager.Error($"[空轨] 射线投射失败: {ex.Message}"); + return double.MinValue; + } + } + + /// + /// 计算包围盒(复用Utils中的方法) + /// + private static BoundingBox3D CalculateBoundingBox(List triangles) + { + if (triangles.Count == 0) + { + return new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0)); + } + + double minX = double.MaxValue, minY = double.MaxValue, minZ = double.MaxValue; + double maxX = double.MinValue, maxY = double.MinValue, maxZ = double.MinValue; + + foreach (var triangle in triangles) + { + minX = Math.Min(minX, Math.Min(triangle.Point1.X, Math.Min(triangle.Point2.X, triangle.Point3.X))); + minY = Math.Min(minY, Math.Min(triangle.Point1.Y, Math.Min(triangle.Point2.Y, triangle.Point3.Y))); + minZ = Math.Min(minZ, Math.Min(triangle.Point1.Z, Math.Min(triangle.Point2.Z, triangle.Point3.Z))); + + maxX = Math.Max(maxX, Math.Max(triangle.Point1.X, Math.Max(triangle.Point2.X, triangle.Point3.X))); + maxY = Math.Max(maxY, Math.Max(triangle.Point1.Y, Math.Max(triangle.Point2.Y, triangle.Point3.Y))); + maxZ = Math.Max(maxZ, Math.Max(triangle.Point1.Z, Math.Max(triangle.Point2.Z, triangle.Point3.Z))); + } + + return new BoundingBox3D( + new Point3D(minX, minY, minZ), + new Point3D(maxX, maxY, maxZ)); + } + + } +} \ No newline at end of file diff --git a/src/UI/WPF/Converters/PathTypeConverter.cs b/src/UI/WPF/Converters/PathTypeConverter.cs new file mode 100644 index 0000000..66c7cbf --- /dev/null +++ b/src/UI/WPF/Converters/PathTypeConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NavisworksTransport.UI.WPF.Converters +{ + /// + /// 路径类型转换器,将PathType枚举转换为中文显示 + /// + public class PathTypeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is NavisworksTransport.PathType pathType) + { + switch (pathType) + { + case NavisworksTransport.PathType.Ground: + return "地面"; + case NavisworksTransport.PathType.Rail: + return "空轨"; + default: + return "未知"; + } + } + return "地面"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/UI/WPF/Models/PathRouteViewModel.cs b/src/UI/WPF/Models/PathRouteViewModel.cs index be5a5a5..68db2c5 100644 --- a/src/UI/WPF/Models/PathRouteViewModel.cs +++ b/src/UI/WPF/Models/PathRouteViewModel.cs @@ -182,6 +182,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private double _totalLength; private bool _isValidated; private string _validationStatus; + private NavisworksTransport.PathType _pathType; // UI状态管理 private readonly UIStateManager _uiStateManager; @@ -272,6 +273,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels set => SetProperty(ref _isOptimal, value); } + /// + /// 路径类型 + /// + public NavisworksTransport.PathType PathType + { + get => _pathType; + set => SetProperty(ref _pathType, value); + } + /// /// 创建时间 /// diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 9a07126..5893f87 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -246,10 +246,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Warning($"[路径可视化] 未找到对应的Core路径: {_selectedPathRoute.Name}"); } } - else - { - LogManager.Debug("[路径可视化] 没有选中的路径,仅清理显示"); - } // 3. 恢复网格可视化(如果网格可视化已启用且当前路径有关联的GridMap) if (_pathPlanningManager?.IsAnyGridVisualizationEnabled == true) @@ -638,6 +634,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels #region 命令 public ICommand NewPathCommand { get; private set; } + public ICommand NewRailPathCommand { get; private set; } public ICommand DeletePathCommand { get; private set; } public ICommand RenamePathCommand { get; private set; } public ICommand StartEditCommand { get; private set; } @@ -665,6 +662,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels public bool CanExecuteNewPath => !IsSelectingStartPoint && !IsSelectingEndPoint; + public bool CanExecuteNewRailPath => !IsSelectingStartPoint && !IsSelectingEndPoint; + public bool CanExecuteStartEdit => SelectedPathRoute != null && (_pathPlanningManager?.PathEditState == PathEditState.Viewing); @@ -802,6 +801,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private void InitializeCommands() { NewPathCommand = new RelayCommand(async () => await ExecuteNewPathAsync(), () => CanExecuteNewPath); + NewRailPathCommand = new RelayCommand(async () => await ExecuteNewRailPathAsync(), () => CanExecuteNewRailPath); DeletePathCommand = new RelayCommand(async () => await ExecuteDeletePathAsync()); RenamePathCommand = new RelayCommand(async () => await ExecuteRenamePathAsync()); StartEditCommand = new RelayCommand(async () => await ExecuteAddPathPointAsync(), () => CanExecuteStartEdit); @@ -910,6 +910,97 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "新建路径"); } + private async Task ExecuteNewRailPathAsync() + { + await SafeExecuteAsync(() => + { + UpdateMainStatus("正在创建新空轨路径..."); + + // 检查是否有空轨基准路径 + if (PathPointRenderPlugin.Instance != null) + { + try + { + // 清除所有现有路径的可视化显示 + PathPointRenderPlugin.Instance.ClearPathsExcept("grid_visualization_all", "grid_visualization_channel", "grid_visualization_unknown", "grid_visualization_obstacle", "grid_visualization_door"); + LogManager.Info("新建空轨路径:已清除现有路径可视化显示(保留网格可视化)"); + } + catch (Exception ex) + { + LogManager.Error($"新建空轨路径:清除现有路径可视化失败: {ex.Message}", ex); + throw; + } + } + + if (_pathPlanningManager != null) + { + var newRoute = _pathPlanningManager.StartCreatingNewRoute(isRailPath: true); + + if (newRoute != null) + { + // 设置路径类型为空轨 + newRoute.PathType = PathType.Rail; + + // 创建对应的 WPF ViewModel + var newPathViewModel = new PathRouteViewModel + { + Id = newRoute.Id, + Name = newRoute.Name, + Description = newRoute.Description, + IsActive = true, + PathType = PathType.Rail + }; + + // 转换路径点 + foreach (var corePoint in newRoute.Points) + { + var wpfPoint = new PathPointViewModel + { + Id = corePoint.Id, + Name = corePoint.Name, + X = corePoint.X, + Y = corePoint.Y, + Z = corePoint.Z, + Type = corePoint.Type + }; + newPathViewModel.Points.Add(wpfPoint); + } + + // 添加到 UI 列表并选中 + PathRoutes.Add(newPathViewModel); + SelectedPathRoute = newPathViewModel; + + // 强制重新初始化ToolPlugin以确保获得鼠标焦点 + if (!ForceReinitializeToolPlugin(subscribeToEvents: true)) + { + UpdateMainStatus("ToolPlugin初始化失败,请重试"); + LogManager.Error("新建空轨路径:ToolPlugin初始化失败"); + return; + } + + // 启动点击工具,设置空轨吸附模式 + _pathPlanningManager.StartClickTool(PathPointType.WayPoint, enableRailSnapping: true); + LogManager.Info($"已启动空轨路径点击工具(吸附模式): {newRoute.Name}"); + + UpdateMainStatus($"已进入新建空轨路径模式: {newRoute.Name} - 请在3D视图中点击空轨模型设置路径点(将自动吸附到基准路径)"); + LogManager.Info($"开始新建空轨路径: {newRoute.Name},已强制重新初始化ToolPlugin获取鼠标焦点"); + } + else + { + UpdateMainStatus("创建新空轨路径失败:没有可通行的物流模型"); + LogManager.Error("创建新空轨路径失败:没有可通行的物流模型"); + MessageBox.Show("创建新空轨路径失败:没有找到任何可通行的物流模型。\n请先为模型设置可通行的物流属性,然后再尝试创建路径。", "错误", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } + else + { + UpdateMainStatus("路径规划管理器未初始化"); + LogManager.Error("路径规划管理器未初始化"); + } + }, "新建空轨路径"); + } + private async Task ExecuteDeletePathAsync() { if (SelectedPathRoute == null) return; @@ -1366,13 +1457,24 @@ namespace NavisworksTransport.UI.WPF.ViewModels SelectedPathRoute.IsActive = true; UpdateMainStatus($"正在激活3D路径编辑模式: {SelectedPathRoute.Name}..."); + // 检查是否是空轨路径 + bool isRailPath = SelectedPathRoute.PathType == PathType.Rail; + // 启动PathPlanningManager的点击工具,这会设置正确的编辑状态 if (_pathPlanningManager != null) { try { - _pathPlanningManager.StartClickTool(PathPointType.WayPoint); - LogManager.Info($"已启动PathPlanningManager点击工具: {SelectedPathRoute.Name}"); + if (isRailPath) + { + _pathPlanningManager.StartClickTool(PathPointType.WayPoint, enableRailSnapping: true); + LogManager.Info($"已启动PathPlanningManager点击工具(空轨吸附模式): {SelectedPathRoute.Name}"); + } + else + { + _pathPlanningManager.StartClickTool(PathPointType.WayPoint); + LogManager.Info($"已启动PathPlanningManager点击工具: {SelectedPathRoute.Name}"); + } } catch (Exception ex) { @@ -1390,7 +1492,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels return; } - UpdateMainStatus($"已进入3D路径编辑模式: {SelectedPathRoute.Name} - 在3D视图中点击设置路径点"); + string modeText = isRailPath ? "(空轨吸附模式)" : ""; + UpdateMainStatus($"已进入3D路径编辑模式: {SelectedPathRoute.Name} {modeText} - 在3D视图中点击设置路径点"); LogManager.Info($"开始添加路径点: {SelectedPathRoute.Name},已启动点击工具"); // 手动触发按钮状态更新 @@ -3023,7 +3126,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels Id = coreRoute.Id, Name = coreRoute.Name, Description = coreRoute.Description, - IsActive = false // 历史路径默认不激活 + IsActive = false, // 历史路径默认不激活 + PathType = coreRoute.PathType }; // 设置时间信息 diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 06f6bd5..e241fd6 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -26,6 +26,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 +