From 4ec4cf77eed7639a6398241e61bdec5992be69db Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 2 Apr 2026 22:14:26 +0800 Subject: [PATCH] Refine path viewpoint behavior and config editing --- .../CoordinateSystem/ViewpointHelperTests.cs | 35 ++++---- resources/default_config.toml | 13 +++ src/Core/Config/ConfigManager.cs | 4 + src/Core/Config/SystemConfig.cs | 30 +++++++ .../ViewModels/LogisticsControlViewModel.cs | 8 +- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 9 +- .../ViewModels/SystemManagementViewModel.cs | 7 +- src/UI/WPF/Views/ConfigEditorDialog.xaml.cs | 54 +++++++++--- src/Utils/ViewpointHelper.cs | 82 ++++++++++++------- 9 files changed, 180 insertions(+), 62 deletions(-) diff --git a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs index 90dad79..168e72a 100644 --- a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs +++ b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NavisworksTransport.Utils; using System.Numerics; +using NavisworksTransport.Core.Config; namespace NavisworksTransport.UnitTests.CoordinateSystem { @@ -10,32 +11,35 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem [TestMethod] public void ResolvePathViewpointProfile_ShouldReturnExpectedDefaults() { + var config = ConfigManager.Instance.Current.PathEditing; var ground = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathGroundSelection); var hoisting = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection); var rail = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection); - Assert.AreEqual(1.0, ground.DistanceScale, 1e-9); - Assert.AreEqual(12.0, ground.MinDistanceMeters, 1e-9); + Assert.AreEqual(12.0, ground.CameraDistanceMeters, 1e-9); + Assert.AreEqual(90.0, ground.ElevationDegrees, 1e-9); - Assert.AreEqual(1.2, hoisting.DistanceScale, 1e-9); - Assert.AreEqual(8.0, hoisting.MinDistanceMeters, 1e-9); - Assert.AreEqual(22.0, hoisting.ElevationDegrees, 1e-9); + Assert.AreEqual(config.HoistingViewDistanceMeters, hoisting.CameraDistanceMeters, 1e-9); + Assert.AreEqual(config.HoistingViewElevationDegrees, hoisting.ElevationDegrees, 1e-9); + Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, hoisting.HorizontalMode); - Assert.AreEqual(0.75, rail.DistanceScale, 1e-9); - Assert.AreEqual(4.5, rail.MinDistanceMeters, 1e-9); - Assert.AreEqual(12.0, rail.ElevationDegrees, 1e-9); + Assert.AreEqual(config.RailViewDistanceMeters, rail.CameraDistanceMeters, 1e-9); + Assert.AreEqual(config.RailViewElevationDegrees, rail.ElevationDegrees, 1e-9); + Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, rail.HorizontalMode); } [TestMethod] - public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromBehindAndAbove() + public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromSideAndSlightlyAbove() { Vector3 hostUp = Vector3.UnitZ; Vector3 forward = Vector3.UnitX; + var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection); - Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Hoisting, hostUp, forward); + Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward); - Assert.IsTrue(offset.X < -0.8f); - Assert.IsTrue(offset.Z > 0.3f); + Assert.AreEqual(0.0f, offset.X, 1e-5f); + Assert.IsTrue(offset.Y > 0.0f); + Assert.IsTrue(offset.Z > 0.0f); } [TestMethod] @@ -43,12 +47,13 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem { Vector3 hostUp = Vector3.UnitZ; Vector3 forward = Vector3.UnitX; + var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection); - Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Rail, hostUp, forward); + Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward); Assert.AreEqual(0.0f, offset.X, 1e-5f); - Assert.IsTrue(offset.Y > 0.9f); - Assert.IsTrue(offset.Z > 0.15f); + Assert.IsTrue(offset.Y > 0.0f); + Assert.IsTrue(offset.Z > 0.0f); } [TestMethod] diff --git a/resources/default_config.toml b/resources/default_config.toml index da3e2b4..8825899 100644 --- a/resources/default_config.toml +++ b/resources/default_config.toml @@ -26,6 +26,19 @@ default_path_turn_radius = 2.5 # 圆弧采样步长(米)- 推荐值:0.02-0.1 arc_sampling_step = 0.05 +# 吊装路径自动视角距离(米) +# 默认为近距离低侧视 +hoisting_view_distance_meters = 6.0 + +# 吊装路径自动视角仰角(度) +hoisting_view_elevation_degrees = 30.0 + +# Rail 路径自动视角距离(米) +rail_view_distance_meters = 6.0 + +# Rail 路径自动视角仰角(度) +rail_view_elevation_degrees = 30.0 + [visualization] # 地图边距比例(0-1之间) margin_ratio = 0.1 diff --git a/src/Core/Config/ConfigManager.cs b/src/Core/Config/ConfigManager.cs index a89a216..5b04d96 100644 --- a/src/Core/Config/ConfigManager.cs +++ b/src/Core/Config/ConfigManager.cs @@ -394,6 +394,10 @@ namespace NavisworksTransport.Core.Config config.PathEditing.SafetyMarginMeters = GetDoubleValueWithDefault(pathEdit, "safety_margin_meters", 0.1, missingItems); config.PathEditing.DefaultPathTurnRadiusMeters = GetDoubleValueWithDefault(pathEdit, "default_path_turn_radius", 2.5, missingItems); config.PathEditing.ArcSamplingStepMeters = GetDoubleValueWithDefault(pathEdit, "arc_sampling_step", 0.05, missingItems); + config.PathEditing.HoistingViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "hoisting_view_distance_meters", 6.0, missingItems); + config.PathEditing.HoistingViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "hoisting_view_elevation_degrees", 30.0, missingItems); + config.PathEditing.RailViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "rail_view_distance_meters", 6.0, missingItems); + config.PathEditing.RailViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "rail_view_elevation_degrees", 30.0, missingItems); } } diff --git a/src/Core/Config/SystemConfig.cs b/src/Core/Config/SystemConfig.cs index 024d340..1c424f7 100644 --- a/src/Core/Config/SystemConfig.cs +++ b/src/Core/Config/SystemConfig.cs @@ -119,6 +119,26 @@ namespace NavisworksTransport.Core.Config /// public double ArcSamplingStepMeters { get; set; } + /// + /// 吊装路径自动视角距离(米) + /// + public double HoistingViewDistanceMeters { get; set; } + + /// + /// 吊装路径自动视角仰角(度) + /// + public double HoistingViewElevationDegrees { get; set; } + + /// + /// Rail 路径自动视角距离(米) + /// + public double RailViewDistanceMeters { get; set; } + + /// + /// Rail 路径自动视角仰角(度) + /// + public double RailViewElevationDegrees { get; set; } + // === 模型单位接口(用于内部计算) === /// @@ -160,6 +180,16 @@ namespace NavisworksTransport.Core.Config /// 圆弧采样步长(模型单位) /// public double ArcSamplingStep => ArcSamplingStepMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor(); + + /// + /// 吊装路径自动视角距离(模型单位) + /// + public double HoistingViewDistance => HoistingViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor(); + + /// + /// Rail 路径自动视角距离(模型单位) + /// + public double RailViewDistance => RailViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor(); } /// diff --git a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs index bfe3c2e..17b7089 100644 --- a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs +++ b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs @@ -240,6 +240,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels { if (IsClipBoxEnabled) { + if (_pathPlanningManager.PathEditState == PathEditState.Creating) + { + LogManager.Info("[剖面盒] 当前处于新建路径流程,跳过自动跟随"); + return; + } + LogManager.Info("[剖面盒] 路径已切换,重新设置剖面盒聚焦到新路径"); EnableClipBoxForCurrentRoute(); } @@ -418,4 +424,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels #endregion } -} \ No newline at end of file +} diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 6e01e40..67fb46d 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; @@ -385,8 +385,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels _pathPlanningManager.SetCurrentRoute(coreRoute); LogManager.Info($"UI路径切换:已同步PathPlanningManager的CurrentRoute到 {value.Name}"); + // 新建路径流程里禁止自动改视角,避免打断建路径操作。 + if (_pathPlanningManager.PathEditState == PathEditState.Creating) + { + LogManager.Info($"UI路径切换:当前处于新建路径流程,跳过自动视角调整: {value.Name}"); + } // 自动调整视角到路径中心(只有路径有点时才调整) - if (coreRoute.Points.Count > 0) + else if (coreRoute.Points.Count > 0) { try { diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs index 4468645..4048f76 100644 --- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs @@ -832,14 +832,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels { try { - // 使用单例模式显示配置编辑器(非模态,避免独占焦点) - // 不传递 owner,让 ShowEditor 自己通过 DialogHelper 处理 + // 使用模态配置编辑器,确保键盘输入不会被 Navisworks 主窗口截获 var dialog = NavisworksTransport.UI.WPF.Views.ConfigEditorDialog.ShowEditor(); if (dialog != null) { - UpdateMainStatus("配置编辑器已打开"); - LogManager.Info("配置编辑器:已打开(非模态)"); + UpdateMainStatus("配置编辑器已关闭"); + LogManager.Info("配置编辑器:已打开并关闭(模态)"); } else { diff --git a/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs b/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs index f8b3638..73e6d56 100644 --- a/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs +++ b/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs @@ -2,13 +2,15 @@ using System; using System.IO; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; using System.Diagnostics; using NavisworksTransport.Core.Config; namespace NavisworksTransport.UI.WPF.Views { /// - /// 配置编辑器对话框 - 非模态窗口,避免独占焦点 + /// 配置编辑器对话框 /// public partial class ConfigEditorDialog : Window { @@ -297,7 +299,7 @@ namespace NavisworksTransport.UI.WPF.Views } /// - /// 窗口加载事件 - 处理激活逻辑避免独占焦点 + /// 窗口初始化后,确保获得稳定的键盘焦点 /// protected override void OnSourceInitialized(EventArgs e) { @@ -305,11 +307,10 @@ namespace NavisworksTransport.UI.WPF.Views try { - // 激活并前置窗口,但不独占焦点 - // 先置顶再取消置顶,让窗口前置但不系统级置顶 this.Activate(); this.Topmost = true; this.Topmost = false; + Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input); LogManager.Debug("配置编辑器对话框已激活并前置显示"); } @@ -319,6 +320,15 @@ namespace NavisworksTransport.UI.WPF.Views } } + /// + /// 窗口内容渲染完成后再次请求焦点,避免宿主窗口抢占键盘 + /// + protected override void OnContentRendered(EventArgs e) + { + base.OnContentRendered(e); + Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input); + } + /// /// 激活并显示已存在的窗口 /// @@ -332,10 +342,10 @@ namespace NavisworksTransport.UI.WPF.Views this.WindowState = WindowState.Normal; } - // 激活并前置窗口(先置顶再取消置顶,避免独占焦点) this.Activate(); this.Topmost = true; this.Topmost = false; + FocusEditorTextBox(); LogManager.Info("配置编辑器对话框已激活并前置显示"); } @@ -345,10 +355,29 @@ namespace NavisworksTransport.UI.WPF.Views } } + private void FocusEditorTextBox() + { + try + { + if (ConfigContentTextBox == null) + { + return; + } + + ConfigContentTextBox.Focus(); + Keyboard.Focus(ConfigContentTextBox); + ConfigContentTextBox.CaretIndex = ConfigContentTextBox.Text?.Length ?? 0; + } + catch (Exception ex) + { + LogManager.Debug($"配置编辑器焦点设置失败: {ex.Message}"); + } + } + #region 静态工厂方法 /// - /// 创建并显示配置编辑器对话框(单例模式,非模态) + /// 创建并显示配置编辑器对话框(单例模式,模态) /// /// 对话框实例 public static ConfigEditorDialog ShowEditor() @@ -385,16 +414,19 @@ namespace NavisworksTransport.UI.WPF.Views // 创建新的对话框实例 var dialog = new ConfigEditorDialog(); - // 使用 DialogHelper 安全地设置 owner - NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog); + // 优先设置 WPF Owner,失败时回退到 Navisworks 主窗口句柄 + if (!NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog)) + { + NavisworksTransport.Utils.DialogHelper.SetWin32Owner(dialog); + } // 设置为当前实例 _currentInstance = dialog; - // 使用 Show() 非模态显示,避免独占焦点 - dialog.Show(); + // 使用 ShowDialog() 确保键盘输入不会被宿主窗口吃掉 + dialog.ShowDialog(); - LogManager.Info("新的配置编辑器对话框已显示(非模态)"); + LogManager.Info("新的配置编辑器对话框已显示(模态)"); return dialog; } } diff --git a/src/Utils/ViewpointHelper.cs b/src/Utils/ViewpointHelper.cs index 08aea47..6b4c0b0 100644 --- a/src/Utils/ViewpointHelper.cs +++ b/src/Utils/ViewpointHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Autodesk.Navisworks.Api; +using NavisworksTransport.Core.Config; using NavisworksTransport.Utils.CoordinateSystem; namespace NavisworksTransport.Utils @@ -22,18 +23,25 @@ namespace NavisworksTransport.Utils CollisionCloseUp } + internal enum PathCameraHorizontalMode + { + Top, + Side, + Rear + } + internal struct PathViewpointProfile { - public PathViewpointProfile(double distanceScale, double minDistanceMeters, double elevationDegrees) + public PathViewpointProfile(double cameraDistanceMeters, double elevationDegrees, PathCameraHorizontalMode horizontalMode) { - DistanceScale = distanceScale; - MinDistanceMeters = minDistanceMeters; + CameraDistanceMeters = cameraDistanceMeters; ElevationDegrees = elevationDegrees; + HorizontalMode = horizontalMode; } - public double DistanceScale { get; } - public double MinDistanceMeters { get; } + public double CameraDistanceMeters { get; } public double ElevationDegrees { get; } + public PathCameraHorizontalMode HorizontalMode { get; } } internal struct FocusViewpointProfile @@ -146,8 +154,7 @@ namespace NavisworksTransport.Utils Point3D startPoint = path.Points[0].Position; Point3D endPoint = path.Points[path.Points.Count - 1].Position; - BoundingBox3D focusBoundingBox = CreateBoundingBoxFromPoints(startPoint, endPoint); - BoundingBox3D viewBoundingBox = CalculatePathBoundingBox(path); + BoundingBox3D focusBoundingBox = ResolveDirectionalFocusBoundingBox(path); Point3D focusCenter = GetBoundingBoxCenter(focusBoundingBox); var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); @@ -158,31 +165,40 @@ namespace NavisworksTransport.Utils (float)(endPoint.Z - startPoint.Z)); Vector3 fallbackForward = ToVector3(ProjectVectorOntoPlane(doc.FrontRightTopViewVector, adapter.HostUpVector)); Vector3 horizontalForward = ResolveHorizontalPathForward(rawPathDirection, hostUp, fallbackForward); - Vector3 cameraOffsetDirection = ResolveCameraOffsetDirection(path.PathType, hostUp, horizontalForward); - - double maxDimensionModel = GetMaxDimension(viewBoundingBox); - double minDistanceModel = UnitsConverter.ConvertFromMeters(profile.MinDistanceMeters); - double cameraDistance = Math.Max(maxDimensionModel * profile.DistanceScale, minDistanceModel); + Vector3 cameraOffsetDirection = ResolveCameraOffsetDirection(profile, hostUp, horizontalForward); + double cameraDistance = UnitsConverter.ConvertFromMeters(profile.CameraDistanceMeters); Point3D cameraPosition = new Point3D( focusCenter.X + cameraOffsetDirection.X * (float)cameraDistance, focusCenter.Y + cameraOffsetDirection.Y * (float)cameraDistance, focusCenter.Z + cameraOffsetDirection.Z * (float)cameraDistance); - double expansionMargin = Math.Max( - maxDimensionModel * (VIEW_BOUNDING_BOX_EXPANSION_FACTOR - 1.0) / 2.0, - UnitsConverter.ConvertFromMeters(2.0)); - - ApplyViewpointWithZoomBox( + ApplyViewpoint( cameraPosition, focusCenter, new Vector3D(hostUp.X, hostUp.Y, hostUp.Z), - viewBoundingBox, - expansionMargin); + useAlignDirection: true); LogManager.Info( $"视角已调整完成: 类型={path.PathType}, 焦点=({focusCenter.X:F2},{focusCenter.Y:F2},{focusCenter.Z:F2}), " + $"相机=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}), " + - $"距离={UnitsConverter.ConvertToMeters(cameraDistance):F2}米, elevation={profile.ElevationDegrees:F1}°"); + $"距离={UnitsConverter.ConvertToMeters(cameraDistance):F2}米, elevation={profile.ElevationDegrees:F1}°, 模式={profile.HorizontalMode}"); + } + + internal static BoundingBox3D ResolveDirectionalFocusBoundingBox(PathRoute path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (path.Points == null || path.Points.Count == 0) + { + throw new ArgumentException("路径为空或没有路径点", nameof(path)); + } + + // 侧视路径应聚焦整条路径包络,而不是只看首尾点。 + // 对吊装路径尤其重要:首尾点常在地面,若只取首尾中心,高处主路径会被挤到画面上半部。 + return CalculatePathBoundingBox(path); } /// @@ -296,15 +312,25 @@ namespace NavisworksTransport.Utils internal static PathViewpointProfile ResolvePathViewpointProfile(ViewpointStrategy strategy) { + PathEditingConfig config = ConfigManager.Instance.Current?.PathEditing ?? new PathEditingConfig(); switch (strategy) { case ViewpointStrategy.PathHoistingSelection: - return new PathViewpointProfile(distanceScale: 1.2, minDistanceMeters: 8.0, elevationDegrees: 22.0); + return new PathViewpointProfile( + cameraDistanceMeters: config.HoistingViewDistanceMeters > 0 ? config.HoistingViewDistanceMeters : 6.0, + elevationDegrees: config.HoistingViewElevationDegrees > 0 ? config.HoistingViewElevationDegrees : 30.0, + horizontalMode: PathCameraHorizontalMode.Side); case ViewpointStrategy.PathRailSelection: - return new PathViewpointProfile(distanceScale: 0.75, minDistanceMeters: 4.5, elevationDegrees: 12.0); + return new PathViewpointProfile( + cameraDistanceMeters: config.RailViewDistanceMeters > 0 ? config.RailViewDistanceMeters : 6.0, + elevationDegrees: config.RailViewElevationDegrees > 0 ? config.RailViewElevationDegrees : 30.0, + horizontalMode: PathCameraHorizontalMode.Side); case ViewpointStrategy.PathGroundSelection: default: - return new PathViewpointProfile(distanceScale: 1.0, minDistanceMeters: 12.0, elevationDegrees: 90.0); + return new PathViewpointProfile( + cameraDistanceMeters: 12.0, + elevationDegrees: 90.0, + horizontalMode: PathCameraHorizontalMode.Top); } } @@ -340,27 +366,25 @@ namespace NavisworksTransport.Utils return Vector3.Normalize(ProjectOntoPlane(canonicalFallback, hostUp)); } - internal static Vector3 ResolveCameraOffsetDirection(PathType pathType, Vector3 hostUp, Vector3 horizontalForward) + internal static Vector3 ResolveCameraOffsetDirection(PathViewpointProfile profile, Vector3 hostUp, Vector3 horizontalForward) { - PathViewpointProfile profile = ResolvePathViewpointProfile(pathType); float elevationRadians = (float)(profile.ElevationDegrees * Math.PI / 180.0); float horizontalWeight = (float)Math.Cos(elevationRadians); float verticalWeight = (float)Math.Sin(elevationRadians); Vector3 horizontalDirection; - switch (pathType) + switch (profile.HorizontalMode) { - case PathType.Hoisting: + case PathCameraHorizontalMode.Rear: horizontalDirection = -horizontalForward; break; - case PathType.Rail: + case PathCameraHorizontalMode.Side: horizontalDirection = Vector3.Cross(hostUp, horizontalForward); if (horizontalDirection.LengthSquared() <= 1e-8f) { horizontalDirection = -horizontalForward; } break; - case PathType.Ground: default: horizontalDirection = hostUp; break;