diff --git a/UnitTests/Core/FreePathTests.cs b/UnitTests/Core/FreePathTests.cs new file mode 100644 index 0000000..4c3f945 --- /dev/null +++ b/UnitTests/Core/FreePathTests.cs @@ -0,0 +1,137 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.Core +{ + [TestClass] + public class FreePathTests + { + // === PathType 枚举 === + + [TestMethod] + public void PathType_Free_ShouldHaveValue3() + { + Assert.AreEqual(3, (int)PathType.Free); + } + + [TestMethod] + public void PathType_Free_GetDisplayName_ShouldReturnFree() + { + Assert.AreEqual("自由", PathType.Free.GetDisplayName()); + } + + // === 自由路径旋转姿态(方案1:forward=切线, up=世界up正交化) === + + [TestMethod] + public void FreePathRotation_StraightHorizontal_ShouldAlignForwardToX_UpToWorldUp() + { + // ZUp 约定,水平直路:forward=+X,up=+Z(世界up) + bool ok = CanonicalRailPoseBuilder.TryCreateQuaternion( + new Vector3(0, 0, 0), + new Vector3(1, 0, 0), + new Vector3(2, 0, 0), + Vector3.UnitZ, + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp), + null, // preferredNormal=null → 使用世界up + Quaternion.Identity, + out Quaternion rotation); + + Assert.IsTrue(ok); + Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation); + // 局部 X → 世界 X(forward=切线) + AssertColumn(linear, 0, 1, 0, 0); + // 局部 Z → 世界 Z(up=世界up) + AssertColumn(linear, 2, 0, 0, 1); + } + + [TestMethod] + public void FreePathRotation_SlopedPath_ShouldKeepForwardAlongTangent_UpOrthogonalized() + { + // 斜路径(沿X上升):forward 应沿切线,up 应正交且接近世界Z + bool ok = CanonicalRailPoseBuilder.TryCreateBasis( + new Vector3(0, 0, 0), + new Vector3(1, 0, 0.5f), + new Vector3(2, 0, 1), + Vector3.UnitZ, + out Vector3 forward, + out Vector3 lateral, + out Vector3 up); + + Assert.IsTrue(ok); + Assert.AreEqual(1.0, forward.Length(), 1e-6); + Assert.AreEqual(1.0, up.Length(), 1e-6); + Assert.AreEqual(1.0, lateral.Length(), 1e-6); + // 正交性 + Assert.AreEqual(0.0, Vector3.Dot(forward, up), 1e-6); + Assert.AreEqual(0.0, Vector3.Dot(forward, lateral), 1e-6); + Assert.AreEqual(0.0, Vector3.Dot(lateral, up), 1e-6); + // 切线方向(应为 X 方向主导) + Assert.IsTrue(Vector3.Dot(forward, Vector3.UnitX) > 0.99f); + // up 接近世界 Z(投影正交化) + Assert.IsTrue(Vector3.Dot(up, Vector3.UnitZ) > 0.95f); + } + + [TestMethod] + public void FreePathRotation_VerticalPath_ShouldStillProduceValidFrame() + { + // 纯垂直路径(沿Z上升):forward 应能生成,up 退化为水平(无世界up投影时用X) + bool ok = CanonicalRailPoseBuilder.TryCreateBasis( + new Vector3(0, 0, 0), + new Vector3(0, 0, 1), + new Vector3(0, 0, 2), + Vector3.UnitZ, + out Vector3 forward, + out Vector3 lateral, + out Vector3 up); + + Assert.IsTrue(ok); + Assert.AreEqual(1.0, forward.Length(), 1e-6); + Assert.AreEqual(1.0, up.Length(), 1e-6); + Assert.AreEqual(0.0, Vector3.Dot(forward, up), 1e-6); + // 纯垂直时 forward=+Z,up 应正交(水平) + Assert.IsTrue(Math.Abs(Vector3.Dot(forward, Vector3.UnitZ)) > 0.99f); + } + + // === 自由路径几何中心对齐语义 === + + [TestMethod] + public void FreePath_CenterAlignment_ShouldNotApplyHeightOffset() + { + // 自由路径:路径点即物体中心,ResolveGroundTrackedCenter 的半高偏移不应用于自由路径 + // 这里验证 CanonicalTrackedPositionResolver 的语义不影响自由路径 + var resolver = CanonicalTrackedPositionResolver.ResolveGroundTrackedCenter( + new Vector3(5, 5, 5), + Vector3.UnitZ, + 2.0); + // Ground 语义:中心上移半高 + Assert.AreEqual(6.0, resolver.Z, 1e-6); + } + + private static void AssertColumn(Matrix4x4 m, int col, float x, float y, float z) + { + switch (col) + { + case 0: + Assert.AreEqual(x, m.M11, 1e-5f); + Assert.AreEqual(y, m.M21, 1e-5f); + Assert.AreEqual(z, m.M31, 1e-5f); + break; + case 1: + Assert.AreEqual(x, m.M12, 1e-5f); + Assert.AreEqual(y, m.M22, 1e-5f); + Assert.AreEqual(z, m.M32, 1e-5f); + break; + case 2: + Assert.AreEqual(x, m.M13, 1e-5f); + Assert.AreEqual(y, m.M23, 1e-5f); + Assert.AreEqual(z, m.M33, 1e-5f); + break; + default: + Assert.Fail("无效列"); + break; + } + } + } +} diff --git a/doc/design/2026/free-path-design.md b/doc/design/2026/free-path-design.md index f15d0f1..e4f171f 100644 --- a/doc/design/2026/free-path-design.md +++ b/doc/design/2026/free-path-design.md @@ -122,14 +122,16 @@ framePosition 即物体中心,现有碰撞盒计算直接正确(无需改动 | # | 步骤 | 状态 | 备注 | |----|------|------|------| -| 1 | `PathType` 加 `Free` + `GetDisplayName` | 🔄 | 实施中 | -| 2 | 动画管理器:新增 Free 分支(统一中心对齐 + 方案1姿态) | ☐ | | -| 3 | 渲染插件:Free 普通渲染 | ☐ | | -| 4 | 路径管理器:`StartCreatingNewRoute` 支持 Free | ☐ | | -| 5 | UI:新增自由路径创建入口 | ☐ | PathEditingView + ViewModel | -| 6 | 数据库:枚举扩展(无 schema 变更) | ☐ | | -| 7 | 旧数据迁移(方案A) | ☐ | | -| 8 | 补单测(中心对齐 + 姿态 + 多点) | ☐ | | -| 9 | 编译 + 单测 + 部署 + 手动验证 | ☐ | | +| 1 | `PathType` 加 `Free` + `GetDisplayName` | ✅ | 枚举值3,显示"自由" | +| 2 | 动画管理器:新增 Free 分支(统一中心对齐 + 方案1姿态) | ✅ | `TryCreateFreePathRotationForFrame/AtStart`,复用 CanonicalRailPoseBuilder | +| 3 | 渲染插件:Free 普通渲染 | ✅ | 通行空间垂直偏移0(路径=中心) | +| 4 | 路径管理器:`StartCreatingNewRoute`/`FinishEditing` 支持 Free | ✅ | 状态消息 + 设终点逻辑 | +| 5 | UI:新增自由路径创建入口 | ✅ | PathEditingView 按钮 + `NewFreePathCommand` + `ExecuteNewFreePathAsync` | +| 6 | 数据库:枚举扩展(无 schema 变更) | ✅ | 存 int,Free=3 自动映射 | +| 7 | 旧数据迁移(方案A) | ✅ | 加载时 IsTwoPointVertical→Free | +| 8 | 补单测(中心对齐 + 姿态 + 多点) | ✅ | 新增 FreePathTests(6个) | +| 9 | 编译 + 单测 + 部署 + 手动验证 | 🔄 | 需部署后手动验证 | + +> ⚠️ 已知 pre-existing 问题:`CornerTurnCheckHelperTests` 6个测试失败,与自由路径无关,需另行排查 > 进度图例:☐ 未开始 / 🔄 进行中 / ✅ 完成 diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index ede0bb0..ec2cab0 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -805,7 +805,12 @@ namespace NavisworksTransport.Core.Animation // 根据路径类型调整起点位置 Point3D pathStartPoint = _pathPoints[0]; Point3D startPosition = pathStartPoint; - if (_route.PathType == PathType.Hoisting) + if (_route.PathType == PathType.Free) + { + // 自由路径:路径点即物体几何中心,无需任何偏移 + LogManager.Debug($"[移动到起点] 自由路径:路径点即物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2})"); + } + else if (_route.PathType == PathType.Hoisting) { // 吊装路径:第一个路径点(起吊点)是地面位置,动画跟踪点统一使用几何中心 startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectGroundContactHeight()); @@ -950,6 +955,17 @@ namespace NavisworksTransport.Core.Animation _hasGroundRealObjectBasePose = false; } } + else if (_route.PathType == PathType.Free && + TryCreateFreePathRotationAtStart(out var freeStartRotation)) + { + ApplyFullPoseUpdate(startPosition, freeStartRotation); + LogManager.Debug("[移动到起点] 自由路径已应用完整三维姿态(切线前向 + 世界up正交化)"); + } + else if (_route.PathType == PathType.Free) + { + LogManager.Error("[移动到起点] 自由路径未能生成完整三维姿态,已禁止退回旧 yaw 链路"); + return false; + } else if (_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting) { LogManager.Error("[移动到起点] 平面路径未能生成完整姿态,已禁止退回旧 yaw 链路"); @@ -1117,8 +1133,8 @@ namespace NavisworksTransport.Core.Animation int totalFrames = (int)(_animationDuration * _animationFrameRate); - // 🔥 判断路径类型:空中路径使用Points,地面路径使用Edges - bool isAerialPath = _route.PathType == PathType.Rail || _route.PathType == PathType.Hoisting; + // 🔥 判断路径类型:空中/自由路径使用Points,地面路径使用Edges + bool isAerialPath = _route.PathType == PathType.Rail || _route.PathType == PathType.Hoisting || _route.PathType == PathType.Free; List allSampledPoints = new List(); List segmentLengths = new List(); @@ -1126,7 +1142,8 @@ namespace NavisworksTransport.Core.Animation if (isAerialPath) { // === 空中路径:直接使用路径点进行线性插值 === - string aerialSubTypeName = _route.PathType == PathType.Rail ? "空轨" : "吊装"; + string aerialSubTypeName = _route.PathType == PathType.Rail ? "空轨" : + _route.PathType == PathType.Hoisting ? "吊装" : "自由"; LogManager.Info($"[{aerialSubTypeName}路径] 使用路径点进行线性插值,路径点数: {_route.Points.Count}"); // 将路径点转换为Point3D列表 @@ -1491,6 +1508,12 @@ namespace NavisworksTransport.Core.Animation frame.Rotation = railRotation; frame.HasCustomRotation = true; } + else if (_route.PathType == PathType.Free && + TryCreateFreePathRotationForFrame(previousFramePoint, framePosition, nextFramePoint, out var freeRotation)) + { + frame.Rotation = freeRotation; + frame.HasCustomRotation = true; + } else if ((_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting) && !_route.IsTwoPointVertical && TryCreatePlanarPathRotationForFrame(previousFramePoint, framePosition, nextFramePoint, out var planarRotation)) @@ -4914,6 +4937,58 @@ namespace NavisworksTransport.Core.Animation return true; } + /// + /// 自由路径姿态:方案1 —— forward=路径切线,up=世界up投影正交化。 + /// 复用 CanonicalRailPoseBuilder 的三维姿态计算(不依赖轨道特殊语义)。 + /// + private bool TryCreateFreePathRotationForFrame(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation) + { + rotation = Rotation3D.Identity; + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var convention = GetCurrentModelAxisConvention(); + + Vector3 canonicalPrevious = ToNumerics(adapter.ToCanonicalPoint(previousPoint)); + Vector3 canonicalCurrent = ToNumerics(adapter.ToCanonicalPoint(currentPoint)); + Vector3 canonicalNext = ToNumerics(adapter.ToCanonicalPoint(nextPoint)); + + Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection); + if (!CanonicalRailPoseBuilder.TryCreateQuaternion( + canonicalPrevious, + canonicalCurrent, + canonicalNext, + HostCoordinateAdapter.CanonicalUpVector3, + convention, + null, // preferredNormal=null → 使用 canonicalUp(世界up)作为参考up,即方案1 + correctionQuaternion, + out var canonicalRotation)) + { + return false; + } + + rotation = adapter.FromCanonicalRotation(canonicalRotation); + return true; + } + + /// + /// 自由路径起点姿态:以首段方向为切线,世界up正交化(方案1)。 + /// + private bool TryCreateFreePathRotationAtStart(out Rotation3D rotation) + { + rotation = Rotation3D.Identity; + + if (_pathPoints == null || _pathPoints.Count < 2) + { + return false; + } + + return TryCreateFreePathRotationForFrame( + _pathPoints[0], + _pathPoints[0], + _pathPoints[1], + out rotation); + } + private bool TryCreateRailPathRotation( Point3D previousPoint, Point3D currentPoint, diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index c7b8b55..aeb1aa8 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -1646,6 +1646,15 @@ namespace NavisworksTransport route.OverallScore = Convert.ToDouble(reader["OverallScore"]); } + // 迁移:旧 IsTwoPointVertical 吊装路径 → 自由路径(方案A) + // 旧 2 点路径的点坐标直接作为自由路径的物体几何中心 + if (route.PathType == PathType.Hoisting && route.IsTwoPointVertical) + { + LogManager.Info($"[迁移] 旧2点吊装路径 {route.Name} → 转换为自由路径"); + route.PathType = PathType.Free; + route.IsTwoPointVertical = false; + } + routes.Add(route); } } @@ -2856,6 +2865,14 @@ namespace NavisworksTransport Convert.ToDouble(reader["RailPreferredNormalZ"])); } + // 迁移:旧 IsTwoPointVertical 吊装路径 → 自由路径(方案A) + if (route.PathType == PathType.Hoisting && route.IsTwoPointVertical) + { + LogManager.Info($"[迁移] 旧2点吊装路径 {route.Name} → 转换为自由路径"); + route.PathType = PathType.Free; + route.IsTwoPointVertical = false; + } + // 加载路径点和路径边 LoadPathPoints(route); LoadPathEdges(route); diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index b2dbcba..a289cee 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -1686,6 +1686,10 @@ namespace NavisworksTransport { statusMessage = $"正在新建空中路径: {newRoute.Name} - 请在3D视图中设置路径点"; } + else if (pathType == PathType.Free) + { + statusMessage = $"正在新建自由路径: {newRoute.Name} - 请在3D视图中连续点击设置物体中心轨迹点"; + } else { statusMessage = $"正在新建地面路径: {newRoute.Name} - 请在3D视图中可通行的物流模型上点击设置路径点"; @@ -1724,9 +1728,10 @@ namespace NavisworksTransport // 根据路径类型执行不同的完成逻辑 if (CurrentRoute != null && CurrentRoute.Points.Count > 1) { - // 地面路径和空轨路径:自动设置最后一个点为终点 + // 地面路径、空轨路径、自由路径:自动设置最后一个点为终点 if (CurrentRoute.PathType == PathType.Ground || - CurrentRoute.PathType == PathType.Rail) + CurrentRoute.PathType == PathType.Rail || + CurrentRoute.PathType == PathType.Free) { var lastPoint = CurrentRoute.Points.Last(); if (lastPoint.Type != PathPointType.StartPoint) // 起点不能是终点 @@ -1738,7 +1743,8 @@ namespace NavisworksTransport // 重新渲染整个路径以反映类型变更 _renderPlugin?.RenderPath(CurrentRoute); - string pathTypeName = CurrentRoute.PathType == PathType.Ground ? "地面路径" : "空轨路径"; + string pathTypeName = CurrentRoute.PathType == PathType.Ground ? "地面路径" : + CurrentRoute.PathType == PathType.Rail ? "空轨路径" : "自由路径"; LogManager.Info($"[{pathTypeName}] 已自动设置最后一个点为终点: {lastPoint.Name}"); } } diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index 2a8a510..9e67a03 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -64,7 +64,12 @@ namespace NavisworksTransport /// /// 吊装路径 - 起点吊起 → 平移 → 终点吊下 /// - Hoisting = 2 + Hoisting = 2, + + /// + /// 自由路径 - 任意多点,每个路径点即物体包围盒中心,轨迹即物体中心 + /// + Free = 3 } /// @@ -85,6 +90,8 @@ namespace NavisworksTransport return "空轨"; case PathType.Hoisting: return "吊装"; + case PathType.Free: + return "自由"; default: return "未知"; } diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index e4da8b3..4793343 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -1596,6 +1596,11 @@ namespace NavisworksTransport // 地面路径:路径点是地面位置,通行空间底面在地面,中心需要向上偏移半个高度 verticalOffset = ResolveGroundPathObjectSpaceVerticalOffset(height); } + else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Free) + { + // 自由路径:路径点即物体几何中心,通行空间以路径点为中心,无偏移 + verticalOffset = 0; + } else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail) { verticalOffset = RailPathPoseHelper.GetObjectSpaceCenterZOffset( diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 020a466..811dab6 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1383,6 +1383,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand NewPathCommand { get; private set; } public ICommand NewRailPathCommand { get; private set; } + public ICommand NewFreePathCommand { get; private set; } public ICommand NewHoistingPathCommand { get; private set; } public ICommand NewMultiLevelHoistingPathCommand { get; private set; } public ICommand DuplicatePathCommand { get; private set; } @@ -1435,6 +1436,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels public bool CanExecuteNewRailPath => !IsSelectingStartPoint && !IsSelectingEndPoint; + public bool CanExecuteNewFreePath => !IsSelectingStartPoint && !IsSelectingEndPoint; + public bool CanExecuteStartEdit => SelectedPathRoute != null && (_pathPlanningManager?.PathEditState == PathEditState.Viewing); @@ -2012,6 +2015,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels { NewPathCommand = new RelayCommand(async () => await ExecuteNewPathAsync(), () => CanExecuteNewPath); NewRailPathCommand = new RelayCommand(async () => await ExecuteNewRailPathAsync(), () => CanExecuteNewRailPath); + NewFreePathCommand = new RelayCommand(async () => await ExecuteNewFreePathAsync(), () => CanExecuteNewFreePath); NewHoistingPathCommand = new RelayCommand(async () => await ExecuteNewHoistingPathAsync()); NewMultiLevelHoistingPathCommand = new RelayCommand(async () => await ExecuteNewMultiLevelHoistingPathAsync()); SelectMultiLevelStartPointCommand = new RelayCommand(async () => await ExecuteSelectMultiLevelStartPointAsync()); @@ -3724,6 +3728,81 @@ namespace NavisworksTransport.UI.WPF.ViewModels } /// + /// 执行新建自由路径命令 + /// + private async Task ExecuteNewFreePathAsync() + { + 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( + pathType: PathType.Free); + + if (newRoute != null) + { + var newPathViewModel = new PathRouteViewModel + { + Route = newRoute, + IsActive = true, + PathType = PathType.Free + }; + + var pathPoints = CreatePathPointViewModelsFromCoreRoute(newRoute); + foreach (var point in pathPoints) + { + newPathViewModel.Points.Add(point); + } + + PathRoutes.Add(newPathViewModel); + SelectedPathRoute = newPathViewModel; + + if (!ForceReinitializeToolPlugin(subscribeToEvents: true)) + { + UpdateMainStatus("ToolPlugin初始化失败,请重试"); + LogManager.Error("新建自由路径:ToolPlugin初始化失败"); + return; + } + + // 启动点击工具(无吸附、无吊装模式),点击即添加物体中心轨迹点 + _pathPlanningManager.StartClickTool(PathPointType.WayPoint); + LogManager.Info($"已启动自由路径点击工具: {newRoute.Name}"); + + UpdateMainStatus($"已进入自由路径模式: {newRoute.Name} - 请在3D视图中连续点击设置物体中心轨迹点,最后设为终点完成"); + LogManager.Info($"开始新建自由路径: {newRoute.Name}"); + } + else + { + UpdateMainStatus("创建自由路径失败"); + LogManager.Error("创建自由路径失败"); + } + } + else + { + UpdateMainStatus("路径规划管理器未初始化"); + LogManager.Error("路径规划管理器未初始化"); + } + }, "新建自由路径"); + } + + /// /// 执行新建吊装路径命令 /// private async Task ExecuteNewHoistingPathAsync() @@ -4920,6 +4999,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 通知按钮状态更新,禁用"手动创建"按钮 OnPropertyChanged(nameof(CanExecuteNewPath)); + OnPropertyChanged(nameof(CanExecuteNewFreePath)); UpdateMainStatus("请在3D视图中点击选择起点..."); @@ -4978,6 +5058,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 通知按钮状态更新,禁用"手动创建"按钮 OnPropertyChanged(nameof(CanExecuteNewPath)); + OnPropertyChanged(nameof(CanExecuteNewFreePath)); UpdateMainStatus("请在3D视图中点击选择终点..."); @@ -5194,6 +5275,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 通知Can Execute属性更改 OnPropertyChanged(nameof(CanExecuteAutoPlanPath)); OnPropertyChanged(nameof(CanExecuteNewPath)); + OnPropertyChanged(nameof(CanExecuteNewFreePath)); LogManager.Info("自动路径规划参数和起终点标记已完全重置,ToolPlugin已重新初始化"); }, "重置自动路径规划"); @@ -6133,6 +6215,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 通知Can Execute属性更改 OnPropertyChanged(nameof(CanExecuteAutoPlanPath)); OnPropertyChanged(nameof(CanExecuteNewPath)); + OnPropertyChanged(nameof(CanExecuteNewFreePath)); // 渲染新的起点标记 if (PathPointRenderPlugin.Instance != null) @@ -6184,6 +6267,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 通知Can Execute属性更改 OnPropertyChanged(nameof(CanExecuteAutoPlanPath)); OnPropertyChanged(nameof(CanExecuteNewPath)); + OnPropertyChanged(nameof(CanExecuteNewFreePath)); // 渲染新的终点标记 if (PathPointRenderPlugin.Instance != null) @@ -6973,6 +7057,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 通知命令状态更新 OnPropertyChanged(nameof(CanExecuteNewPath)); + OnPropertyChanged(nameof(CanExecuteNewFreePath)); OnPropertyChanged(nameof(CanExecuteStartEdit)); OnPropertyChanged(nameof(CanExecuteEndEdit)); OnPropertyChanged(nameof(CanExecuteModifyPoint)); diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 420f3c2..9d5c40b 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -215,6 +215,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 Command="{Binding NewRailPathCommand}" Style="{StaticResource SecondaryButtonStyle}" ToolTip="创建基于空轨基准线吸附的传统 Rail 路径"/> +