291 lines
12 KiB
C#
291 lines
12 KiB
C#
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);
|
||
}
|
||
}
|
||
} |