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 路径编辑页签视图 - 采用与动画控制和分层管
+