解决空轨计算提取三角形引起的程序崩溃问题。

This commit is contained in:
tian 2026-01-13 11:11:56 +08:00
parent b1da586b27
commit 779d043299
4 changed files with 26 additions and 533 deletions

View File

@ -319,7 +319,7 @@ msiexec /i NavisworksTransport.Setup.msi /qn /l*v install.log
- **日志记录**: 使用内置的LogManager
- **异常处理**: 通过GlobalExceptionHandler统一管理,不掩盖问题
- **线程安全**: COM API调用必须在主UI线程(STA)执行
- **单位系统**:
- **单位系统**:
- **内部计算**: 统一使用模型单位(避免频繁转换,提高性能)
- **对外输出**: 转换为米(m)单位(用户界面、日志、数据库存储等)
- **单位转换**: 使用 `UnitsConverter.ConvertToMeters()``UnitsConverter.ConvertFromMeters()` 进行转换

View File

@ -546,6 +546,13 @@ namespace NavisworksTransport
if (items == null || items.Count == 0)
return;
// 确保在主线程上执行Navisworks COM API 要求)
if (!NavisworksApiHelper.IsOnUIThread())
{
NavisworksApiHelper.ExecuteOnUIThread(() => PreCalculateRailBaselinePaths(items));
return;
}
LogManager.Info($"[空轨] 开始预计算 {items.Count} 个空轨模型的基准路径");
int successCount = 0;
@ -556,7 +563,7 @@ namespace NavisworksTransport
try
{
// 调用RailGeometryHelper提取基准路径
var baselinePath = PathPlanning.RailGeometryHelper.ExtractRailBaselinePath(item);
var baselinePath = RailGeometryHelper.ExtractRailBaselinePath(item);
if (baselinePath != null && baselinePath.PathPoints != null && baselinePath.PathPoints.Count > 0)
{

View File

@ -407,6 +407,12 @@ namespace NavisworksTransport.PathPlanning
/// <returns>成功提取的基准路径数量</returns>
public static int ExtractAndRenderAllRailBaselines()
{
// 确保在主线程上执行Navisworks COM API 要求)
if (!NavisworksApiHelper.IsOnUIThread())
{
return NavisworksApiHelper.ExecuteOnUIThread(ExtractAndRenderAllRailBaselines);
}
try
{
LogManager.Info("[空轨] 开始提取所有空轨模型的基准路径");
@ -458,15 +464,6 @@ namespace NavisworksTransport.PathPlanning
return 0;
}
}
/// <summary>
/// 兼容方法:从空轨几何体提取下表面中心线(一步完成)
/// </summary>
/// <param name="railModel">空轨模型项</param>
/// <param name="startPoint">起点3D坐标</param>
/// <param name="endPoint">终点3D坐标</param>
/// <param name="samplingInterval">采样间隔模型单位默认0.5</param>
/// <returns>路径点列表包含精确的Z坐标</returns>
/// <summary>
/// 使用 OBB 包围体提取空轨下表面中心线(新方法,支持倾斜)
@ -726,21 +723,23 @@ namespace NavisworksTransport.PathPlanning
centroid.Y + axes[0].Y * (minX + maxX) / 2 + axes[1].Y * (minY + maxY) / 2 + axes[2].Y * (minZ + maxZ) / 2,
centroid.Z + axes[0].Z * (minX + maxX) / 2 + axes[1].Z * (minY + maxY) / 2 + axes[2].Z * (minZ + maxZ) / 2
);
var obb = new OrientedBoundingBox
{
Center = obbCenter,
Axes = axes,
Extents = new Point3D(
(maxX - minX) / 2,
(maxY - minY) / 2,
(maxZ - minZ) / 2
)
),
// 保存投影范围信息,用于计算下表面
ProjectedMinZ = minZ,
ProjectedMaxZ = maxZ
};
// 保存投影范围信息,用于计算下表面
obb.ProjectedMinZ = minZ;
obb.ProjectedMaxZ = maxZ;
LogManager.Info($"[空轨] OBB各轴长度模型单位: {obb.GetAxisLength(0):F2}, {obb.GetAxisLength(1):F2}, {obb.GetAxisLength(2):F2}");
@ -796,166 +795,6 @@ namespace NavisworksTransport.PathPlanning
return result;
}
/// <summary>
/// 兼容方法:从空轨几何体提取下表面中心线(一步完成)
/// </summary>
/// <param name="railModel">空轨模型项</param>
/// <param name="startPoint">起点3D坐标</param>
/// <param name="endPoint">终点3D坐标</param>
/// <param name="samplingInterval">采样间隔模型单位默认0.5</param>
/// <returns>路径点列表包含精确的Z坐标</returns>
public static List<Point3D> ExtractRailBottomCenterLine(
ModelItem railModel,
Point3D startPoint,
Point3D endPoint,
double samplingInterval = 0.5)
{
try
{
LogManager.Info($"[空轨] 开始提取下表面中心线: {railModel.DisplayName}");
LogManager.Info($"[空轨] 起点: ({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2})");
LogManager.Info($"[空轨] 终点: ({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})");
LogManager.Info($"[空轨] 采样间隔: {samplingInterval:F2}");
// 1. 提取空轨下表面信息(带缓存)
var surfaceInfo = ExtractRailBottomSurface(railModel);
if (surfaceInfo == null || surfaceInfo.BottomTriangles.Count == 0)
{
throw new InvalidOperationException($"无法从空轨模型提取下表面三角形: {railModel.DisplayName}");
}
LogManager.Info($"[空轨] 提取到 {surfaceInfo.BottomTriangles.Count} 个下表面三角形");
// 2. 提取骨架线
var skeleton = ExtractSkeletonFromTriangles(surfaceInfo.BottomTriangles);
LogManager.Info($"[空轨] 提取到 {skeleton.Count} 个骨架点");
// 3. 沿骨架线生成采样点
var samplePoints = GenerateSamplePointsAlongSkeleton(skeleton, samplingInterval);
LogManager.Info($"[空轨] 生成 {samplePoints.Count} 个采样点");
// 4. 对每个采样点,从下方射线投射获取精确高度
var pathPoints = new List<Point3D>();
int successCount = 0;
int failCount = 0;
foreach (var samplePoint in samplePoints)
{
try
{
double bottomZ = GetBottomHeightByRaycast(
samplePoint,
surfaceInfo.BottomTriangles,
surfaceInfo.MinZ);
if (bottomZ > surfaceInfo.MinZ - 1000) // 检查是否有效
{
pathPoints.Add(new Point3D(
samplePoint.X,
samplePoint.Y,
bottomZ));
successCount++;
}
else
{
LogManager.Warning($"[空轨] 采样点 ({samplePoint.X:F2}, {samplePoint.Y:F2}) 未找到有效交点");
failCount++;
}
}
catch (Exception ex)
{
LogManager.Warning($"[空轨] 采样点 ({samplePoint.X:F2}, {samplePoint.Y:F2}) 射线投射失败: {ex.Message}");
failCount++;
}
}
LogManager.Info($"[空轨] 提取完成: 成功 {successCount} 个点, 失败 {failCount} 个点");
if (pathPoints.Count < 2)
{
throw new InvalidOperationException($"提取的中心线点数不足: {pathPoints.Count} (最少需要2个)");
}
return pathPoints;
}
catch (Exception ex)
{
LogManager.Error($"[空轨] 提取下表面中心线失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 提取空轨下表面信息(带缓存)
/// </summary>
private static RailBottomSurfaceInfo ExtractRailBottomSurface(ModelItem railModel)
{
try
{
// 生成缓存键使用DisplayName作为唯一标识
var cacheKey = $"rail_{railModel.DisplayName}_{railModel.GetHashCode()}";
// 检查缓存
if (_railCache.TryGetValue(cacheKey, out var cachedInfo))
{
// 检查缓存是否过期
if (DateTime.Now - cachedInfo.CacheTime < _cacheValidDuration)
{
LogManager.Debug($"[空轨] 使用缓存的下表面信息");
return cachedInfo;
}
else
{
LogManager.Debug($"[空轨] 缓存已过期,重新提取");
_railCache.Remove(cacheKey);
}
}
// 提取所有三角形
var allTriangles = GeometryHelper.ExtractTriangles(new[] { railModel });
if (allTriangles.Count == 0)
{
LogManager.Warning($"[空轨] 模型 {railModel.DisplayName} 没有三角形数据");
return null;
}
LogManager.Info($"[空轨] 提取到 {allTriangles.Count} 个三角形");
// 筛选下表面三角形
var bottomTriangles = FilterBottomTriangles(allTriangles);
LogManager.Info($"[空轨] 筛选出 {bottomTriangles.Count} 个下表面三角形");
if (bottomTriangles.Count == 0)
{
LogManager.Warning($"[空轨] 没有找到下表面三角形");
return null;
}
// 计算包围盒
var bounds = CalculateBoundingBox(bottomTriangles);
// 创建下表面信息
var surfaceInfo = new RailBottomSurfaceInfo
{
BottomTriangles = bottomTriangles,
Bounds = bounds,
MinZ = bounds.Min.Z,
MaxZ = bounds.Max.Z,
CacheTime = DateTime.Now
};
// 加入缓存
_railCache[cacheKey] = surfaceInfo;
return surfaceInfo;
}
catch (Exception ex)
{
LogManager.Error($"[空轨] 提取下表面信息失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 筛选下表面三角形法线向下Z < 0
/// </summary>
@ -1018,222 +857,6 @@ namespace NavisworksTransport.PathPlanning
return normal;
}
/// <summary>
/// 从下表面三角形提取骨架线(支持拐弯)
/// </summary>
private static List<Point3D> ExtractSkeletonFromTriangles(List<Triangle3D> bottomTriangles)
{
try
{
// 1. 计算每个三角形的中心点
var triangleCenters = new List<Point3D>();
foreach (var triangle in bottomTriangles)
{
var center = new Point3D(
(triangle.Point1.X + triangle.Point2.X + triangle.Point3.X) / 3,
(triangle.Point1.Y + triangle.Point2.Y + triangle.Point3.Y) / 3,
(triangle.Point1.Z + triangle.Point2.Z + triangle.Point3.Z) / 3
);
triangleCenters.Add(center);
}
LogManager.Info($"[空轨] 计算了 {triangleCenters.Count} 个三角形中心点");
// 2. 按邻近度排序,形成路径
var sortedPoints = SortPointsByProximity(triangleCenters);
LogManager.Info($"[空轨] 排序后得到 {sortedPoints.Count} 个骨架点");
// 2.5 使用全局PCA压缩对称点适用于直线路径
var compressedPath = CollapseSymmetricPointsByPCA(sortedPoints);
LogManager.Info($"[空轨] PCA压缩后得到 {compressedPath.Count} 个骨架点");
// 3. 简化路径(移除过近的点)
var simplifiedPath = SimplifyPath(compressedPath, 0.1); // 0.1米阈值
LogManager.Info($"[空轨] 简化后得到 {simplifiedPath.Count} 个骨架点");
// 4. 平滑路径(可选)
var smoothedPath = SmoothPath(simplifiedPath);
LogManager.Info($"[空轨] 平滑后得到 {smoothedPath.Count} 个骨架点");
return smoothedPath;
}
catch (Exception ex)
{
LogManager.Error($"[空轨] 提取骨架线失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 按邻近度排序点(形成路径)
/// </summary>
private static List<Point3D> SortPointsByProximity(List<Point3D> points)
{
if (points.Count == 0) return new List<Point3D>();
var sorted = new List<Point3D>();
var remaining = new List<Point3D>(points);
// 从边界点开始选择X最小的点
var current = remaining.OrderBy(p => p.X).First();
sorted.Add(current);
remaining.Remove(current);
// 依次查找最近的点
while (remaining.Count > 0)
{
Point3D nearest = null;
double minDistance = double.MaxValue;
foreach (var point in remaining)
{
double distance = CalculateDistance2D(current, point);
if (distance < minDistance)
{
minDistance = distance;
nearest = point;
}
}
if (nearest != null && minDistance < 10.0) // 10米阈值避免跳跃
{
sorted.Add(nearest);
remaining.Remove(nearest);
current = nearest;
}
else
{
// 没有找到合适的邻近点,结束
break;
}
}
return sorted;
}
/// <summary>
/// 使用全局PCA压缩对称点适用于直线路径
/// </summary>
private static List<Point3D> CollapseSymmetricPointsByPCA(List<Point3D> points)
{
try
{
if (points.Count < 3)
{
return new List<Point3D>(points);
}
LogManager.Info("[空轨] 开始PCA压缩对称点");
// 1. 计算质心
var centroid = new Point3D(
points.Average(p => p.X),
points.Average(p => p.Y),
points.Average(p => p.Z)
);
// 2. 计算协方差矩阵
double covXX = 0, covYY = 0, covZZ = 0;
double covXY = 0, covXZ = 0, covYZ = 0;
foreach (var point in points)
{
double dx = point.X - centroid.X;
double dy = point.Y - centroid.Y;
double dz = point.Z - centroid.Z;
covXX += dx * dx;
covYY += dy * dy;
covZZ += dz * dz;
covXY += dx * dy;
covXZ += dx * dz;
covYZ += dy * dz;
}
covXX /= points.Count;
covYY /= points.Count;
covZZ /= points.Count;
covXY /= points.Count;
covXZ /= points.Count;
covYZ /= points.Count;
// 3. 计算特征值和特征向量3x3对称矩阵
// 使用数值方法计算特征值
var eigenvalues = new double[3];
var eigenvectors = new Point3D[3];
ComputeEigen3x3(covXX, covYY, covZZ, covXY, covXZ, covYZ, eigenvalues, eigenvectors);
// 4. 找到最小特征值对应的特征向量(压缩方向)
int minIndex = 0;
if (eigenvalues[1] < eigenvalues[minIndex]) minIndex = 1;
if (eigenvalues[2] < eigenvalues[minIndex]) minIndex = 2;
var compressAxis = eigenvectors[minIndex];
LogManager.Info($"[空轨] PCA特征值: {eigenvalues[0]:F4}, {eigenvalues[1]:F4}, {eigenvalues[2]:F4}");
LogManager.Info($"[空轨] 压缩方向: ({compressAxis.X:F3}, {compressAxis.Y:F3}, {compressAxis.Z:F3})");
// 5. 沿路径方向分段
// 找到最大特征值对应的特征向量(路径方向)
int maxIndex = 0;
if (eigenvalues[1] > eigenvalues[maxIndex]) maxIndex = 1;
if (eigenvalues[2] > eigenvalues[maxIndex]) maxIndex = 2;
var pathAxis = eigenvectors[maxIndex];
// 将点投影到路径方向
var projected = points.Select(p => new
{
Point = p,
Position = Dot(new Point3D((p - centroid).X, (p - centroid).Y, (p - centroid).Z), pathAxis)
}).OrderBy(x => x.Position).ToList();
// 6. 分段压缩
var segmentLength = 0.5; // 0.5米分段
var segments = new List<List<Point3D>>();
var currentSegment = new List<Point3D>();
double currentStart = projected[0].Position;
foreach (var item in projected)
{
if (item.Position - currentStart < segmentLength)
{
currentSegment.Add(item.Point);
}
else
{
segments.Add(currentSegment);
currentSegment = new List<Point3D> { item.Point };
currentStart = item.Position;
}
}
if (currentSegment.Count > 0)
segments.Add(currentSegment);
// 7. 每段压缩到中心
var compressed = new List<Point3D>();
foreach (var segment in segments)
{
var segmentCentroid = new Point3D(
segment.Average(p => p.X),
segment.Average(p => p.Y),
segment.Average(p => p.Z)
);
compressed.Add(segmentCentroid);
}
LogManager.Info($"[空轨] PCA压缩完成: {points.Count}个点 -> {compressed.Count}个点");
return compressed;
}
catch (Exception ex)
{
LogManager.Warning($"[空轨] PCA压缩失败: {ex.Message},返回原始点");
return new List<Point3D>(points);
}
}
/// <summary>
/// 计算3x3对称矩阵的特征值和特征向量
/// </summary>
@ -1327,113 +950,6 @@ namespace NavisworksTransport.PathPlanning
eigenvectors[2] = new Point3D(V[0, 2], V[1, 2], V[2, 2]);
}
/// <summary>
/// 点积
/// </summary>
private static double Dot(Point3D a, Point3D b)
{
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
/// <summary>
/// 点积Vector3D版本
/// </summary>
private static double Dot(Vector3D a, Point3D b)
{
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
/// <summary>
/// 简化路径(移除过近的点)
/// </summary>
private static List<Point3D> SimplifyPath(List<Point3D> path, double threshold)
{
if (path.Count <= 2) return new List<Point3D>(path);
var simplified = new List<Point3D> { path[0] };
for (int i = 1; i < path.Count - 1; i++)
{
double distanceToPrev = CalculateDistance2D(path[i], simplified.Last());
double distanceToNext = CalculateDistance2D(path[i], path[i + 1]);
if (distanceToPrev >= threshold || distanceToNext >= threshold)
{
simplified.Add(path[i]);
}
}
simplified.Add(path.Last());
return simplified;
}
/// <summary>
/// 平滑路径(简单移动平均)
/// </summary>
private static List<Point3D> SmoothPath(List<Point3D> path)
{
if (path.Count <= 2) return new List<Point3D>(path);
var smoothed = new List<Point3D>();
// 第一个点保持不变
smoothed.Add(path[0]);
// 中间点使用移动平均
for (int i = 1; i < path.Count - 1; i++)
{
var prev = path[i - 1];
var curr = path[i];
var next = path[i + 1];
var smoothedPoint = new Point3D(
(prev.X + curr.X + next.X) / 3,
(prev.Y + curr.Y + next.Y) / 3,
(prev.Z + curr.Z + next.Z) / 3
);
smoothed.Add(smoothedPoint);
}
// 最后一个点保持不变
smoothed.Add(path.Last());
return smoothed;
}
/// <summary>
/// 沿骨架线生成采样点
/// </summary>
private static List<Point3D> GenerateSamplePointsAlongSkeleton(
List<Point3D> skeleton,
double samplingInterval)
{
if (skeleton.Count < 2)
{
throw new ArgumentException("骨架线至少需要2个点");
}
var samplePoints = new List<Point3D>();
// 计算骨架线总长度
double totalLength = CalculatePathLength(skeleton);
// 计算采样点数
int sampleCount = Math.Max(2, (int)Math.Ceiling(totalLength / samplingInterval));
LogManager.Info($"[空轨] 骨架线总长度: {totalLength:F2}, 采样点数: {sampleCount}");
// 沿骨架线均匀采样
for (int i = 0; i < sampleCount; i++)
{
double progress = (double)i / (sampleCount - 1);
var point = InterpolateOnPath(skeleton, progress);
samplePoints.Add(point);
}
return samplePoints;
}
/// <summary>
/// 在路径上插值(支持拐弯)
/// </summary>
@ -1554,35 +1070,5 @@ namespace NavisworksTransport.PathPlanning
return double.MinValue;
}
}
/// <summary>
/// 计算包围盒复用Utils中的方法
/// </summary>
private static BoundingBox3D CalculateBoundingBox(List<Triangle3D> triangles)
{
if (triangles.Count == 0)
{
return new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0));
}
double minX = double.MaxValue, minY = double.MaxValue, minZ = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue, maxZ = double.MinValue;
foreach (var triangle in triangles)
{
minX = Math.Min(minX, Math.Min(triangle.Point1.X, Math.Min(triangle.Point2.X, triangle.Point3.X)));
minY = Math.Min(minY, Math.Min(triangle.Point1.Y, Math.Min(triangle.Point2.Y, triangle.Point3.Y)));
minZ = Math.Min(minZ, Math.Min(triangle.Point1.Z, Math.Min(triangle.Point2.Z, triangle.Point3.Z)));
maxX = Math.Max(maxX, Math.Max(triangle.Point1.X, Math.Max(triangle.Point2.X, triangle.Point3.X)));
maxY = Math.Max(maxY, Math.Max(triangle.Point1.Y, Math.Max(triangle.Point2.Y, triangle.Point3.Y)));
maxZ = Math.Max(maxZ, Math.Max(triangle.Point1.Z, Math.Max(triangle.Point2.Z, triangle.Point3.Z)));
}
return new BoundingBox3D(
new Point3D(minX, minY, minZ),
new Point3D(maxX, maxY, maxZ));
}
}
}
}

View File

@ -526,8 +526,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
try
{
// 2. 纯业务逻辑执行后台线程不使用UIStateManager
var result = await Task.Run(() =>
// 2. 业务逻辑执行(必须在主线程上执行,因为包含 Navisworks COM API 调用
var result = NavisworksApiHelper.ExecuteOnUIThread(() =>
{
try
{