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="创建吊装路径(支持多层)"/>
+