NavisworksTransport/src/Utils/ViewpointHelper.cs

862 lines
37 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
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
{
/// <summary>
/// Navisworks 视角调整工具类
/// 提供自动调整摄像机位置和视角的功能,用于碰撞报告截图
/// </summary>
public static class ViewpointHelper
{
public enum ViewpointStrategy
{
PathGroundSelection,
PathHoistingSelection,
PathRailSelection,
ModelFocus,
CollisionCloseUp
}
internal enum PathCameraHorizontalMode
{
Top,
Side,
Rear
}
internal struct PathViewpointProfile
{
public PathViewpointProfile(double cameraDistanceMeters, double elevationDegrees, PathCameraHorizontalMode horizontalMode)
{
CameraDistanceMeters = cameraDistanceMeters;
ElevationDegrees = elevationDegrees;
HorizontalMode = horizontalMode;
}
public double CameraDistanceMeters { get; }
public double ElevationDegrees { get; }
public PathCameraHorizontalMode HorizontalMode { get; }
}
internal struct FocusViewpointProfile
{
public FocusViewpointProfile(double viewAngleDegrees, double targetViewRatio)
{
ViewAngleDegrees = viewAngleDegrees;
TargetViewRatio = targetViewRatio;
}
public double ViewAngleDegrees { get; }
public double TargetViewRatio { get; }
}
/// <summary>
/// 包围盒扩展系数(用于 ZoomBox 调整视野范围)
/// </summary>
private const double VIEW_BOUNDING_BOX_EXPANSION_FACTOR = 1.2;
/// <summary>
/// 默认视野扩展范围(米)- 用于调整视角时确保目标完全可见
/// </summary>
private const double DEFAULT_VIEW_EXPANSION_METERS = 10.0;
/// <summary>
/// 智能调整视角:调整到路径中心,确保整个路径都在视野内
/// </summary>
/// <param name="path">路径对象</param>
/// <param name="collisions">碰撞结果列表(用于日志记录,不影响视角调整)</param>
public static void AdjustViewpointSmart(PathRoute path, List<CollisionResult> collisions)
{
// 调整视角到路径中心,确保整个路径都在视野内
AdjustViewpointToPathCenter(path);
}
/// <summary>
/// 按路径类型调整视角。
/// Ground 使用俯视;
/// Hoisting 使用后上方视角;
/// Rail 使用侧面轻俯视角。
/// </summary>
public static void AdjustViewpointForPath(PathRoute path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
switch (path.PathType)
{
case PathType.Hoisting:
AdjustDirectionalViewpointToPath(path, ResolvePathViewpointProfile(ViewpointStrategy.PathHoistingSelection));
return;
case PathType.Rail:
AdjustDirectionalViewpointToPath(path, ResolvePathViewpointProfile(ViewpointStrategy.PathRailSelection));
return;
case PathType.Ground:
default:
AdjustViewpointToPathCenter(path);
return;
}
}
/// <summary>
/// 调整视角到路径中心,确保整个路径都在视野内
/// </summary>
/// <param name="path">路径对象</param>
/// <exception cref="InvalidOperationException">没有活动的文档</exception>
/// <exception cref="ArgumentException">路径为空或没有路径点</exception>
/// <exception cref="InvalidOperationException">路径长度为0</exception>
public static void AdjustViewpointToPathCenter(PathRoute path)
{
// 1. 计算路径起点和终点形成的包围盒(用于聚焦)
Point3D startPoint = path.Points[0].Position;
Point3D endPoint = path.Points[path.Points.Count - 1].Position;
double minX = Math.Min(startPoint.X, endPoint.X);
double minY = Math.Min(startPoint.Y, endPoint.Y);
double minZ = Math.Min(startPoint.Z, endPoint.Z);
double maxX = Math.Max(startPoint.X, endPoint.X);
double maxY = Math.Max(startPoint.Y, endPoint.Y);
double maxZ = Math.Max(startPoint.Z, endPoint.Z);
BoundingBox3D startEndBoundingBox = new BoundingBox3D(
new Point3D(minX, minY, minZ),
new Point3D(maxX, maxY, maxZ)
);
// 2. 计算路径的包围盒(用于视野范围)
BoundingBox3D pathBoundingBox = CalculatePathBoundingBox(path);
// 3. 使用路径长度作为视野基准
double baseDimension = path.TotalLength;
// 4. 调整视角:聚焦起点终点中心,视野覆盖整个路径
AdjustViewpointToBoundingBox(startEndBoundingBox, pathBoundingBox, baseDimension);
}
private static void AdjustDirectionalViewpointToPath(PathRoute path, PathViewpointProfile profile)
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
if (path?.Points == null || path.Points.Count == 0)
{
throw new ArgumentException("路径为空或没有路径点", nameof(path));
}
Point3D startPoint = path.Points[0].Position;
Point3D endPoint = path.Points[path.Points.Count - 1].Position;
BoundingBox3D focusBoundingBox = ResolveDirectionalFocusBoundingBox(path);
Point3D focusCenter = GetBoundingBoxCenter(focusBoundingBox);
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
Vector3 hostUp = ToVector3(adapter.HostUpVector);
Vector3 rawPathDirection = new Vector3(
(float)(endPoint.X - startPoint.X),
(float)(endPoint.Y - startPoint.Y),
(float)(endPoint.Z - startPoint.Z));
Vector3 fallbackForward = ToVector3(ProjectVectorOntoPlane(doc.FrontRightTopViewVector, adapter.HostUpVector));
Vector3 horizontalForward = ResolveHorizontalPathForward(rawPathDirection, hostUp, fallbackForward);
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);
ApplyViewpoint(
cameraPosition,
focusCenter,
new Vector3D(hostUp.X, hostUp.Y, hostUp.Z),
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}°, 模式={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>
/// 调整视角到指定的包围盒(通用方法)
/// 设置俯视位置,确保包围盒完整显示在视野中
/// </summary>
/// <param name="focusBoundingBox">聚焦包围盒(用于设置相机位置)</param>
/// <param name="viewBoundingBox">视野包围盒(用于 ZoomBox 调整视野范围)</param>
/// <param name="baseDimension">基准尺寸(米,用于计算相机距离)</param>
/// <exception cref="InvalidOperationException">没有活动的文档</exception>
private static void AdjustViewpointToBoundingBox(BoundingBox3D focusBoundingBox, BoundingBox3D viewBoundingBox, double baseDimension)
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
Vector3D hostUp = adapter.HostUpVector;
Vector3D screenUp = adapter.FromCanonicalVector(new Vector3D(0, 1, 0));
// 1. 计算聚焦包围盒中心(相机指向的位置)
Point3D focusCenter = new Point3D(
(focusBoundingBox.Min.X + focusBoundingBox.Max.X) / 2,
(focusBoundingBox.Min.Y + focusBoundingBox.Max.Y) / 2,
(focusBoundingBox.Min.Z + focusBoundingBox.Max.Z) / 2
);
// 2. 设置俯视位置(从上方往下看)
// 将基准尺寸(米)转换为模型单位
double baseDimensionInModelUnits = UnitsConverter.ConvertFromMeters(baseDimension);
double cameraDistance = baseDimensionInModelUnits; // 相机距离设置为基准尺寸的1倍可调整比例以获得更好视野
Point3D cameraPosition = new Point3D(
focusCenter.X + hostUp.X * cameraDistance,
focusCenter.Y + hostUp.Y * cameraDistance,
focusCenter.Z + hostUp.Z * cameraDistance);
// 将相机距离转换回米用于日志显示
double cameraDistanceMeters = UnitsConverter.ConvertToMeters(cameraDistance);
LogManager.Info($"相机设置: 位置=({cameraPosition.X:F2}, {cameraPosition.Y:F2}, {cameraPosition.Z:F2}), 焦点=({focusCenter.X:F2}, {focusCenter.Y:F2}, {focusCenter.Z:F2}), 相机距离={cameraDistanceMeters:F2}米");
// 3. 计算扩展边距
double boxWidth = viewBoundingBox.Max.X - viewBoundingBox.Min.X;
double expansionFactor = VIEW_BOUNDING_BOX_EXPANSION_FACTOR + (DEFAULT_VIEW_EXPANSION_METERS / baseDimension);
double expansionMargin = boxWidth * (expansionFactor - 1) / 2;
// 4. 应用视角(使用通用方法,带 ZoomBox
ApplyViewpointWithZoomBox(cameraPosition, focusCenter, screenUp, viewBoundingBox, expansionMargin);
LogManager.Info(
$"视角已调整完成: 俯视图(已摆正), HostUp=({hostUp.X:F2},{hostUp.Y:F2},{hostUp.Z:F2}), " +
$"ScreenUp=({screenUp.X:F2},{screenUp.Y:F2},{screenUp.Z:F2}), 基准尺寸={baseDimension:F2}米, " +
$"相机距离={cameraDistanceMeters:F2}米, FOV=自动, 包围盒扩展系数={VIEW_BOUNDING_BOX_EXPANSION_FACTOR:F1}, 扩展边距={expansionMargin:F2}米");
}
/// <summary>
/// 计算路径的包围盒
/// </summary>
/// <param name="path">路径对象</param>
/// <returns>路径的包围盒</returns>
/// <exception cref="ArgumentException">路径为空或没有路径点</exception>
public static BoundingBox3D CalculatePathBoundingBox(PathRoute path)
{
if (path == null)
{
throw new ArgumentException("路径不能为空");
}
// 参考 BoundingBoxGeometryUtils.CalculateTotalBounds 的方式计算包围盒
var firstPoint = path.Points[0].Position;
double minX = firstPoint.X;
double minY = firstPoint.Y;
double minZ = firstPoint.Z;
double maxX = firstPoint.X;
double maxY = firstPoint.Y;
double maxZ = firstPoint.Z;
foreach (var point in path.Points.Skip(1))
{
var pos = point.Position;
minX = Math.Min(minX, pos.X);
minY = Math.Min(minY, pos.Y);
minZ = Math.Min(minZ, pos.Z);
maxX = Math.Max(maxX, pos.X);
maxY = Math.Max(maxY, pos.Y);
maxZ = Math.Max(maxZ, pos.Z);
}
var bounds = new BoundingBox3D(
new Point3D(minX, minY, minZ),
new Point3D(maxX, maxY, maxZ)
);
return bounds;
}
internal static PathViewpointProfile ResolvePathViewpointProfile(PathType pathType)
{
switch (pathType)
{
case PathType.Hoisting:
return ResolvePathViewpointProfile(ViewpointStrategy.PathHoistingSelection);
case PathType.Rail:
return ResolvePathViewpointProfile(ViewpointStrategy.PathRailSelection);
case PathType.Ground:
default:
return ResolvePathViewpointProfile(ViewpointStrategy.PathGroundSelection);
}
}
internal static PathViewpointProfile ResolvePathViewpointProfile(ViewpointStrategy strategy)
{
PathEditingConfig config = ConfigManager.Instance.Current?.PathEditing ?? new PathEditingConfig();
switch (strategy)
{
case ViewpointStrategy.PathHoistingSelection:
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(
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(
cameraDistanceMeters: 12.0,
elevationDegrees: 90.0,
horizontalMode: PathCameraHorizontalMode.Top);
}
}
internal static FocusViewpointProfile ResolveFocusViewpointProfile(ViewpointStrategy strategy)
{
switch (strategy)
{
case ViewpointStrategy.CollisionCloseUp:
return new FocusViewpointProfile(viewAngleDegrees: 60.0, targetViewRatio: 0.25);
case ViewpointStrategy.ModelFocus:
default:
return new FocusViewpointProfile(viewAngleDegrees: 60.0, targetViewRatio: 0.25);
}
}
internal static Vector3 ResolveHorizontalPathForward(Vector3 rawPathDirection, Vector3 hostUp, Vector3 fallbackForward)
{
Vector3 projected = ProjectOntoPlane(rawPathDirection, hostUp);
if (projected.LengthSquared() > 1e-8f)
{
return Vector3.Normalize(projected);
}
Vector3 fallbackProjected = ProjectOntoPlane(fallbackForward, hostUp);
if (fallbackProjected.LengthSquared() > 1e-8f)
{
return Vector3.Normalize(fallbackProjected);
}
Vector3 canonicalFallback = Math.Abs(Vector3.Dot(Vector3.UnitX, hostUp)) > 0.9f
? Vector3.UnitY
: Vector3.UnitX;
return Vector3.Normalize(ProjectOntoPlane(canonicalFallback, hostUp));
}
internal static Vector3 ResolveCameraOffsetDirection(PathViewpointProfile profile, Vector3 hostUp, Vector3 horizontalForward)
{
float elevationRadians = (float)(profile.ElevationDegrees * Math.PI / 180.0);
float horizontalWeight = (float)Math.Cos(elevationRadians);
float verticalWeight = (float)Math.Sin(elevationRadians);
Vector3 horizontalDirection;
switch (profile.HorizontalMode)
{
case PathCameraHorizontalMode.Rear:
horizontalDirection = -horizontalForward;
break;
case PathCameraHorizontalMode.Side:
horizontalDirection = Vector3.Cross(hostUp, horizontalForward);
if (horizontalDirection.LengthSquared() <= 1e-8f)
{
horizontalDirection = -horizontalForward;
}
break;
default:
horizontalDirection = hostUp;
break;
}
Vector3 offset = horizontalDirection * horizontalWeight + hostUp * verticalWeight;
return Vector3.Normalize(offset);
}
private static BoundingBox3D CreateBoundingBoxFromPoints(Point3D pointA, Point3D pointB)
{
return new BoundingBox3D(
new Point3D(
Math.Min(pointA.X, pointB.X),
Math.Min(pointA.Y, pointB.Y),
Math.Min(pointA.Z, pointB.Z)),
new Point3D(
Math.Max(pointA.X, pointB.X),
Math.Max(pointA.Y, pointB.Y),
Math.Max(pointA.Z, pointB.Z)));
}
private static Point3D GetBoundingBoxCenter(BoundingBox3D boundingBox)
{
return new Point3D(
(boundingBox.Min.X + boundingBox.Max.X) / 2.0,
(boundingBox.Min.Y + boundingBox.Max.Y) / 2.0,
(boundingBox.Min.Z + boundingBox.Max.Z) / 2.0);
}
private static double GetMaxDimension(BoundingBox3D boundingBox)
{
return Math.Max(
boundingBox.Max.X - boundingBox.Min.X,
Math.Max(
boundingBox.Max.Y - boundingBox.Min.Y,
boundingBox.Max.Z - boundingBox.Min.Z));
}
private static Vector3D ProjectVectorOntoPlane(Vector3D vector, Vector3D planeNormal)
{
double dot = vector.X * planeNormal.X + vector.Y * planeNormal.Y + vector.Z * planeNormal.Z;
return new Vector3D(
vector.X - planeNormal.X * dot,
vector.Y - planeNormal.Y * dot,
vector.Z - planeNormal.Z * dot);
}
private static Vector3 ProjectOntoPlane(Vector3 vector, Vector3 planeNormal)
{
float dot = Vector3.Dot(vector, planeNormal);
return vector - planeNormal * dot;
}
private static Vector3 ToVector3(Vector3D vector)
{
return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z);
}
/// <summary>
/// 计算所有碰撞位置的总包围盒
/// </summary>
/// <param name="collisions">碰撞结果列表</param>
/// <returns>所有碰撞位置的总包围盒</returns>
/// <exception cref="ArgumentException">碰撞列表为空或没有有效的碰撞位置信息</exception>
public static BoundingBox3D CalculateCollisionsBoundingBox(List<CollisionResult> collisions)
{
if (collisions == null || collisions.Count == 0)
{
throw new ArgumentException("碰撞列表为空");
}
// 获取所有碰撞的中心点
var collisionCenters = collisions
.Where(c => c.Center != null)
.Select(c => c.Center)
.ToList();
if (collisionCenters.Count == 0)
{
throw new ArgumentException("没有有效的碰撞位置信息");
}
// 创建初始包围盒
BoundingBox3D bounds = new BoundingBox3D(
collisionCenters[0],
collisionCenters[0]
);
// 扩展包围盒以包含所有碰撞位置
foreach (var center in collisionCenters)
{
bounds.Extend(center);
}
return bounds;
}
/// <summary>
/// 保存当前视角
/// </summary>
/// <returns>当前视角的副本</returns>
/// <exception cref="InvalidOperationException">没有活动的文档</exception>
public static Viewpoint SaveCurrentViewpoint()
{
try
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
Viewpoint currentViewpoint = doc.CurrentViewpoint.Value;
return currentViewpoint.CreateCopy();
}
catch (Exception ex)
{
LogManager.Error($"保存当前视角失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 恢复视角
/// </summary>
/// <param name="savedViewpoint">要恢复的视角</param>
/// <exception cref="InvalidOperationException">没有活动的文档</exception>
/// <exception cref="ArgumentNullException">视角不能为空</exception>
public static void RestoreViewpoint(Viewpoint savedViewpoint)
{
if (savedViewpoint == null)
{
throw new ArgumentNullException(nameof(savedViewpoint));
}
try
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
doc.CurrentViewpoint.CopyFrom(savedViewpoint);
LogManager.Info("视角已恢复");
}
catch (Exception ex)
{
LogManager.Error($"恢复视角失败: {ex.Message}");
throw;
}
}
#region
/// <summary>
/// 应用视角设置(核心通用方法)
/// </summary>
/// <param name="cameraPosition">相机位置</param>
/// <param name="targetPoint">目标点(相机看向的位置)</param>
/// <param name="upVector">相机上方向</param>
/// <param name="useAlignDirection">是否使用 AlignDirection/AlignUptrue还是直接设置 Rotationfalse</param>
private static void ApplyViewpoint(Point3D cameraPosition, Point3D targetPoint, Vector3D upVector, bool useAlignDirection = true)
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
newViewpoint.Position = cameraPosition;
if (useAlignDirection)
{
// 方法1使用 AlignDirection + AlignUp推荐用于聚焦对象
Vector3D lookDirection = new Vector3D(
targetPoint.X - cameraPosition.X,
targetPoint.Y - cameraPosition.Y,
targetPoint.Z - cameraPosition.Z
);
lookDirection.Normalize();
newViewpoint.AlignDirection(lookDirection);
newViewpoint.AlignUp(upVector);
}
else
{
// 方法2直接设置 Rotation 和 WorldUpVector用于标准俯视图等
newViewpoint.Rotation = new Rotation3D();
newViewpoint.WorldUpVector = new UnitVector3D(upVector.X, upVector.Y, upVector.Z);
}
doc.CurrentViewpoint.CopyFrom(newViewpoint);
}
/// <summary>
/// 应用视角并缩放视野(使用 ZoomBox
/// </summary>
/// <param name="cameraPosition">相机位置</param>
/// <param name="targetPoint">目标点</param>
/// <param name="upVector">相机上方向</param>
/// <param name="viewBoundingBox">视野包围盒(用于 ZoomBox</param>
/// <param name="expansionMargin">包围盒扩展边距</param>
private static void ApplyViewpointWithZoomBox(Point3D cameraPosition, Point3D targetPoint, Vector3D upVector, BoundingBox3D viewBoundingBox, double expansionMargin = 0)
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
newViewpoint.Position = cameraPosition;
// 使用 AlignDirection + AlignUp
Vector3D lookDirection = new Vector3D(
targetPoint.X - cameraPosition.X,
targetPoint.Y - cameraPosition.Y,
targetPoint.Z - cameraPosition.Z
);
lookDirection.Normalize();
newViewpoint.AlignDirection(lookDirection);
newViewpoint.AlignUp(upVector);
// 使用 ZoomBox 调整视野
BoundingBox3D expandedBox = expansionMargin > 0
? BoundingBoxGeometryUtils.ExpandBoundingBox(viewBoundingBox, expansionMargin)
: viewBoundingBox;
newViewpoint.ZoomBox(expandedBox);
doc.CurrentViewpoint.CopyFrom(newViewpoint);
}
#endregion
#region
/// <summary>
/// 聚焦到指定模型元素
/// 相机会 positioned 在元素斜上方使元素占据视图约1/4宽度
/// </summary>
/// <param name="modelItem">要聚焦的模型元素</param>
/// <param name="viewAngleDegrees">视角角度推荐45度斜上方</param>
/// <param name="targetViewRatio">目标占据视图比例1/4 = 0.25</param>
public static void FocusOnModelItem(ModelItem modelItem, double viewAngleDegrees, double targetViewRatio)
{
if (modelItem == null)
{
throw new ArgumentNullException(nameof(modelItem));
}
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
try
{
// 1. 获取模型元素的包围盒
BoundingBox3D boundingBox = modelItem.BoundingBox();
if (boundingBox == null || boundingBox.Size == new Vector3D(0, 0, 0))
{
LogManager.Warning($"模型元素 {modelItem.DisplayName} 没有有效的包围盒,使用默认视角");
return;
}
// 2. 计算包围盒中心和尺寸
Point3D center = new Point3D(
(boundingBox.Min.X + boundingBox.Max.X) / 2,
(boundingBox.Min.Y + boundingBox.Max.Y) / 2,
(boundingBox.Min.Z + boundingBox.Max.Z) / 2
);
double maxDimension = Math.Max(
boundingBox.Max.X - boundingBox.Min.X,
Math.Max(boundingBox.Max.Y - boundingBox.Min.Y, boundingBox.Max.Z - boundingBox.Min.Z)
);
LogManager.Debug($"聚焦到模型元素: {modelItem.DisplayName}, 最大边: {maxDimension:F2}");
// 3. 计算相机位置(使用模型标准视角方向)
Point3D cameraPosition = CalculateCameraPosition(center, maxDimension, viewAngleDegrees, targetViewRatio);
// 4. 应用视角(使用通用方法)
Vector3D upVector = doc.FrontRightTopViewUpVector;
ApplyViewpoint(cameraPosition, center, upVector, useAlignDirection: true);
LogManager.Debug($"视角已调整: 标准前右上视角, 目标占据视图{targetViewRatio:P0}");
}
catch (Exception ex)
{
LogManager.Error($"聚焦到模型元素失败: {ex.Message}", ex);
throw;
}
}
public static void FocusOnModelItem(ModelItem modelItem, ViewpointStrategy strategy)
{
FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
FocusOnModelItem(modelItem, profile.ViewAngleDegrees, profile.TargetViewRatio);
}
public static void FocusOnModelItems(IEnumerable<ModelItem> modelItems, ViewpointStrategy strategy)
{
if (modelItems == null)
{
throw new ArgumentNullException(nameof(modelItems));
}
List<ModelItem> items = modelItems.Where(item => item != null).ToList();
if (items.Count == 0)
{
throw new ArgumentException("至少需要一个模型元素", nameof(modelItems));
}
BoundingBox3D combinedBounds = items[0].BoundingBox();
for (int i = 1; i < items.Count; i++)
{
BoundingBox3D itemBounds = items[i].BoundingBox();
combinedBounds.Extend(itemBounds.Min);
combinedBounds.Extend(itemBounds.Max);
}
Point3D center = new Point3D(
(combinedBounds.Min.X + combinedBounds.Max.X) / 2.0,
(combinedBounds.Min.Y + combinedBounds.Max.Y) / 2.0,
(combinedBounds.Min.Z + combinedBounds.Max.Z) / 2.0);
double targetSize = GetMaxDimension(combinedBounds);
FocusOnPosition(center, targetSize, strategy);
}
/// <summary>
/// 聚焦到多个模型元素(如碰撞中的两个对象)
/// </summary>
/// <param name="item1">第一个模型元素(运动物体)</param>
/// <param name="item2">第二个模型元素(被撞对象)</param>
/// <param name="viewAngleDegrees">视角角度推荐45度斜上方</param>
/// <param name="targetViewRatio">目标占据视图比例1/4 = 0.25</param>
public static void FocusOnCollision(ModelItem item1, ModelItem item2, double viewAngleDegrees, double targetViewRatio)
{
if (item1 == null && item2 == null)
{
throw new ArgumentException("至少需要一个模型元素");
}
try
{
// 计算两个元素的联合包围盒
BoundingBox3D combinedBounds = null;
if (item1 != null)
{
combinedBounds = item1.BoundingBox();
}
if (item2 != null)
{
if (combinedBounds == null)
{
combinedBounds = item2.BoundingBox();
}
else
{
BoundingBox3D item2Box = item2.BoundingBox();
combinedBounds.Extend(item2Box.Min);
combinedBounds.Extend(item2Box.Max);
}
}
if (combinedBounds == null || combinedBounds.Size == new Vector3D(0, 0, 0))
{
LogManager.Warning("碰撞对象没有有效的包围盒");
return;
}
// 使用联合包围盒的中心点和尺寸
Point3D center = new Point3D(
(combinedBounds.Min.X + combinedBounds.Max.X) / 2,
(combinedBounds.Min.Y + combinedBounds.Max.Y) / 2,
(combinedBounds.Min.Z + combinedBounds.Max.Z) / 2
);
double maxDimension = Math.Max(
combinedBounds.Max.X - combinedBounds.Min.X,
Math.Max(combinedBounds.Max.Y - combinedBounds.Min.Y, combinedBounds.Max.Z - combinedBounds.Min.Z)
);
FocusOnPosition(center, maxDimension, viewAngleDegrees, targetViewRatio);
LogManager.Info($"已聚焦到碰撞位置: {item1?.DisplayName} ↔ {item2?.DisplayName}");
}
catch (Exception ex)
{
LogManager.Error($"聚焦到碰撞位置失败: {ex.Message}", ex);
throw;
}
}
public static void FocusOnCollision(ModelItem item1, ModelItem item2, ViewpointStrategy strategy)
{
FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
FocusOnCollision(item1, item2, profile.ViewAngleDegrees, profile.TargetViewRatio);
}
/// <summary>
/// 聚焦到指定位置和尺寸
/// </summary>
/// <param name="center">目标中心点</param>
/// <param name="targetSize">目标尺寸(最大边)</param>
/// <param name="viewAngleDegrees">视角角度</param>
/// <param name="targetViewRatio">目标占据视图比例1/4 = 0.25</param>
public static void FocusOnPosition(Point3D center, double targetSize, double viewAngleDegrees, double targetViewRatio)
{
Document doc = Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
throw new InvalidOperationException("没有活动的文档");
}
try
{
// 计算相机位置(使用模型标准视角方向)
Point3D cameraPosition = CalculateCameraPosition(center, targetSize, viewAngleDegrees, targetViewRatio);
// 应用视角(使用通用方法)
Vector3D upVector = doc.FrontRightTopViewUpVector;
ApplyViewpoint(cameraPosition, center, upVector, useAlignDirection: true);
}
catch (Exception ex)
{
LogManager.Error($"聚焦到位置失败: {ex.Message}", ex);
throw;
}
}
public static void FocusOnPosition(Point3D center, double targetSize, ViewpointStrategy strategy)
{
FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
FocusOnPosition(center, targetSize, profile.ViewAngleDegrees, profile.TargetViewRatio);
}
/// <summary>
/// 计算相机位置(使用模型标准前右上视角)
/// 基于 Navisworks 的标准 FrontRightTopViewVector 确定相机方向
/// </summary>
private static Point3D CalculateCameraPosition(Point3D center, double targetSize, double viewAngleDegrees, double targetViewRatio)
{
// 计算相机距离(标准透视投影公式)
// 公式distance = (targetSize / 2) / tan(FOV / 2) / targetViewRatio
double fovRadians = viewAngleDegrees * Math.PI / 180.0;
double cameraDistance = (targetSize / 2.0) / Math.Tan(fovRadians / 2.0) / targetViewRatio;
// 调试日志:打印计算参数
LogManager.Debug($"[ViewpointHelper] FOV={viewAngleDegrees:F2}°, targetSize={targetSize:F2}, ratio={targetViewRatio:F2}, " +
$"distance={cameraDistance:F2}, center=({center.X:F2},{center.Y:F2},{center.Z:F2})");
// 获取模型的标准前右上视角向量
Vector3D viewDirection = Application.ActiveDocument.FrontRightTopViewVector;
viewDirection.Normalize();
// 沿标准视角方向偏移相机位置
return new Point3D(
center.X - viewDirection.X * cameraDistance,
center.Y - viewDirection.Y * cameraDistance,
center.Z - viewDirection.Z * cameraDistance
);
}
#endregion
}
}