NavisworksTransport/src/Utils/ViewpointHelper.cs
2026-02-01 10:13:51 +08:00

291 lines
12 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>
/// 视图更新等待时间(毫秒)
/// 确保视角调整后视图更新完成
/// </summary>
private const int VIEW_UPDATE_DELAY_MS = 100;
/// <summary>
/// 包围盒扩展系数(用于 ZoomBox 调整视野范围)
/// </summary>
private const double VIEW_BOUNDING_BOX_EXPANSION_FACTOR = 1.2;
/// <summary> 视野扩展范围(米)
/// </summary>
const double 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("没有活动的文档");
}
// 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. 设置俯视位置(从上方往下看)
// 获取坐标系
ICoordinateSystem cs = CoordinateSystemManager.Instance.Current;
// 将基准尺寸(米)转换为模型单位
double baseDimensionInModelUnits = UnitsConverter.ConvertFromMeters(baseDimension);
double cameraDistance = baseDimensionInModelUnits; // 相机距离设置为基准尺寸的1倍可调整比例以获得更好视野
// 计算相机位置(在高度方向偏移)
var horizontalCoords = cs.GetHorizontalCoords(focusCenter);
Point3D cameraPosition = cs.CreatePoint(horizontalCoords.Item1, horizontalCoords.Item2, cs.GetElevation(focusCenter) + 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. 创建当前视角的副本(关键:使用 CreateCopy 而不是 new Viewpoint()
Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
// 使用无旋转来摆正视图
newViewpoint.Rotation = new Rotation3D();
// 设置标准俯视图的相机配置
// 相机位置:在目标正上方
newViewpoint.Position = cameraPosition;
// 使用 AlignDirection 让相机向下看(适配不同坐标系)
newViewpoint.AlignDirection(cs.VerticalScanDirection);
// 使用 AlignUp 设置向上向量(适配不同坐标系)
newViewpoint.AlignUp(cs.UpVector);
// 4. 扩展路径包围盒,避免边缘贴得太近
double boxWidth = viewBoundingBox.Max.X - viewBoundingBox.Min.X;
double expansionFactor = VIEW_BOUNDING_BOX_EXPANSION_FACTOR + (EXPANSION_METERS / baseDimension);
double expansionMargin = boxWidth * (expansionFactor - 1) / 2;
BoundingBox3D expandedViewBoundingBox = BoundingBoxGeometryUtils.ExpandBoundingBox(viewBoundingBox, expansionMargin);
// 5. 使用 ZoomBox 调整视野范围(使用扩展后的视野包围盒,确保整个路径都在视野内)
newViewpoint.ZoomBox(expandedViewBoundingBox);
// 6. 应用视角到文档
doc.CurrentViewpoint.CopyFrom(newViewpoint);
LogManager.Info($"视角已调整完成: 俯视图(已摆正), 基准尺寸={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;
}
}
/// <summary>
/// 等待视图更新完成
/// </summary>
public static void WaitForViewUpdate()
{
System.Threading.Thread.Sleep(VIEW_UPDATE_DELAY_MS);
}
}
}