862 lines
37 KiB
C#
862 lines
37 KiB
C#
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/AlignUp(true)还是直接设置 Rotation(false)</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
|
||
}
|
||
}
|