NavisworksTransport/src/Utils/ViewpointHelper.cs

532 lines
23 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 Autodesk.Navisworks.Api;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Utils
{
/// <summary>
/// Navisworks 视角调整工具类
/// 提供自动调整摄像机位置和视角的功能,用于碰撞报告截图
/// </summary>
public static class ViewpointHelper
{
/// <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>
/// 调整视角到路径中心,确保整个路径都在视野内
/// </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);
}
/// <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;
}
/// <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;
}
}
/// <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;
}
}
/// <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;
}
}
/// <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
}
}