1055 lines
46 KiB
C#
1055 lines
46 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Autodesk.Navisworks.Api;
|
||
using NavisworksTransport.Utils;
|
||
using NavisworksTransport.Utils.CoordinateSystem;
|
||
|
||
namespace NavisworksTransport.PathPlanning
|
||
{
|
||
/// <summary>
|
||
/// 通道地面高度检测器
|
||
/// 负责检测路径点在通道中的精确地面高度
|
||
/// 支持动态坐标系(Z-Up 或 Y-Up)
|
||
/// </summary>
|
||
public class ChannelHeightDetector
|
||
{
|
||
/// <summary>
|
||
/// 默认射线投射容差(米)- 与配置中的安全间隙一致
|
||
/// </summary>
|
||
private const double DEFAULT_RAYCAST_TOLERANCE_METERS = 0.1;
|
||
|
||
/// <summary>
|
||
/// 当前使用的坐标系
|
||
/// </summary>
|
||
private readonly ICoordinateSystem _coordinateSystem;
|
||
private readonly Dictionary<string, ChannelHeightInfo> _heightCache;
|
||
private readonly double _raycastTolerance;
|
||
|
||
/// <summary>
|
||
/// 构造函数(自动使用当前坐标系)
|
||
/// </summary>
|
||
/// <param name="raycastTolerance">射线投射容差(米)</param>
|
||
public ChannelHeightDetector(double raycastTolerance = DEFAULT_RAYCAST_TOLERANCE_METERS)
|
||
: this(CoordinateSystemManager.Instance.Current, raycastTolerance)
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造函数
|
||
/// </summary>
|
||
/// <param name="coordinateSystem">坐标系</param>
|
||
/// <param name="raycastTolerance">射线投射容差(米)</param>
|
||
public ChannelHeightDetector(ICoordinateSystem coordinateSystem, double raycastTolerance = DEFAULT_RAYCAST_TOLERANCE_METERS)
|
||
{
|
||
_coordinateSystem = coordinateSystem ?? throw new ArgumentNullException(nameof(coordinateSystem));
|
||
_heightCache = new Dictionary<string, ChannelHeightInfo>();
|
||
_raycastTolerance = raycastTolerance;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定位置的通道地面高度(底面高度)
|
||
/// 注意:此方法返回楼板/通道结构的底面高度,用于传统的高度检测
|
||
/// 对于物体路径规划,建议使用GetChannelTopHeight获取顶面行驶表面高度
|
||
/// </summary>
|
||
/// <param name="position">世界坐标位置</param>
|
||
/// <param name="channelItems">通道模型项集合(可能包含楼板、走廊等可通行结构)</param>
|
||
/// <returns>通道底面高度(米)</returns>
|
||
public double GetChannelFloorHeight(Point3D position, IEnumerable<ModelItem> channelItems)
|
||
{
|
||
try
|
||
{
|
||
//LogManager.Info($"[高度检测] 🔍 开始检测位置 ({position.X:F2}, {position.Y:F2}, {position.Z:F2}) 的地面高度,通道数={channelItems?.Count() ?? 0}");
|
||
|
||
// 寻找包含该位置的通道
|
||
var containingChannel = FindContainingChannel(position, channelItems);
|
||
if (containingChannel == null)
|
||
{
|
||
double fallbackElevation = _coordinateSystem.GetElevation(position);
|
||
LogManager.Info($"[高度检测] ❌ 位置 ({position.X:F2}, {position.Y:F2}, {position.Z:F2}) 未找到包含的通道,使用当前宿主高程: {fallbackElevation:F2}");
|
||
return fallbackElevation;
|
||
}
|
||
|
||
//LogManager.Info($"[高度检测] ✅ 找到包含通道: {containingChannel.DisplayName}");
|
||
|
||
// 检查缓存
|
||
var cacheKey = GenerateCacheKey(containingChannel, position);
|
||
if (_heightCache.TryGetValue(cacheKey, out var cachedInfo))
|
||
{
|
||
// 使用缓存的高度信息,但调用精确高度计算方法来确定最终高度
|
||
var preciseHeight = CalculatePreciseHeightAtPosition(position, cachedInfo, containingChannel);
|
||
LogManager.Debug($"[高度检测] 使用缓存的高度信息: {preciseHeight:F2}");
|
||
return preciseHeight;
|
||
}
|
||
|
||
// 分析通道几何,获取高度信息
|
||
var heightInfo = AnalyzeChannelGeometry(containingChannel);
|
||
if (heightInfo != null)
|
||
{
|
||
_heightCache[cacheKey] = heightInfo;
|
||
|
||
// 根据位置在通道中的具体位置计算精确高度
|
||
var preciseHeight = CalculatePreciseHeightAtPosition(position, heightInfo, containingChannel);
|
||
LogManager.Debug($"[高度检测] 计算得到精确高度: {preciseHeight:F2}");
|
||
return preciseHeight;
|
||
}
|
||
|
||
// 如果几何分析失败,使用射线投射方法
|
||
var raycastHeight = GetHeightByRaycast(position, containingChannel);
|
||
LogManager.Debug($"[高度检测] 射线投射得到高度: {raycastHeight:F2}");
|
||
return raycastHeight;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[高度检测] 检测高度时发生错误: {ex.Message}");
|
||
return _coordinateSystem.GetElevation(position);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取通道顶面高度(物体行驶表面)
|
||
/// 专门用于物体路径规划的高度校正,返回楼板/通道结构的顶面高度
|
||
///
|
||
/// 应用场景:
|
||
/// - 楼板结构:物体在楼板顶面行驶,而不是底面
|
||
/// - 走廊/通道:返回可行驶表面的高度
|
||
/// - 路径规划:确保物体路径点位于正确的行驶表面
|
||
/// </summary>
|
||
/// <param name="position">位置坐标</param>
|
||
/// <param name="channelItems">通道模型项集合(包含楼板、走廊等可通行结构)</param>
|
||
/// <returns>通道顶面高度(米),即物体实际行驶的表面高度</returns>
|
||
public double GetChannelTopHeight(Point3D position, IEnumerable<ModelItem> channelItems)
|
||
{
|
||
try
|
||
{
|
||
//LogManager.Info($"[高度检测] 🔍 开始检测位置 ({position.X:F2}, {position.Y:F2}, {position.Z:F2}) 的顶面高度,通道数={channelItems?.Count() ?? 0}");
|
||
|
||
// 寻找包含该位置的通道
|
||
var containingChannel = FindContainingChannel(position, channelItems);
|
||
if (containingChannel == null)
|
||
{
|
||
double fallbackElevation = _coordinateSystem.GetElevation(position);
|
||
LogManager.Info($"[高度检测] ❌ 位置 ({position.X:F2}, {position.Y:F2}, {position.Z:F2}) 未找到包含的通道,使用当前宿主高程: {fallbackElevation:F2}");
|
||
return fallbackElevation;
|
||
}
|
||
|
||
//LogManager.Info($"[高度检测] ✅ 找到包含通道: {containingChannel.DisplayName}");
|
||
|
||
// 检查缓存
|
||
var cacheKey = GenerateCacheKey(containingChannel, position);
|
||
if (_heightCache.TryGetValue(cacheKey, out var cachedInfo))
|
||
{
|
||
// 🔥 关键修改:返回通道顶面高度而不是底面高度
|
||
var topHeight = cachedInfo.CeilingHeight;
|
||
//LogManager.Info($"[高度检测] 使用缓存的顶面高度信息: {topHeight:F2}");
|
||
return topHeight;
|
||
}
|
||
|
||
// 分析通道几何,获取高度信息
|
||
var heightInfo = AnalyzeChannelGeometry(containingChannel);
|
||
if (heightInfo != null)
|
||
{
|
||
_heightCache[cacheKey] = heightInfo;
|
||
|
||
// 🔥 关键修改:返回通道顶面高度(楼板表面,物体行驶面)
|
||
var topHeight = heightInfo.CeilingHeight;
|
||
//LogManager.Info($"[高度检测] 计算得到通道顶面高度: {topHeight:F2}");
|
||
return topHeight;
|
||
}
|
||
|
||
// 如果几何分析失败,尝试使用边界框获取顶面高度
|
||
var bounds = containingChannel.BoundingBox();
|
||
if (bounds != null)
|
||
{
|
||
var (_, fallbackTopHeight) = GetBoundsElevationRange(bounds, _coordinateSystem);
|
||
LogManager.Info($"[高度检测] 使用边界框顶面高度: {fallbackTopHeight:F2}");
|
||
return fallbackTopHeight;
|
||
}
|
||
|
||
// 最后的备选方案
|
||
double finalFallbackElevation = _coordinateSystem.GetElevation(position);
|
||
LogManager.Warning($"[高度检测] ⚠️ 无法获取通道顶面高度,使用当前宿主高程: {finalFallbackElevation:F2}");
|
||
return finalFallbackElevation;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[高度检测] 检测顶面高度时发生错误: {ex.Message}");
|
||
return _coordinateSystem.GetElevation(position);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析通道几何,提取高度信息
|
||
/// </summary>
|
||
/// <param name="channel">通道模型项</param>
|
||
/// <returns>通道高度信息</returns>
|
||
public ChannelHeightInfo AnalyzeChannelGeometry(ModelItem channel)
|
||
{
|
||
try
|
||
{
|
||
if (channel?.Geometry == null)
|
||
{
|
||
LogManager.Warning("[高度检测] 通道没有几何信息");
|
||
return null;
|
||
}
|
||
|
||
// 获取通道的边界框
|
||
var bounds = channel.Geometry.BoundingBox;
|
||
if (bounds == null)
|
||
{
|
||
LogManager.Warning("[高度检测] 无法获取通道边界框");
|
||
return null;
|
||
}
|
||
|
||
var (floorHeight, ceilingHeight) = GetBoundsElevationRange(bounds, _coordinateSystem);
|
||
|
||
// 创建高度信息对象
|
||
var heightInfo = new ChannelHeightInfo
|
||
{
|
||
FloorHeight = floorHeight,
|
||
CeilingHeight = ceilingHeight,
|
||
Type = ChannelType.Other, // 不再依赖类型判断
|
||
HeightProfile = new List<HeightSample>()
|
||
};
|
||
|
||
// 采样通道高度剖面
|
||
SampleChannelHeightProfile(channel, heightInfo);
|
||
|
||
return heightInfo;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[高度检测] 分析通道几何时发生错误: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 寻找包含指定位置的通道
|
||
/// </summary>
|
||
/// <param name="position">位置点</param>
|
||
/// <param name="channelItems">通道集合</param>
|
||
/// <returns>包含该位置的通道</returns>
|
||
private ModelItem FindContainingChannel(Point3D position, IEnumerable<ModelItem> channelItems)
|
||
{
|
||
if (channelItems == null) return null;
|
||
|
||
foreach (var channel in channelItems)
|
||
{
|
||
if (IsPositionInChannel(position, channel))
|
||
{
|
||
//LogManager.Debug($"[高度检测] 找到包含位置的通道: {channel.DisplayName}");
|
||
return channel;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查位置是否在通道内
|
||
/// </summary>
|
||
/// <param name="position">位置点</param>
|
||
/// <param name="channel">通道模型项</param>
|
||
/// <returns>是否在通道内</returns>
|
||
private bool IsPositionInChannel(Point3D position, ModelItem channel)
|
||
{
|
||
try
|
||
{
|
||
if (channel?.Geometry?.BoundingBox == null)
|
||
return false;
|
||
|
||
var bounds = channel.Geometry.BoundingBox;
|
||
return IsPositionWithinBounds(position, bounds, _coordinateSystem, 2.0);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 采样通道高度剖面
|
||
/// </summary>
|
||
/// <param name="channel">通道模型项</param>
|
||
/// <param name="heightInfo">高度信息对象</param>
|
||
private void SampleChannelHeightProfile(ModelItem channel, ChannelHeightInfo heightInfo)
|
||
{
|
||
try
|
||
{
|
||
var bounds = channel.Geometry.BoundingBox;
|
||
var sampleCount = 10; // 采样10个点
|
||
|
||
// 沿通道长度方向采样
|
||
for (int i = 0; i < sampleCount; i++)
|
||
{
|
||
double ratio = (double)i / (sampleCount - 1);
|
||
var samplePosition = CreateHeightProfileSamplePoint(bounds, _coordinateSystem, ratio);
|
||
var (floorHeight, ceilingHeight) = GetBoundsElevationRange(bounds, _coordinateSystem);
|
||
|
||
var sample = new HeightSample
|
||
{
|
||
Position = samplePosition,
|
||
FloorHeight = floorHeight,
|
||
CeilingHeight = ceilingHeight
|
||
};
|
||
|
||
heightInfo.HeightProfile.Add(sample);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[高度检测] 采样高度剖面时发生错误: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据位置在通道中计算精确高度
|
||
/// </summary>
|
||
/// <param name="position">位置点</param>
|
||
/// <param name="heightInfo">通道高度信息</param>
|
||
/// <param name="channel">通道模型项</param>
|
||
/// <returns>精确高度</returns>
|
||
private double CalculatePreciseHeightAtPosition(Point3D position, ChannelHeightInfo heightInfo, ModelItem channel)
|
||
{
|
||
LogManager.Info($"[高度计算] 开始精确表面高度检测: 位置({position.X:F2}, {position.Y:F2})");
|
||
|
||
// 方法1: 尝试使用几何射线投射进行精确表面检测
|
||
if (TryGeometricRaycast(position, channel, out double raycastHeight))
|
||
{
|
||
LogManager.Info($"[高度计算] ✅ 几何射线投射成功: {raycastHeight:F2}");
|
||
return raycastHeight;
|
||
}
|
||
|
||
// 方法2: 如果几何射线投射失败,尝试使用View API作为备选
|
||
if (TryGetGeometricSurfaceHeight(position, channel, out double viewApiHeight))
|
||
{
|
||
LogManager.Info($"[高度计算] ✅ View API备选检测成功: {viewApiHeight:F2}");
|
||
return viewApiHeight;
|
||
}
|
||
|
||
// 方法3: 最后使用几何分析
|
||
var surfaceHeight = AnalyzeSurfaceHeightAtPosition(position, channel, heightInfo.FloorHeight, heightInfo.CeilingHeight);
|
||
LogManager.Info($"[高度计算] 📐 几何分析结果: {surfaceHeight:F2}");
|
||
return surfaceHeight;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从高度剖面插值计算高度
|
||
/// </summary>
|
||
/// <param name="position">位置点</param>
|
||
/// <param name="heightProfile">高度剖面</param>
|
||
/// <returns>插值高度</returns>
|
||
private double InterpolateHeightFromProfile(Point3D position, List<HeightSample> heightProfile)
|
||
{
|
||
if (heightProfile.Count == 0) return 0;
|
||
if (heightProfile.Count == 1) return heightProfile[0].CeilingHeight;
|
||
|
||
// 找到最近的两个采样点进行插值
|
||
var closestSamples = heightProfile
|
||
.OrderBy(s => CalculatePlanarDistance(s.Position, position, _coordinateSystem))
|
||
.Take(2)
|
||
.ToList();
|
||
|
||
if (closestSamples.Count == 1)
|
||
{
|
||
return closestSamples[0].CeilingHeight;
|
||
}
|
||
|
||
// 线性插值
|
||
var sample1 = closestSamples[0];
|
||
var sample2 = closestSamples[1];
|
||
|
||
var distance1 = CalculatePlanarDistance(sample1.Position, position, _coordinateSystem);
|
||
var distance2 = CalculatePlanarDistance(sample2.Position, position, _coordinateSystem);
|
||
var totalDistance = distance1 + distance2;
|
||
|
||
if (totalDistance < 0.001) return sample1.CeilingHeight;
|
||
|
||
var weight1 = 1.0 - (distance1 / totalDistance);
|
||
var weight2 = 1.0 - (distance2 / totalDistance);
|
||
|
||
return (sample1.CeilingHeight * weight1 + sample2.CeilingHeight * weight2) / (weight1 + weight2);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用射线投射获取高度
|
||
/// </summary>
|
||
/// <param name="position">位置点</param>
|
||
/// <param name="channel">通道模型项</param>
|
||
/// <returns>射线投射得到的高度</returns>
|
||
private double GetHeightByRaycast(Point3D position, ModelItem channel)
|
||
{
|
||
try
|
||
{
|
||
// 尝试使用Navisworks几何分析获取更精确的表面高度
|
||
if (channel?.Geometry?.BoundingBox != null)
|
||
{
|
||
var bbox = channel.Geometry.BoundingBox;
|
||
|
||
if (IsPositionWithinBounds(position, bbox, _coordinateSystem, 0.0))
|
||
{
|
||
// 位置在通道范围内,尝试分析表面高度
|
||
var (floorHeight, ceilingHeight) = GetBoundsElevationRange(bbox, _coordinateSystem);
|
||
var surfaceHeight = AnalyzeSurfaceHeightAtPosition(position, channel, floorHeight, ceilingHeight);
|
||
LogManager.Debug($"[射线投射] 表面高度分析结果: {surfaceHeight:F2}");
|
||
return surfaceHeight;
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[射线投射] 位置({position.X:F2}, {position.Y:F2}, {position.Z:F2})不在通道范围内");
|
||
return _coordinateSystem.GetElevation(position);
|
||
}
|
||
}
|
||
|
||
double fallbackElevation = _coordinateSystem.GetElevation(position);
|
||
LogManager.Warning($"[射线投射] 通道几何信息无效,使用当前宿主高程: {fallbackElevation:F2}");
|
||
return fallbackElevation;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[射线投射] 射线投射计算失败: {ex.Message}");
|
||
return _coordinateSystem.GetElevation(position);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析指定位置的表面高度
|
||
/// </summary>
|
||
/// <param name="position">位置</param>
|
||
/// <param name="channel">通道模型</param>
|
||
/// <param name="floorHeight">地面高度</param>
|
||
/// <param name="ceilingHeight">顶面高度</param>
|
||
/// <returns>表面高度</returns>
|
||
private double AnalyzeSurfaceHeightAtPosition(Point3D position, ModelItem channel, double floorHeight, double ceilingHeight)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info($"[表面分析] 开始分析位置({position.X:F2}, {position.Y:F2})的实际表面高度");
|
||
LogManager.Info($"[表面分析] 通道边界: 地面={floorHeight:F2}, 顶面={ceilingHeight:F2}");
|
||
|
||
// 尝试多种方法获取真实表面高度
|
||
|
||
// 方法1: 使用射线投射从上方检测表面
|
||
var raycastHeight = TryRaycastFromAbove(position, channel, ceilingHeight);
|
||
if (raycastHeight.HasValue)
|
||
{
|
||
LogManager.Info($"[表面分析] ✅ 射线投射检测到表面高度: {raycastHeight.Value:F2}");
|
||
return raycastHeight.Value;
|
||
}
|
||
|
||
// 方法2: 使用多点采样获取局部表面高度
|
||
var sampledHeight = TryMultiPointSampling(position, channel);
|
||
if (sampledHeight.HasValue)
|
||
{
|
||
LogManager.Info($"[表面分析] ✅ 多点采样检测到表面高度: {sampledHeight.Value:F2}");
|
||
return sampledHeight.Value;
|
||
}
|
||
|
||
// 方法3: 作为最后手段,返回几何中心高度
|
||
var midHeight = (floorHeight + ceilingHeight) / 2.0;
|
||
LogManager.Warning($"[表面分析] ⚠️ 无法检测到精确表面,使用几何中心高度: {midHeight:F2}");
|
||
return midHeight;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[表面分析] 表面高度分析失败: {ex.Message},使用顶面高度");
|
||
return ceilingHeight;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试获取几何表面高度(使用View投影和PickItemFromPoint方法)
|
||
/// </summary>
|
||
private bool TryGetGeometricSurfaceHeight(Point3D position, ModelItem channel, out double height)
|
||
{
|
||
height = 0;
|
||
try
|
||
{
|
||
// 获取当前激活的视图
|
||
var activeView = Application.ActiveDocument?.ActiveView;
|
||
if (activeView == null)
|
||
{
|
||
LogManager.Debug($"[几何分析] 无法获取活动视图");
|
||
return false;
|
||
}
|
||
|
||
// 将3D世界坐标投影到屏幕坐标
|
||
if (TryProjectWorldToScreen(position, activeView, out GridPoint2D screenPoint))
|
||
{
|
||
// 检查屏幕坐标是否在视图范围内
|
||
if (screenPoint.X >= 0 && screenPoint.X < activeView.Width &&
|
||
screenPoint.Y >= 0 && screenPoint.Y < activeView.Height)
|
||
{
|
||
// 使用PickItemFromPoint获取该屏幕位置的实际表面点
|
||
var pickResult = activeView.PickItemFromPoint((int)screenPoint.X, (int)screenPoint.Y);
|
||
if (pickResult != null)
|
||
{
|
||
LogManager.Debug($"[几何分析] PickItemFromPoint结果: 模型={pickResult.ModelItem?.DisplayName ?? "NULL"}, 3D点=({pickResult.Point.X:F2}, {pickResult.Point.Y:F2}, {pickResult.Point.Z:F2})");
|
||
|
||
// 验证拾取到的模型是否为目标通道(或其子项)
|
||
if (pickResult.ModelItem.Equals(channel) || ModelItemAnalysisHelper.IsChildOf(pickResult.ModelItem, channel))
|
||
{
|
||
height = GetPickedSurfaceElevation(
|
||
pickResult.Point.X,
|
||
pickResult.Point.Y,
|
||
pickResult.Point.Z,
|
||
_coordinateSystem.Type);
|
||
LogManager.Info($"[几何分析] ✅ 使用投影法获得精确表面高度: {height:F2},通道: {channel.DisplayName}");
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
LogManager.Debug($"[几何分析] 拾取到的模型不是目标通道: 拾取={pickResult.ModelItem?.DisplayName}, 目标={channel.DisplayName}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogManager.Debug($"[几何分析] PickItemFromPoint在屏幕坐标 ({screenPoint.X:F1}, {screenPoint.Y:F1}) 处未找到模型");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogManager.Debug($"[几何分析] 投影的屏幕坐标 ({screenPoint.X:F1}, {screenPoint.Y:F1}) 超出视图范围 ({activeView.Width}x{activeView.Height})");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogManager.Debug($"[几何分析] 3D坐标投影到屏幕失败");
|
||
}
|
||
|
||
return false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[几何分析] 几何表面高度获取失败: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将3D世界坐标投影到屏幕坐标
|
||
/// </summary>
|
||
private bool TryProjectWorldToScreen(Point3D worldPoint, View view, out GridPoint2D screenPoint)
|
||
{
|
||
screenPoint = new GridPoint2D(0, 0);
|
||
try
|
||
{
|
||
// 使用Navisworks View API的ProjectPoint方法进行精确投影
|
||
var projectionResult = view.ProjectPoint(worldPoint, false, false);
|
||
if (projectionResult != null)
|
||
{
|
||
screenPoint = new GridPoint2D((int)projectionResult.X, (int)projectionResult.Y);
|
||
LogManager.Debug($"[投影] 3D世界坐标 ({worldPoint.X:F2}, {worldPoint.Y:F2}, {worldPoint.Z:F2}) 投影到屏幕坐标 ({projectionResult.X:F1}, {projectionResult.Y:F1})");
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
LogManager.Debug($"[投影] 投影失败,ProjectPoint返回null");
|
||
return false;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Debug($"[投影] 投影计算失败: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算位置在通道中的相对位置
|
||
/// </summary>
|
||
private Point3D CalculateRelativePositionInChannel(Point3D position, BoundingBox3D bbox)
|
||
{
|
||
var (positionH1, positionH2) = _coordinateSystem.GetHorizontalCoords(position);
|
||
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bbox);
|
||
var (minElevation, maxElevation) = GetBoundsElevationRange(bbox, _coordinateSystem);
|
||
|
||
var relativeX = Math.Abs(maxH1 - minH1) < 1e-9 ? 0.0 : (positionH1 - minH1) / (maxH1 - minH1);
|
||
var relativeY = Math.Abs(maxH2 - minH2) < 1e-9 ? 0.0 : (positionH2 - minH2) / (maxH2 - minH2);
|
||
var currentElevation = _coordinateSystem.GetElevation(position);
|
||
var relativeZ = Math.Abs(maxElevation - minElevation) < 1e-9 ? 0.0 : (currentElevation - minElevation) / (maxElevation - minElevation);
|
||
|
||
return new Point3D(relativeX, relativeY, relativeZ);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 检查两个边界框是否重叠
|
||
/// </summary>
|
||
private bool BoundingBoxesOverlap(BoundingBox3D bbox1, BoundingBox3D bbox2)
|
||
{
|
||
return !(bbox1.Max.X < bbox2.Min.X || bbox2.Max.X < bbox1.Min.X ||
|
||
bbox1.Max.Y < bbox2.Min.Y || bbox2.Max.Y < bbox1.Min.Y ||
|
||
bbox1.Max.Z < bbox2.Min.Z || bbox2.Max.Z < bbox1.Min.Z);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成缓存键
|
||
/// </summary>
|
||
/// <param name="channel">通道模型项</param>
|
||
/// <param name="position">位置点</param>
|
||
/// <returns>缓存键</returns>
|
||
private string GenerateCacheKey(ModelItem channel, Point3D position)
|
||
{
|
||
// 使用通道的实例ID和位置的网格坐标作为缓存键
|
||
var (h1, h2) = _coordinateSystem.GetHorizontalCoords(position);
|
||
var gridX = (int)(h1 / 1.0);
|
||
var gridY = (int)(h2 / 1.0);
|
||
return $"{channel.InstanceGuid}_{gridX}_{gridY}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除高度缓存
|
||
/// </summary>
|
||
public void ClearCache()
|
||
{
|
||
_heightCache.Clear();
|
||
LogManager.Info("[高度检测] 高度缓存已清除");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取缓存统计信息
|
||
/// </summary>
|
||
/// <returns>缓存项数量</returns>
|
||
public int GetCacheCount()
|
||
{
|
||
return _heightCache.Count;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试从上方进行射线投射检测表面高度
|
||
/// </summary>
|
||
/// <param name="position">目标位置</param>
|
||
/// <param name="channel">通道模型</param>
|
||
/// <param name="maxHeight">最大高度</param>
|
||
/// <returns>检测到的表面高度,如果失败则返回null</returns>
|
||
private double? TryRaycastFromAbove(Point3D position, ModelItem channel, double maxHeight)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info($"[射线投射] 从上方({position.X:F2}, {position.Y:F2}, {maxHeight:F2})向下投射射线");
|
||
|
||
// 获取当前活动视图
|
||
var activeView = Application.ActiveDocument?.ActiveView;
|
||
if (activeView == null)
|
||
{
|
||
LogManager.Debug($"[射线投射] 无法获取活动视图");
|
||
return null;
|
||
}
|
||
|
||
// 从通道上方的点开始,向下投射多条射线
|
||
var testHeights = new[] { maxHeight + 100, maxHeight + 50, maxHeight + 10 };
|
||
|
||
foreach (var testHeight in testHeights)
|
||
{
|
||
var testPoint = _coordinateSystem.SetElevation(position, testHeight);
|
||
|
||
// 将3D点投影到屏幕
|
||
if (TryProjectWorldToScreen(testPoint, activeView, out GridPoint2D screenPoint))
|
||
{
|
||
// 检查屏幕坐标是否在视图范围内
|
||
if (screenPoint.X >= 0 && screenPoint.X < activeView.Width &&
|
||
screenPoint.Y >= 0 && screenPoint.Y < activeView.Height)
|
||
{
|
||
// 使用PickItemFromPoint获取该屏幕位置的实际表面点
|
||
var pickResult = activeView.PickItemFromPoint(screenPoint.X, screenPoint.Y);
|
||
if (pickResult != null && (pickResult.ModelItem.Equals(channel) || ModelItemAnalysisHelper.IsChildOf(pickResult.ModelItem, channel)))
|
||
{
|
||
double detectedElevation = _coordinateSystem.GetElevation(pickResult.Point);
|
||
LogManager.Info($"[射线投射] ✅ 成功检测到表面点高程: {detectedElevation:F2}");
|
||
return detectedElevation;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
LogManager.Debug($"[射线投射] 所有射线投射尝试均失败");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[射线投射] 射线投射失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试COM API几何射线投射获取精确表面高度
|
||
/// </summary>
|
||
/// <param name="position">目标位置</param>
|
||
/// <param name="channel">通道模型</param>
|
||
/// <param name="surfaceHeight">输出的表面高度</param>
|
||
/// <returns>是否成功获取表面高度</returns>
|
||
private bool TryGeometricRaycast(Point3D position, ModelItem channel, out double surfaceHeight)
|
||
{
|
||
surfaceHeight = 0.0;
|
||
try
|
||
{
|
||
LogManager.Info($"[COM几何射线] 开始从ModelItem提取三角形几何数据: {channel.DisplayName}");
|
||
|
||
// 使用COM API提取三角形几何数据
|
||
var triangles = GeometryHelper.ExtractTriangles(new[] { channel });
|
||
if (triangles.Count == 0)
|
||
{
|
||
LogManager.Warning($"[COM几何射线] 未能从ModelItem提取到三角形数据: {channel.DisplayName}");
|
||
return false;
|
||
}
|
||
|
||
LogManager.Info($"[COM几何射线] 成功提取到 {triangles.Count} 个三角形,开始射线-三角形交点计算");
|
||
|
||
// 执行垂直射线与三角形的交点检测
|
||
var intersectionPoints = PerformRayTriangleIntersection(position, triangles);
|
||
|
||
if (intersectionPoints.Count > 0)
|
||
{
|
||
// 选择最高的交点作为表面高度
|
||
surfaceHeight = intersectionPoints.Max();
|
||
LogManager.Info($"[COM几何射线] ✅ 射线-三角形交点检测成功: 找到 {intersectionPoints.Count} 个交点,最高点Z={surfaceHeight:F2}");
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[COM几何射线] 射线-三角形交点检测失败: 位置({position.X:F2}, {position.Y:F2})未与任何三角形相交");
|
||
return false;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[COM几何射线] COM API几何射线投射出错: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行射线与三角形的交点检测
|
||
/// </summary>
|
||
/// <param name="position">射线起点的水平坐标</param>
|
||
/// <param name="triangles">三角形列表</param>
|
||
/// <returns>交点的高度值列表</returns>
|
||
private List<double> PerformRayTriangleIntersection(Point3D position, List<Triangle3D> triangles)
|
||
{
|
||
var intersectionPoints = new List<double>();
|
||
|
||
try
|
||
{
|
||
// 获取坐标系的垂直扫描方向
|
||
var scanDirection = _coordinateSystem.VerticalScanDirection;
|
||
|
||
// 计算三角形在垂直方向上的最大范围
|
||
double maxHeight = 0;
|
||
if (triangles.Count > 0)
|
||
{
|
||
maxHeight = triangles.Max(t =>
|
||
Math.Max(
|
||
_coordinateSystem.GetElevation(t.Point1),
|
||
Math.Max(
|
||
_coordinateSystem.GetElevation(t.Point2),
|
||
_coordinateSystem.GetElevation(t.Point3)
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
// 从最高点上方开始扫描
|
||
var scanStartHeight = maxHeight + 1000.0;
|
||
var rayOrigin = _coordinateSystem.SetElevation(position, scanStartHeight);
|
||
|
||
// 使用坐标系的垂直扫描方向(转换为 Point3D)
|
||
var rayDirection = new Point3D(scanDirection.X, scanDirection.Y, scanDirection.Z);
|
||
|
||
int intersectionCount = 0;
|
||
foreach (var triangle in triangles)
|
||
{
|
||
if (GeometryHelper.RayTriangleIntersect(rayOrigin, rayDirection, triangle, out double intersectionElevation))
|
||
{
|
||
intersectionPoints.Add(intersectionElevation);
|
||
intersectionCount++;
|
||
}
|
||
}
|
||
|
||
LogManager.Debug($"[射线-三角形] 找到 {intersectionCount} 个交点,扫描方向: ({rayDirection.X:F2}, {rayDirection.Y:F2}, {rayDirection.Z:F2})");
|
||
|
||
return intersectionPoints;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[射线-三角形] 射线-三角形交点计算失败: {ex.Message}");
|
||
return intersectionPoints;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试多点采样获取表面高度
|
||
/// </summary>
|
||
/// <param name="position">中心位置</param>
|
||
/// <param name="channel">通道模型</param>
|
||
/// <returns>采样得到的表面高度,如果失败则返回null</returns>
|
||
private double? TryMultiPointSampling(Point3D position, ModelItem channel)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info($"[多点采样] 在位置({position.X:F2}, {position.Y:F2})周围进行多点采样");
|
||
|
||
var activeView = Application.ActiveDocument?.ActiveView;
|
||
if (activeView == null)
|
||
{
|
||
LogManager.Debug($"[多点采样] 无法获取活动视图");
|
||
return null;
|
||
}
|
||
|
||
var validHeights = new List<double>();
|
||
var sampleRadius = 50.0; // 50单位半径内采样
|
||
var samplePoints = new[]
|
||
{
|
||
CreatePlanarOffsetPoint(position, _coordinateSystem, 0.0, 0.0),
|
||
CreatePlanarOffsetPoint(position, _coordinateSystem, sampleRadius, 0.0),
|
||
CreatePlanarOffsetPoint(position, _coordinateSystem, -sampleRadius, 0.0),
|
||
CreatePlanarOffsetPoint(position, _coordinateSystem, 0.0, sampleRadius),
|
||
CreatePlanarOffsetPoint(position, _coordinateSystem, 0.0, -sampleRadius),
|
||
};
|
||
|
||
foreach (var samplePoint in samplePoints)
|
||
{
|
||
if (TryProjectWorldToScreen(samplePoint, activeView, out GridPoint2D screenPoint))
|
||
{
|
||
if (screenPoint.X >= 0 && screenPoint.X < activeView.Width &&
|
||
screenPoint.Y >= 0 && screenPoint.Y < activeView.Height)
|
||
{
|
||
var pickResult = activeView.PickItemFromPoint((int)screenPoint.X, (int)screenPoint.Y);
|
||
if (pickResult != null && (pickResult.ModelItem.Equals(channel) || ModelItemAnalysisHelper.IsChildOf(pickResult.ModelItem, channel)))
|
||
{
|
||
double detectedElevation = _coordinateSystem.GetElevation(pickResult.Point);
|
||
validHeights.Add(detectedElevation);
|
||
LogManager.Debug($"[多点采样] 采样点({samplePoint.X:F1}, {samplePoint.Y:F1}, {samplePoint.Z:F1})高程: {detectedElevation:F2}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (validHeights.Count > 0)
|
||
{
|
||
// 使用中位数减少异常值影响
|
||
validHeights.Sort();
|
||
var medianHeight = validHeights[validHeights.Count / 2];
|
||
LogManager.Info($"[多点采样] ✅ 采样成功,采样点数={validHeights.Count},中位数高度={medianHeight:F2}");
|
||
return medianHeight;
|
||
}
|
||
|
||
LogManager.Debug($"[多点采样] 没有找到有效的采样点");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[多点采样] 多点采样失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static (double min, double max) GetBoundsElevationRange(BoundingBox3D bounds, ICoordinateSystem coordinateSystem)
|
||
{
|
||
if (bounds == null)
|
||
{
|
||
throw new ArgumentNullException(nameof(bounds));
|
||
}
|
||
|
||
if (coordinateSystem == null)
|
||
{
|
||
throw new ArgumentNullException(nameof(coordinateSystem));
|
||
}
|
||
|
||
return GetBoundsElevationRange(
|
||
bounds.Min.X, bounds.Min.Y, bounds.Min.Z,
|
||
bounds.Max.X, bounds.Max.Y, bounds.Max.Z,
|
||
coordinateSystem.Type);
|
||
}
|
||
|
||
private static Point3D CreateHeightProfileSamplePoint(BoundingBox3D bounds, ICoordinateSystem coordinateSystem, double ratio)
|
||
{
|
||
if (bounds == null)
|
||
{
|
||
throw new ArgumentNullException(nameof(bounds));
|
||
}
|
||
|
||
if (coordinateSystem == null)
|
||
{
|
||
throw new ArgumentNullException(nameof(coordinateSystem));
|
||
}
|
||
|
||
return CreateHeightProfileSamplePoint(
|
||
bounds.Min.X, bounds.Min.Y, bounds.Min.Z,
|
||
bounds.Max.X, bounds.Max.Y, bounds.Max.Z,
|
||
coordinateSystem.Type,
|
||
ratio);
|
||
}
|
||
|
||
private static bool IsPositionWithinBounds(Point3D position, BoundingBox3D bounds, ICoordinateSystem coordinateSystem, double elevationTolerance)
|
||
{
|
||
if (position == null || bounds == null || coordinateSystem == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var (positionH1, positionH2) = coordinateSystem.GetHorizontalCoords(position);
|
||
var (minH1, maxH1, minH2, maxH2) = coordinateSystem.GetHorizontalRange(bounds);
|
||
var (minElevation, maxElevation) = coordinateSystem.GetHeightRange(bounds);
|
||
var elevation = coordinateSystem.GetElevation(position);
|
||
|
||
return positionH1 >= minH1 && positionH1 <= maxH1 &&
|
||
positionH2 >= minH2 && positionH2 <= maxH2 &&
|
||
elevation >= minElevation - elevationTolerance &&
|
||
elevation <= maxElevation + elevationTolerance;
|
||
}
|
||
|
||
private static double CalculatePlanarDistance(Point3D pointA, Point3D pointB, ICoordinateSystem coordinateSystem)
|
||
{
|
||
var (aH1, aH2) = coordinateSystem.GetHorizontalCoords(pointA);
|
||
var (bH1, bH2) = coordinateSystem.GetHorizontalCoords(pointB);
|
||
return Math.Sqrt(Math.Pow(aH1 - bH1, 2) + Math.Pow(aH2 - bH2, 2));
|
||
}
|
||
|
||
private static Point3D CreatePlanarOffsetPoint(Point3D position, ICoordinateSystem coordinateSystem, double deltaH1, double deltaH2)
|
||
{
|
||
var (h1, h2) = coordinateSystem.GetHorizontalCoords(position);
|
||
var elevation = coordinateSystem.GetElevation(position);
|
||
return coordinateSystem.CreatePoint(h1 + deltaH1, h2 + deltaH2, elevation);
|
||
}
|
||
|
||
private static (double min, double max) GetBoundsElevationRange(
|
||
double minX, double minY, double minZ,
|
||
double maxX, double maxY, double maxZ,
|
||
CoordinateSystemType coordinateSystemType)
|
||
{
|
||
var adapter = new HostCoordinateAdapter(coordinateSystemType);
|
||
double elevation1 = HostPlanarGridHelper.GetElevation3(
|
||
new System.Numerics.Vector3((float)minX, (float)minY, (float)minZ),
|
||
adapter);
|
||
double elevation2 = HostPlanarGridHelper.GetElevation3(
|
||
new System.Numerics.Vector3((float)maxX, (float)maxY, (float)maxZ),
|
||
adapter);
|
||
return (Math.Min(elevation1, elevation2), Math.Max(elevation1, elevation2));
|
||
}
|
||
|
||
private static Point3D CreateHeightProfileSamplePoint(
|
||
double minX, double minY, double minZ,
|
||
double maxX, double maxY, double maxZ,
|
||
CoordinateSystemType coordinateSystemType,
|
||
double ratio)
|
||
{
|
||
var samplePoint = CreateHeightProfileSamplePoint3(
|
||
minX, minY, minZ,
|
||
maxX, maxY, maxZ,
|
||
coordinateSystemType,
|
||
ratio);
|
||
return new Point3D(samplePoint.X, samplePoint.Y, samplePoint.Z);
|
||
}
|
||
|
||
private static System.Numerics.Vector3 CreateHeightProfileSamplePoint3(
|
||
double minX, double minY, double minZ,
|
||
double maxX, double maxY, double maxZ,
|
||
CoordinateSystemType coordinateSystemType,
|
||
double ratio)
|
||
{
|
||
var adapter = new HostCoordinateAdapter(coordinateSystemType);
|
||
var hostMin = new System.Numerics.Vector3((float)minX, (float)minY, (float)minZ);
|
||
var hostMax = new System.Numerics.Vector3((float)maxX, (float)maxY, (float)maxZ);
|
||
var (minH1, maxH1, minH2, maxH2) = HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
|
||
var (minElevation, _) = GetBoundsElevationRange(minX, minY, minZ, maxX, maxY, maxZ, coordinateSystemType);
|
||
double sampleH1 = minH1 + (maxH1 - minH1) * ratio;
|
||
double sampleH2 = minH2 + (maxH2 - minH2) * ratio;
|
||
return HostPlanarGridHelper.CreateHostPoint3(sampleH1, sampleH2, minElevation, adapter);
|
||
}
|
||
|
||
private static double GetPickedSurfaceElevation(
|
||
double pointX,
|
||
double pointY,
|
||
double pointZ,
|
||
CoordinateSystemType coordinateSystemType)
|
||
{
|
||
var adapter = new HostCoordinateAdapter(coordinateSystemType);
|
||
return HostPlanarGridHelper.GetElevation3(
|
||
new System.Numerics.Vector3((float)pointX, (float)pointY, (float)pointZ),
|
||
adapter);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通道高度信息
|
||
/// </summary>
|
||
public class ChannelHeightInfo
|
||
{
|
||
/// <summary>
|
||
/// 地面高度(米)
|
||
/// </summary>
|
||
public double FloorHeight { get; set; }
|
||
|
||
/// <summary>
|
||
/// 顶面高度(米)
|
||
/// </summary>
|
||
public double CeilingHeight { get; set; }
|
||
|
||
/// <summary>
|
||
/// 通道类型
|
||
/// </summary>
|
||
public ChannelType Type { get; set; }
|
||
|
||
/// <summary>
|
||
/// 高度剖面采样点
|
||
/// </summary>
|
||
public List<HeightSample> HeightProfile { get; set; } = new List<HeightSample>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 高度采样点
|
||
/// </summary>
|
||
public class HeightSample
|
||
{
|
||
/// <summary>
|
||
/// 采样位置
|
||
/// </summary>
|
||
public Point3D Position { get; set; }
|
||
|
||
/// <summary>
|
||
/// 该位置的地面高度
|
||
/// </summary>
|
||
public double FloorHeight { get; set; }
|
||
|
||
/// <summary>
|
||
/// 该位置的顶面高度
|
||
/// </summary>
|
||
public double CeilingHeight { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通道类型枚举
|
||
/// </summary>
|
||
public enum ChannelType
|
||
{
|
||
/// <summary>
|
||
/// 走廊/通道
|
||
/// </summary>
|
||
Corridor,
|
||
|
||
/// <summary>
|
||
/// 楼梯
|
||
/// </summary>
|
||
Stairs,
|
||
|
||
/// <summary>
|
||
/// 坡道
|
||
/// </summary>
|
||
Ramp,
|
||
|
||
/// <summary>
|
||
/// 电梯
|
||
/// </summary>
|
||
Elevator,
|
||
|
||
/// <summary>
|
||
/// 其他
|
||
/// </summary>
|
||
Other
|
||
}
|
||
|
||
}
|