Add path duplication workflow

This commit is contained in:
tian 2026-03-28 12:36:24 +08:00
parent a5d1db6416
commit 7db5a7a201
7 changed files with 217 additions and 6 deletions

View File

@ -53,7 +53,9 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="UnitTests\Core\PathHelperTests.cs" />
<Compile Include="UnitTests\Core\PathPersistenceTests.cs" />
<Compile Include="UnitTests\Core\PathRouteCloneTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalPlanarPoseBuilderTests.cs" />

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<string>(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<string, string>();
// 克隆所有路径点
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<Point3D>(edge.SampledPoints)
: new List<Point3D>()
};
clonedRoute.Edges.Add(clonedEdge);
}
return clonedRoute;

View File

@ -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<HoistingLevelItem>((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

View File

@ -214,6 +214,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding NewHoistingPathCommand}"
Style="{StaticResource ActionButtonStyle}"
ToolTip="创建吊装路径(支持多层)"/>
<Button Content="复制"
Command="{Binding DuplicatePathCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding SelectedPathRoute, Converter={x:Static converters:BoolToVisibilityConverter.IsNotNullToBoolConverter}}"/>
<Button Content="删除"
Command="{Binding DeletePathCommand}"
Style="{StaticResource SecondaryButtonStyle}"

View File

@ -2,6 +2,7 @@ using System;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils
@ -83,6 +84,26 @@ namespace NavisworksTransport.Utils
return $"{prefix}_{timestamp}.{extension}";
}
/// <summary>
/// 生成复制路径名称。
/// 如果原名称以 MMdd_HHmmss 结尾则重建时间戳否则仅添加“副本_”前缀。
/// </summary>
public static string BuildDuplicatedPathName(string originalName, DateTime? now = null)
{
var normalizedName = string.IsNullOrWhiteSpace(originalName) ? "路径" : originalName.Trim();
var timestampPattern = new Regex(@"^(.*)_\d{4}_\d{6}$");
var match = timestampPattern.Match(normalizedName);
if (!match.Success)
{
return $"副本_{normalizedName}";
}
var baseName = match.Groups[1].Value;
var timestamp = (now ?? DateTime.Now).ToString("MMdd_HHmmss");
return $"副本_{baseName}_{timestamp}";
}
/// <summary>
/// 计算从HTML文件到目标文件的相对路径
/// </summary>
@ -203,4 +224,4 @@ namespace NavisworksTransport.Utils
}
}
}
}
}