diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index 0a8140c..aa14ce5 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -53,7 +53,9 @@ + + diff --git a/UnitTests/Core/PathHelperTests.cs b/UnitTests/Core/PathHelperTests.cs new file mode 100644 index 0000000..36bf856 --- /dev/null +++ b/UnitTests/Core/PathHelperTests.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.UnitTests.Core +{ + [TestClass] + public class PathHelperTests + { + [TestMethod] + public void BuildDuplicatedPathName_ShouldRebuildTrailingTimestamp() + { + var now = new DateTime(2026, 3, 28, 9, 45, 12); + + var duplicatedName = PathHelper.BuildDuplicatedPathName("人工_0328_081530", now); + + Assert.AreEqual("副本_人工_0328_094512", duplicatedName); + } + + [TestMethod] + public void BuildDuplicatedPathName_ShouldKeepPlainNameWithoutTimestamp() + { + var duplicatedName = PathHelper.BuildDuplicatedPathName("我的路径"); + + Assert.AreEqual("副本_我的路径", duplicatedName); + } + } +} diff --git a/UnitTests/Core/PathRouteCloneTests.cs b/UnitTests/Core/PathRouteCloneTests.cs new file mode 100644 index 0000000..a1e5c75 --- /dev/null +++ b/UnitTests/Core/PathRouteCloneTests.cs @@ -0,0 +1,71 @@ +using Autodesk.Navisworks.Api; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NavisworksTransport.UnitTests.Core +{ + [TestClass] + public class PathRouteCloneTests + { + [TestMethod] + public void Clone_ShouldPreserveRailSpecificPropertiesAndGenerateNewIds() + { + var route = new PathRoute("原路径") + { + PathType = PathType.Rail, + RailMountMode = RailMountMode.OverRail, + RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine, + RailNormalOffset = 12.5, + RailPreferredNormal = new Point3D(0, 1, 0) + }; + + route.Points.Add(new PathPoint(new Point3D(1, 2, 3), "起点", PathPointType.StartPoint)); + route.Points.Add(new PathPoint(new Point3D(4, 5, 6), "终点", PathPointType.EndPoint)); + + var clonedRoute = route.Clone(); + + Assert.AreEqual(PathType.Rail, clonedRoute.PathType); + Assert.AreEqual(RailMountMode.OverRail, clonedRoute.RailMountMode); + Assert.AreEqual(RailPathDefinitionMode.RailCenterLine, clonedRoute.RailPathDefinitionMode); + Assert.AreEqual(12.5, clonedRoute.RailNormalOffset, 1e-6); + Assert.IsNotNull(clonedRoute.RailPreferredNormal); + Assert.AreNotEqual(route.Id, clonedRoute.Id); + Assert.AreEqual(2, clonedRoute.Points.Count); + Assert.AreNotEqual(route.Points[0].Id, clonedRoute.Points[0].Id); + Assert.AreEqual(route.Points[0].Name, clonedRoute.Points[0].Name); + Assert.AreEqual(route.Points[0].Type, clonedRoute.Points[0].Type); + } + + [TestMethod] + public void Clone_ShouldCopyGroundEdgesAndRemapPointReferences() + { + var route = new PathRoute("地面路径") + { + PathType = PathType.Ground, + TurnRadius = 3.0, + IsCurved = true + }; + + var startPoint = new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint); + var endPoint = new PathPoint(new Point3D(10, 0, 0), "终点", PathPointType.EndPoint); + route.Points.Add(startPoint); + route.Points.Add(endPoint); + route.Edges.Add(new PathEdge + { + StartPointId = startPoint.Id, + EndPointId = endPoint.Id, + SegmentType = PathSegmentType.Straight, + PhysicalLength = 10.0 + }); + + var clonedRoute = route.Clone(); + + Assert.AreEqual(PathType.Ground, clonedRoute.PathType); + Assert.AreEqual(1, clonedRoute.Edges.Count); + Assert.AreEqual(3.0, clonedRoute.TurnRadius, 1e-6); + Assert.IsTrue(clonedRoute.IsCurved); + Assert.AreEqual(clonedRoute.Points[0].Id, clonedRoute.Edges[0].StartPointId); + Assert.AreEqual(clonedRoute.Points[1].Id, clonedRoute.Edges[0].EndPointId); + Assert.AreNotEqual(route.Edges[0].Id, clonedRoute.Edges[0].Id); + } + } +} diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index 2cbd84b..eabea26 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -1058,7 +1058,7 @@ namespace NavisworksTransport Id = Guid.NewGuid().ToString(), EstimatedTime = EstimatedTime, // TotalLength:计算属性,自动从几何数据计算,克隆时会重新计算 - CreatedTime = CreatedTime, + CreatedTime = DateTime.Now, LastModified = DateTime.Now, Description = Description, AssociatedChannelIds = new List(AssociatedChannelIds), @@ -1072,14 +1072,18 @@ namespace NavisworksTransport MaxObjectWidth = MaxObjectWidth, MaxObjectHeight = MaxObjectHeight, SafetyMargin = SafetyMargin, + TurnRadius = TurnRadius, + IsCurved = IsCurved, + PathType = PathType, + LiftHeight = LiftHeight, RailMountMode = RailMountMode, RailPathDefinitionMode = RailPathDefinitionMode, RailNormalOffset = RailNormalOffset, - RailPreferredNormal = RailPreferredNormal != null - ? new Point3D(RailPreferredNormal.X, RailPreferredNormal.Y, RailPreferredNormal.Z) - : null + RailPreferredNormal = RailPreferredNormal }; + var pointIdMap = new Dictionary(); + // 克隆所有路径点 foreach (var point in Points) { @@ -1088,9 +1092,43 @@ namespace NavisworksTransport Id = Guid.NewGuid().ToString(), Index = point.Index, CreatedTime = point.CreatedTime, - Notes = point.Notes + Notes = point.Notes, + SpeedLimit = point.SpeedLimit, + CustomTurnRadius = point.CustomTurnRadius, + VisualizationDiameter = point.VisualizationDiameter, + Direction = point.Direction }; clonedRoute.Points.Add(clonedPoint); + pointIdMap[point.Id] = clonedPoint.Id; + } + + foreach (var edge in Edges) + { + var clonedEdge = new PathEdge + { + Id = Guid.NewGuid().ToString(), + StartPointId = pointIdMap.ContainsKey(edge.StartPointId) ? pointIdMap[edge.StartPointId] : edge.StartPointId, + EndPointId = pointIdMap.ContainsKey(edge.EndPointId) ? pointIdMap[edge.EndPointId] : edge.EndPointId, + SegmentType = edge.SegmentType, + PhysicalLength = edge.PhysicalLength, + Trajectory = edge.Trajectory != null + ? new ArcTrajectory + { + Ts = edge.Trajectory.Ts, + Te = edge.Trajectory.Te, + ArcCenter = edge.Trajectory.ArcCenter, + RequestedRadius = edge.Trajectory.RequestedRadius, + ActualRadius = edge.Trajectory.ActualRadius, + DeflectionAngle = edge.Trajectory.DeflectionAngle, + ArcLength = edge.Trajectory.ArcLength + } + : null, + SampledPoints = edge.SampledPoints != null + ? new List(edge.SampledPoints) + : new List() + }; + + clonedRoute.Edges.Add(clonedEdge); } return clonedRoute; diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 7a279b4..019a605 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1005,6 +1005,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand NewRailPathCommand { get; private set; } public ICommand NewHoistingPathCommand { get; private set; } public ICommand NewMultiLevelHoistingPathCommand { get; private set; } + public ICommand DuplicatePathCommand { get; private set; } public ICommand DeletePathCommand { get; private set; } public ICommand RenamePathCommand { get; private set; } public ICommand StartEditCommand { get; private set; } @@ -1364,6 +1365,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels PickMultiLevelTargetCommand = new RelayCommand((item) => ExecutePickMultiLevelTarget(item)); CreateMultiLevelPathCommand = new RelayCommand(async () => await ExecuteCreateMultiLevelPathAsync(), () => CanCreateMultiLevelPath); CancelMultiLevelHoistingCommand = new RelayCommand(() => ExecuteCancelMultiLevelHoisting()); + DuplicatePathCommand = new RelayCommand(async () => await ExecuteDuplicatePathAsync()); DeletePathCommand = new RelayCommand(async () => await ExecuteDeletePathAsync()); RenamePathCommand = new RelayCommand(async () => await ExecuteRenamePathAsync()); StartEditCommand = new RelayCommand(async () => await ExecuteAddPathPointAsync(), () => CanExecuteStartEdit); @@ -3595,6 +3597,51 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "删除路径"); } + private async Task ExecuteDuplicatePathAsync() + { + if (SelectedPathRoute == null || _pathPlanningManager == null) return; + + await SafeExecuteAsync(async () => + { + var sourceRoute = SelectedPathRoute.Route ?? + _pathPlanningManager.Routes.FirstOrDefault(r => r.Id == SelectedPathRoute.Id); + if (sourceRoute == null) + { + throw new InvalidOperationException("未找到要复制的路径数据"); + } + + UpdateMainStatus($"正在复制路径: {sourceRoute.Name}..."); + + var duplicatedRoute = sourceRoute.Clone(); + duplicatedRoute.Name = PathHelper.BuildDuplicatedPathName(sourceRoute.Name); + duplicatedRoute.CreatedTime = DateTime.Now; + duplicatedRoute.LastModified = DateTime.Now; + + if (!_pathPlanningManager.AddRoute(duplicatedRoute)) + { + throw new InvalidOperationException($"复制路径失败: {sourceRoute.Name}"); + } + + var duplicatedPathViewModel = new PathRouteViewModel(isFromDatabase: true) + { + Route = duplicatedRoute, + IsActive = false + }; + duplicatedPathViewModel.SetTimeInfo(duplicatedRoute.CreatedTime, duplicatedRoute.LastModified); + + foreach (var point in CreatePathPointViewModelsFromCoreRoute(duplicatedRoute)) + { + duplicatedPathViewModel.Points.Add(point); + } + + PathRoutes.Add(duplicatedPathViewModel); + SelectedPathRoute = duplicatedPathViewModel; + UpdateMainStatus($"已复制路径: {sourceRoute.Name} -> {duplicatedPathViewModel.Name}"); + + await Task.CompletedTask; + }, "复制路径"); + } + #endregion #region 坐标编辑命令 diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 5cef289..84e645a 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -214,6 +214,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 Command="{Binding NewHoistingPathCommand}" Style="{StaticResource ActionButtonStyle}" ToolTip="创建吊装路径(支持多层)"/> +