Refine path viewpoint behavior and config editing

This commit is contained in:
tian 2026-04-02 22:14:26 +08:00
parent 24951d4205
commit 4ec4cf77ee
9 changed files with 180 additions and 62 deletions

View File

@ -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]

View File

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

View File

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

View File

@ -119,6 +119,26 @@ namespace NavisworksTransport.Core.Config
/// </summary>
public double ArcSamplingStepMeters { get; set; }
/// <summary>
/// 吊装路径自动视角距离(米)
/// </summary>
public double HoistingViewDistanceMeters { get; set; }
/// <summary>
/// 吊装路径自动视角仰角(度)
/// </summary>
public double HoistingViewElevationDegrees { get; set; }
/// <summary>
/// Rail 路径自动视角距离(米)
/// </summary>
public double RailViewDistanceMeters { get; set; }
/// <summary>
/// Rail 路径自动视角仰角(度)
/// </summary>
public double RailViewElevationDegrees { get; set; }
// === 模型单位接口(用于内部计算) ===
/// <summary>
@ -160,6 +180,16 @@ namespace NavisworksTransport.Core.Config
/// 圆弧采样步长(模型单位)
/// </summary>
public double ArcSamplingStep => ArcSamplingStepMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
/// <summary>
/// 吊装路径自动视角距离(模型单位)
/// </summary>
public double HoistingViewDistance => HoistingViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
/// <summary>
/// Rail 路径自动视角距离(模型单位)
/// </summary>
public double RailViewDistance => RailViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
}
/// <summary>

View File

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

View File

@ -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
{

View File

@ -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
{

View File

@ -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
{
/// <summary>
/// 配置编辑器对话框 - 非模态窗口,避免独占焦点
/// 配置编辑器对话框
/// </summary>
public partial class ConfigEditorDialog : Window
{
@ -297,7 +299,7 @@ namespace NavisworksTransport.UI.WPF.Views
}
/// <summary>
/// 窗口加载事件 - 处理激活逻辑避免独占焦点
/// 窗口初始化后,确保获得稳定的键盘焦点
/// </summary>
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
}
}
/// <summary>
/// 窗口内容渲染完成后再次请求焦点,避免宿主窗口抢占键盘
/// </summary>
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input);
}
/// <summary>
/// 激活并显示已存在的窗口
/// </summary>
@ -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
/// <summary>
/// 创建并显示配置编辑器对话框(单例模式,非模态)
/// 创建并显示配置编辑器对话框(单例模式,模态)
/// </summary>
/// <returns>对话框实例</returns>
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;
}
}

View File

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