diff --git a/IFLOW.md b/IFLOW.md index b0a3aca..6616c24 100644 --- a/IFLOW.md +++ b/IFLOW.md @@ -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()` 进行转换 diff --git a/src/Core/Properties/CategoryAttributeManager.cs b/src/Core/Properties/CategoryAttributeManager.cs index d810de3..ede8491 100644 --- a/src/Core/Properties/CategoryAttributeManager.cs +++ b/src/Core/Properties/CategoryAttributeManager.cs @@ -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) { diff --git a/src/PathPlanning/RailGeometryHelper.cs b/src/PathPlanning/RailGeometryHelper.cs index 3c9eb22..24dc992 100644 --- a/src/PathPlanning/RailGeometryHelper.cs +++ b/src/PathPlanning/RailGeometryHelper.cs @@ -407,6 +407,12 @@ namespace NavisworksTransport.PathPlanning /// 成功提取的基准路径数量 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; } } - - /// - /// 兼容方法:从空轨几何体提取下表面中心线(一步完成) - /// - /// 空轨模型项 - /// 起点(3D坐标) - /// 终点(3D坐标) - /// 采样间隔(模型单位),默认0.5 - /// 路径点列表(包含精确的Z坐标) /// /// 使用 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; } - /// - /// 兼容方法:从空轨几何体提取下表面中心线(一步完成) - /// - /// 空轨模型项 - /// 起点(3D坐标) - /// 终点(3D坐标) - /// 采样间隔(模型单位),默认0.5 - /// 路径点列表(包含精确的Z坐标) - public static List 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(); - 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; - } - } - - /// - /// 提取空轨下表面信息(带缓存) - /// - 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; - } - } - /// /// 筛选下表面三角形(法线向下,Z < 0) /// @@ -1018,222 +857,6 @@ namespace NavisworksTransport.PathPlanning return normal; } - /// - /// 从下表面三角形提取骨架线(支持拐弯) - /// - private static List ExtractSkeletonFromTriangles(List bottomTriangles) - { - try - { - // 1. 计算每个三角形的中心点 - var triangleCenters = new List(); - 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; - } - } - - /// - /// 按邻近度排序点(形成路径) - /// - private static List SortPointsByProximity(List points) - { - if (points.Count == 0) return new List(); - - var sorted = new List(); - var remaining = new List(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; - } - - /// - /// 使用全局PCA压缩对称点(适用于直线路径) - /// - private static List CollapseSymmetricPointsByPCA(List points) - { - try - { - if (points.Count < 3) - { - return new List(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>(); - var currentSegment = new List(); - 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 { item.Point }; - currentStart = item.Position; - } - } - if (currentSegment.Count > 0) - segments.Add(currentSegment); - - // 7. 每段压缩到中心 - var compressed = new List(); - 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(points); - } - } - /// /// 计算3x3对称矩阵的特征值和特征向量 /// @@ -1327,113 +950,6 @@ namespace NavisworksTransport.PathPlanning eigenvectors[2] = new Point3D(V[0, 2], V[1, 2], V[2, 2]); } - /// - /// 点积 - /// - private static double Dot(Point3D a, Point3D b) - { - return a.X * b.X + a.Y * b.Y + a.Z * b.Z; - } - - /// - /// 点积(Vector3D版本) - /// - private static double Dot(Vector3D a, Point3D b) - { - return a.X * b.X + a.Y * b.Y + a.Z * b.Z; - } - - /// - /// 简化路径(移除过近的点) - /// - private static List SimplifyPath(List path, double threshold) - { - if (path.Count <= 2) return new List(path); - - var simplified = new List { 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; - } - - /// - /// 平滑路径(简单移动平均) - /// - private static List SmoothPath(List path) - { - if (path.Count <= 2) return new List(path); - - var smoothed = new List(); - - // 第一个点保持不变 - 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; - } - - /// - /// 沿骨架线生成采样点 - /// - private static List GenerateSamplePointsAlongSkeleton( - List skeleton, - double samplingInterval) - { - if (skeleton.Count < 2) - { - throw new ArgumentException("骨架线至少需要2个点"); - } - - var samplePoints = new List(); - - // 计算骨架线总长度 - 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; - } - /// /// 在路径上插值(支持拐弯) /// @@ -1554,35 +1070,5 @@ namespace NavisworksTransport.PathPlanning return double.MinValue; } } - - /// - /// 计算包围盒(复用Utils中的方法) - /// - private static BoundingBox3D CalculateBoundingBox(List 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)); - } - - } + } } \ No newline at end of file diff --git a/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs b/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs index 972667e..acc3161 100644 --- a/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs +++ b/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs @@ -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 {