From 3f0b42770d72dfed09cf3221570839a708fd1d72 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 19 Feb 2026 22:07:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E7=9A=84=E5=89=96=E9=9D=A2=E7=9B=92=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TransportPlugin.csproj | 1 + doc/requirement/todo_features.md | 6 +- .../ViewModels/LogisticsControlViewModel.cs | 120 +++ .../WPF/ViewModels/ModelSettingsViewModel.cs | 2 +- src/UI/WPF/Views/LogisticsControlPanel.xaml | 20 + src/Utils/SectionClipHelper.cs | 683 ++++++++++++++++++ 6 files changed, 828 insertions(+), 4 deletions(-) create mode 100644 src/Utils/SectionClipHelper.cs diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index a901fde..6ffd61f 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -333,6 +333,7 @@ + diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md index 6fe07c5..35981b8 100644 --- a/doc/requirement/todo_features.md +++ b/doc/requirement/todo_features.md @@ -77,10 +77,10 @@ 1. [x] (功能)对路径上的各点进行坐标编辑 2. [ ] (功能)记录并查看路径文件操作的历史记录 3. [ ] (功能)自动隐藏或淡化非关键层,以便专注于物流路径相关的层级。 -4. [ ] (优化)优化路径时间标签功能 +4. [x] (优化)优化路径时间标签功能 5. [ ] (测试)路径规划文件能导入DELMIA -6. [ ] (优化)优化路径规划分析和分析报告 -7. [ ] (功能)增加物流属性自定义 +6. [x] (优化)优化路径规划分析和分析报告 +7. [x] (功能)增加物流属性自定义 8. [x] (BUG) 动画时物流车在起点时应该朝向路径方向,切换虚拟物体和指定物体时,原有的要归位 9. [x] (功能)物流车在路径点转弯时,设置转弯半径等参数,将路径变成曲线 diff --git a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs index 79a6a36..2029b28 100644 --- a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs +++ b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using Autodesk.Navisworks.Api; using NavisApplication = Autodesk.Navisworks.Api.Application; using NavisworksTransport.Core; +using NavisworksTransport.Core.Config; +using NavisworksTransport.Utils; namespace NavisworksTransport.UI.WPF.ViewModels { @@ -139,6 +143,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private bool _isClipBoxEnabled; + + /// + /// 是否启用剖面盒(聚焦当前路径) + /// + public bool IsClipBoxEnabled + { + get => _isClipBoxEnabled; + set + { + if (SetProperty(ref _isClipBoxEnabled, value)) + { + if (value) + { + EnableClipBoxForCurrentRoute(); + } + else + { + SectionClipHelper.ClearClipBox(); + LogManager.Info("[剖面盒] 已关闭"); + } + } + } + } + /// /// 从 PathPointRenderPlugin 同步渲染状态 /// @@ -284,5 +313,96 @@ namespace NavisworksTransport.UI.WPF.ViewModels } #endregion + + #region 剖面盒功能 + + /// + /// 启用剖面盒并聚焦到当前路径 + /// + private void EnableClipBoxForCurrentRoute() + { + try + { + var currentRoute = _pathPlanningManager?.CurrentRoute; + if (currentRoute?.Points == null || currentRoute.Points.Count == 0) + { + LogManager.Warning("[剖面盒] 当前没有路径,无法设置剖面盒"); + IsClipBoxEnabled = false; + return; + } + + // 获取虚拟物体默认高度(米) + double objectHeightInMeters = ConfigManager.Instance.Current.PathEditing.ObjectHeightMeters; + + // ========== 第1步:处理路径,计算包含虚拟物体高度的包围盒 ========== + + // 转换路径点,并根据路径类型调整Z值范围(包含虚拟物体高度) + var adjustedPoints = currentRoute.Points.Select(p => new Point3D(p.X, p.Y, p.Z)).ToList(); + + // 根据路径类型,添加包含虚拟物体高度的额外点 + if (currentRoute.PathType == PathType.Ground) + { + // 地面路径:在路径点上方添加物体高度点 + foreach (var p in currentRoute.Points) + { + adjustedPoints.Add(new Point3D(p.X, p.Y, p.Z + objectHeightInMeters)); + } + } + else + { + // 空中路径:在路径点下方添加物体高度点 + foreach (var p in currentRoute.Points) + { + adjustedPoints.Add(new Point3D(p.X, p.Y, p.Z - objectHeightInMeters)); + } + } + + // ========== 第2步:统一扩展(水平4米,高度2米)========== + + bool success = SectionClipHelper.SetClipBoxByPath(adjustedPoints, + marginInMeters: 4.0, // 水平方向扩展4米 + heightMarginInMeters: 2.0 // 高度方向上下各扩展2米 + ); + + if (success) + { + LogManager.Info($"[剖面盒] 已启用,路径点数量: {currentRoute.Points.Count}, 各方向扩展: 2m"); + } + else + { + LogManager.Warning("[剖面盒] 设置失败"); + IsClipBoxEnabled = false; + } + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒] 启用失败: {ex.Message}"); + IsClipBoxEnabled = false; + } + } + + /// + /// 计算点列表的包围盒 + /// + private (Point3D Center, double SizeX, double SizeY, double SizeZ) CalculatePointsBoundingBox(List points) + { + double minX = double.MaxValue, minY = double.MaxValue, minZ = double.MaxValue; + double maxX = double.MinValue, maxY = double.MinValue, maxZ = double.MinValue; + + foreach (var p in points) + { + minX = Math.Min(minX, p.X); + minY = Math.Min(minY, p.Y); + minZ = Math.Min(minZ, p.Z); + maxX = Math.Max(maxX, p.X); + maxY = Math.Max(maxY, p.Y); + maxZ = Math.Max(maxZ, p.Z); + } + + var center = new Point3D((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2); + return (center, maxX - minX, maxY - minY, maxZ - minZ); + } + + #endregion } } \ No newline at end of file diff --git a/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs b/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs index 7bb17b4..a21cb70 100644 --- a/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs +++ b/src/UI/WPF/ViewModels/ModelSettingsViewModel.cs @@ -1524,7 +1524,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels try { - // 聚焦到模型(斜上方45度视角) + // 聚焦到模型(斜上方60度视角) ViewpointHelper.FocusOnModelItem(logisticsModel.NavisworksItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25); UpdateMainStatus($"已聚焦到物流模型: {logisticsModel.Name}"); diff --git a/src/UI/WPF/Views/LogisticsControlPanel.xaml b/src/UI/WPF/Views/LogisticsControlPanel.xaml index c9b1539..90ac1a5 100644 --- a/src/UI/WPF/Views/LogisticsControlPanel.xaml +++ b/src/UI/WPF/Views/LogisticsControlPanel.xaml @@ -139,6 +139,26 @@ NavisworksTransport 主控制面板 - 采用与其他视图一致的Navisworks 2 + + + + + + + + diff --git a/src/Utils/SectionClipHelper.cs b/src/Utils/SectionClipHelper.cs new file mode 100644 index 0000000..e74f68c --- /dev/null +++ b/src/Utils/SectionClipHelper.cs @@ -0,0 +1,683 @@ +using System; +using System.Collections.Generic; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils +{ + /// + /// 剖面盒辅助类 - 用于优化碰撞检测性能 + /// 通过设置视口剖面盒,只处理路径周围的对象 + /// + /// 注意:Navisworks 的 ClipPlaneSet API 需要使用 JSON 字符串方式设置 + /// 直接修改 ClipPlaneSet 对象的属性在手工打开剖面功能后会导致 "Object is Read-Only" 错误 + /// + public static class SectionClipHelper + { + // 默认边距(米):路径周围保留的空间 + private const double DEFAULT_MARGIN_METERS = 2.0; + + // 默认高度范围(米):上下各延伸的高度 + private const double DEFAULT_HEIGHT_MARGIN_METERS = 1.0; + + /// + /// 根据路径点列表设置剖面盒 + /// + /// 路径点列表(模型单位) + /// 水平边距(米) + /// 高度边距(米,上下各延伸) + /// 是否成功设置 + public static bool SetClipBoxByPath(List pathPoints, + double marginInMeters = DEFAULT_MARGIN_METERS, + double heightMarginInMeters = DEFAULT_HEIGHT_MARGIN_METERS) + { + try + { + if (pathPoints == null || pathPoints.Count == 0) + { + LogManager.Warning("[剖面盒] 路径点为空,无法设置剖面盒"); + return false; + } + + // 米转换为模型单位 + double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units); + double margin = marginInMeters * metersToUnits; + double heightMargin = heightMarginInMeters * metersToUnits; + + // 计算路径的包围盒 + var pathBounds = CalculatePathBoundingBox(pathPoints); + + // 扩展边距(使用模型单位) + var clipBox = ExpandBoundingBox(pathBounds, margin, heightMargin); + + // 应用到视口 + ApplyClipBox(clipBox); + + LogManager.Info($"[剖面盒] 已设置 - 水平边距: {marginInMeters}m, 高度边距: {heightMarginInMeters}m, " + + $"范围: X[{clipBox.Min.X:F2}, {clipBox.Max.X:F2}], Y[{clipBox.Min.Y:F2}, {clipBox.Max.Y:F2}], Z[{clipBox.Min.Z:F2}, {clipBox.Max.Z:F2}]"); + + return true; + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒] 设置失败: {ex.Message}"); + return false; + } + } + + /// + /// 根据单个点设置剖面盒(用于吊装路径等单点场景) + /// + /// 中心点 + /// 范围(米) + /// 高度范围(米) + /// 是否成功设置 + public static bool SetClipBoxByPoint(Point3D centerPoint, + double rangeMeters = 10.0, + double heightRangeMeters = 5.0) + { + try + { + var min = new Point3D( + centerPoint.X - rangeMeters, + centerPoint.Y - rangeMeters, + centerPoint.Z - heightRangeMeters); + var max = new Point3D( + centerPoint.X + rangeMeters, + centerPoint.Y + rangeMeters, + centerPoint.Z + heightRangeMeters); + + var clipBox = new BoundingBox3D(min, max); + ApplyClipBox(clipBox); + + LogManager.Info($"[剖面盒] 已设置 - 中心: ({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " + + $"范围: {rangeMeters}m x {rangeMeters}m x {heightRangeMeters}m"); + + return true; + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒] 设置失败: {ex.Message}"); + return false; + } + } + + /// + /// 根据楼层设置剖面盒 + /// + /// 楼层模型项 + /// 边距(米) + /// 是否成功设置 + public static bool SetClipBoxByFloor(ModelItem floorItem, double marginMeters = 1.0) + { + try + { + if (floorItem == null) + { + LogManager.Warning("[剖面盒] 楼层对象为空"); + return false; + } + + BoundingBox3D floorBounds = floorItem.BoundingBox(); + BoundingBox3D clipBox = ExpandBoundingBox(floorBounds, marginMeters, marginMeters); + ApplyClipBox(clipBox); + + LogManager.Info($"[剖面盒] 已设置到楼层: {floorItem.DisplayName}"); + return true; + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒] 设置失败: {ex.Message}"); + return false; + } + } + + /// + /// 清除剖面盒(显示全部模型) + /// 使用 JSON 字符串方式设置 Enabled = false + /// + public static void ClearClipBox() + { + try + { + // 构建禁用剖面的 JSON + string json = BuildClipPlaneSetJson( + new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0)), + false); + + var view = Application.ActiveDocument.ActiveView; + view.TrySetClippingPlanes(json); + + LogManager.Info("[剖面盒] 已清除"); + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒] 清除失败: {ex.Message}"); + } + } + + /// + /// 检查剖面盒是否已启用 + /// + public static bool IsClipBoxEnabled + { + get + { + try + { + return Application.ActiveDocument.CurrentViewpoint.Value.ClipPlanes.Enabled; + } + catch + { + return false; + } + } + } + + /// + /// 获取当前剖面盒 + /// + /// 输出剖面盒 + /// 是否成功获取 + public static bool TryGetCurrentClipBox(out BoundingBox3D clipBox) + { + clipBox = new BoundingBox3D(); + try + { + var clipPlanes = Application.ActiveDocument.CurrentViewpoint.Value.ClipPlanes; + if (!clipPlanes.Enabled || clipPlanes.Mode != ClipPlaneSetMode.Box) + return false; + + clipBox = clipPlanes.Box; + return true; + } + catch + { + return false; + } + } + + /// + /// 测试点是否在剖面盒内 + /// + public static bool IsPointInClipBox(Point3D point) + { + if (!TryGetCurrentClipBox(out var clipBox)) return true; // 无剖面盒时默认全部包含 + + return clipBox.Contains(point); + } + + /// + /// 测试包围盒是否与剖面盒相交 + /// + public static bool IntersectsClipBox(BoundingBox3D box) + { + if (!TryGetCurrentClipBox(out var clipBox)) return true; // 无剖面盒时默认全部相交 + + return clipBox.Intersects(box); + } + + /// + /// 统计剖面盒内/外的对象数量(用于测试) + /// + /// 总对象数 + /// 在剖面盒内的对象数 + /// 在剖面盒外的对象数 + public static void CountObjectsInClipBox(out int totalCount, out int insideCount, out int outsideCount) + { + totalCount = 0; + insideCount = 0; + outsideCount = 0; + + try + { + if (!TryGetCurrentClipBox(out var clipBox)) + { + LogManager.Warning("[剖面盒统计] 剖面盒未启用"); + return; + } + + var stack = new Stack(); + foreach (var model in Application.ActiveDocument.Models) + { + stack.Push(model.RootItem); + } + + while (stack.Count > 0) + { + var item = stack.Pop(); + totalCount++; + + // 测试包围盒是否相交 + BoundingBox3D itemBox = item.BoundingBox(); + if (clipBox.Intersects(itemBox)) + { + insideCount++; + } + else + { + outsideCount++; + continue; // 跳过子节点(剪枝) + } + + // 继续遍历子节点 + foreach (var child in item.Children) + { + stack.Push(child); + } + } + + LogManager.Info($"[剖面盒统计] 总数: {totalCount}, 盒内: {insideCount}, 盒外: {outsideCount}, " + + $"过滤率: {(outsideCount * 100.0 / totalCount):F1}%"); + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒统计] 失败: {ex.Message}"); + } + } + + /// + /// 使用采样点(8个角点)检测对象是否真正与剖面盒相交 + /// 用于排除大包围盒但实际几何体远离剖面盒的对象(如地基) + /// + public static bool IntersectsByCornerPoints(BoundingBox3D itemBox, BoundingBox3D clipBox) + { + // 获取对象包围盒的8个角点 + var corners = new Point3D[] + { + new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Min.Z), + new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Max.Z), + new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Min.Z), + new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Max.Z), + new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Min.Z), + new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Max.Z), + new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Min.Z), + new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Max.Z) + }; + + // 检查是否有任何角点在剖面盒内 + foreach (var corner in corners) + { + if (clipBox.Contains(corner)) + { + return true; + } + } + + return false; + } + + /// + /// 使用棱上自适应采样检测对象是否与剖面盒相交 + /// 根据物体大小和剖面盒尺寸动态计算采样点数,确保长物体(如管道)不会漏检 + /// 采样间距小于剖面盒最小边长,确保任何穿过剖面盒的物体都能被检测到 + /// + /// 对象包围盒 + /// 剖面盒 + /// 每条棱的最小采样点数(默认3个) + /// 是否有采样点在剖面盒内 + public static bool IntersectsByEdgeSampling(BoundingBox3D itemBox, BoundingBox3D clipBox, int minSamplesPerEdge = 3) + { + // 计算剖面盒的最小边长(作为采样间距上限) + double clipBoxMinSize = Math.Min( + Math.Min(clipBox.Max.X - clipBox.Min.X, clipBox.Max.Y - clipBox.Min.Y), + clipBox.Max.Z - clipBox.Min.Z + ); + + // 计算物体包围盒的三边长度 + double itemSizeX = itemBox.Max.X - itemBox.Min.X; + double itemSizeY = itemBox.Max.Y - itemBox.Min.Y; + double itemSizeZ = itemBox.Max.Z - itemBox.Min.Z; + + // 根据边长动态计算每条棱的采样点数 + // 采样间距 = 边长 / (采样点数 - 1),确保间距 < 剖面盒最小边长 + int GetSampleCount(double edgeLength) + { + if (edgeLength <= 0) return minSamplesPerEdge; + // 需要的采样点数 = 边长 / 剖面盒尺寸 + 1,至少minSamplesPerEdge个 + int count = (int)Math.Ceiling(edgeLength / clipBoxMinSize) + 1; + return Math.Max(count, minSamplesPerEdge); + } + + int samplesX = GetSampleCount(itemSizeX); + int samplesY = GetSampleCount(itemSizeY); + int samplesZ = GetSampleCount(itemSizeZ); + + // 12条棱的端点定义(索引对应8个角点)和采样点数 + // 0: (0,0,0) 1: (0,0,1) 2: (0,1,0) 3: (0,1,1) + // 4: (1,0,0) 5: (1,0,1) 6: (1,1,0) 7: (1,1,1) + var edges = new (int start, int end, int samples)[] + { + // X方向棱(4条),用samplesX + (0, 4, samplesX), (1, 5, samplesX), (2, 6, samplesX), (3, 7, samplesX), + // Y方向棱(4条),用samplesY + (0, 2, samplesY), (1, 3, samplesY), (4, 6, samplesY), (5, 7, samplesY), + // Z方向棱(4条),用samplesZ + (0, 1, samplesZ), (2, 3, samplesZ), (4, 5, samplesZ), (6, 7, samplesZ) + }; + + // 8个角点的坐标 + var cornerPoints = new Point3D[] + { + new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Min.Z), + new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Max.Z), + new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Min.Z), + new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Max.Z), + new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Min.Z), + new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Max.Z), + new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Min.Z), + new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Max.Z) + }; + + // 在每条棱上采样 + foreach (var edge in edges) + { + var start = cornerPoints[edge.start]; + var end = cornerPoints[edge.end]; + int samples = edge.samples; + + for (int i = 0; i < samples; i++) + { + double t = (double)i / (samples - 1); // 0.0 到 1.0 + var samplePoint = new Point3D( + start.X + t * (end.X - start.X), + start.Y + t * (end.Y - start.Y), + start.Z + t * (end.Z - start.Z) + ); + + if (clipBox.Contains(samplePoint)) + { + return true; + } + } + } + + return false; + } + + /// + /// 统计对象数量(使用采样点检测) + /// 与 CountObjectsInClipBox 的区别:使用8个角点检测,排除大包围盒假阳性 + /// + public static void CountObjectsInClipBoxByCorners(out int totalCount, out int insideCount, out int outsideCount, out int largeBoxFiltered, List debugDetails = null) + { + totalCount = 0; + insideCount = 0; + outsideCount = 0; + largeBoxFiltered = 0; + + try + { + if (!TryGetCurrentClipBox(out var clipBox)) + { + LogManager.Warning("[剖面盒角点统计] 剖面盒未启用"); + return; + } + + var stack = new Stack(); + foreach (var model in Application.ActiveDocument.Models) + { + stack.Push(model.RootItem); + } + + while (stack.Count > 0) + { + var item = stack.Pop(); + totalCount++; + + BoundingBox3D itemBox = item.BoundingBox(); + + // 先检查包围盒是否相交(快速排除) + if (!clipBox.Intersects(itemBox)) + { + outsideCount++; + continue; // 跳过子节点(剪枝) + } + + // 相交:再用角点检测确认 + bool hasCornerInside = IntersectsByCornerPoints(itemBox, clipBox); + + if (hasCornerInside) + { + insideCount++; + } + else + { + outsideCount++; + largeBoxFiltered++; + + // 记录被过滤的大对象 + if (item.HasGeometry) + { + LogManager.Debug($"[剖面盒角点过滤] 大包围盒但无角点在盒内: {item.DisplayName}, 包围盒: X[{itemBox.Min.X:F2},{itemBox.Max.X:F2}], Y[{itemBox.Min.Y:F2},{itemBox.Max.Y:F2}], Z[{itemBox.Min.Z:F2},{itemBox.Max.Z:F2}]"); + } + + // 注意:这里继续遍历子节点,因为父对象可能只是外壳,子对象可能在盒内 + } + + // 继续遍历子节点 + foreach (var child in item.Children) + { + stack.Push(child); + } + } + + LogManager.Info($"[剖面盒角点统计] 总数: {totalCount}, 盒内(角点): {insideCount}, 盒外: {outsideCount}, 大包围盒过滤: {largeBoxFiltered}, " + + $"过滤率: {(outsideCount * 100.0 / totalCount):F1}%"); + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒角点统计] 失败: {ex.Message}"); + } + } + + /// + /// 测试包围盒相交检测(用于验证) + /// + public static void TestIntersection() + { + try + { + if (!TryGetCurrentClipBox(out var clipBox)) + { + LogManager.Warning("[剖面盒测试] 请先设置剖面盒"); + return; + } + + // 测试点 + var center = clipBox.Center; + var testPointInside = new Point3D(center.X, center.Y, center.Z); + var testPointOutside = new Point3D( + center.X + 1000, + center.Y + 1000, + center.Z + 1000); + + bool inside1 = IsPointInClipBox(testPointInside); + bool inside2 = IsPointInClipBox(testPointOutside); + + LogManager.Info($"[剖面盒测试] 中心点: {inside1} (应为 True)"); + LogManager.Info($"[剖面盒测试] 远点: {inside2} (应为 False)"); + + // 测试包围盒 + var boxInside = new BoundingBox3D( + new Point3D(center.X - 1, center.Y - 1, center.Z - 1), + new Point3D(center.X + 1, center.Y + 1, center.Z + 1)); + var boxOutside = new BoundingBox3D( + new Point3D(center.X + 100, center.Y + 100, center.Z + 100), + new Point3D(center.X + 200, center.Y + 200, center.Z + 200)); + + bool intersect1 = IntersectsClipBox(boxInside); + bool intersect2 = IntersectsClipBox(boxOutside); + + LogManager.Info($"[剖面盒测试] 内部包围盒相交: {intersect1} (应为 True)"); + LogManager.Info($"[剖面盒测试] 外部包围盒相交: {intersect2} (应为 False)"); + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒测试] 失败: {ex.Message}"); + } + } + + #region 私有方法 + + /// + /// 计算路径点的包围盒 + /// + private static BoundingBox3D CalculatePathBoundingBox(List points) + { + if (points.Count == 1) + { + return new BoundingBox3D(points[0], points[0]); + } + + double minX = double.MaxValue, minY = double.MaxValue, minZ = double.MaxValue; + double maxX = double.MinValue, maxY = double.MinValue, maxZ = double.MinValue; + + foreach (var p in points) + { + minX = Math.Min(minX, p.X); + minY = Math.Min(minY, p.Y); + minZ = Math.Min(minZ, p.Z); + maxX = Math.Max(maxX, p.X); + maxY = Math.Max(maxY, p.Y); + maxZ = Math.Max(maxZ, p.Z); + } + + return new BoundingBox3D( + new Point3D(minX, minY, minZ), + new Point3D(maxX, maxY, maxZ)); + } + + /// + /// 扩展包围盒 + /// + private static BoundingBox3D ExpandBoundingBox(BoundingBox3D box, + double horizontalMargin, double verticalMargin) + { + return ExpandBoundingBox(box, horizontalMargin, verticalMargin, verticalMargin); + } + + /// + /// 扩展包围盒(支持分别设置上下边距) + /// + private static BoundingBox3D ExpandBoundingBox(BoundingBox3D box, + double horizontalMargin, double bottomMargin, double topMargin) + { + var min = new Point3D( + box.Min.X - horizontalMargin, + box.Min.Y - horizontalMargin, + box.Min.Z - bottomMargin); + var max = new Point3D( + box.Max.X + horizontalMargin, + box.Max.Y + horizontalMargin, + box.Max.Z + topMargin); + + return new BoundingBox3D(min, max); + } + + /// + /// 根据路径点列表设置剖面盒(支持分别设置上下边距) + /// + /// 路径点列表(模型单位) + /// 水平边距(米) + /// 底部高度边距(米) + /// 顶部高度边距(米) + public static bool SetClipBoxByPathWithMargins(List pathPoints, + double marginInMeters = DEFAULT_MARGIN_METERS, + double heightMarginBottomInMeters = DEFAULT_HEIGHT_MARGIN_METERS, + double heightMarginTopInMeters = DEFAULT_HEIGHT_MARGIN_METERS) + { + try + { + if (pathPoints == null || pathPoints.Count == 0) + { + LogManager.Warning("[剖面盒] 路径点为空,无法设置剖面盒"); + return false; + } + + // 米转换为模型单位 + double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units); + double margin = marginInMeters * metersToUnits; + double bottomMargin = heightMarginBottomInMeters * metersToUnits; + double topMargin = heightMarginTopInMeters * metersToUnits; + + // 计算路径的包围盒 + var pathBounds = CalculatePathBoundingBox(pathPoints); + + // 扩展边距(分别设置上下,使用模型单位) + var clipBox = ExpandBoundingBox(pathBounds, margin, bottomMargin, topMargin); + + // 应用到视口 + ApplyClipBox(clipBox); + + LogManager.Info($"[剖面盒] 已设置 - 水平边距: {marginInMeters}m, 底部边距: {heightMarginBottomInMeters}m, 顶部边距: {heightMarginTopInMeters}m, " + + $"范围: X[{clipBox.Min.X:F2}, {clipBox.Max.X:F2}], Y[{clipBox.Min.Y:F2}, {clipBox.Max.Y:F2}], Z[{clipBox.Min.Z:F2}, {clipBox.Max.Z:F2}]"); + + return true; + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒] 设置失败: {ex.Message}"); + return false; + } + } + + /// + /// 应用剖面盒到视口 + /// 使用 JSON 字符串方式设置,避免 "Object is Read-Only" 错误 + /// + private static void ApplyClipBox(BoundingBox3D box) + { + // 构建 JSON 格式的 ClipPlaneSet + string json = BuildClipPlaneSetJson(box, true); + + // 使用 View.SetClippingPlanes 方法设置 + var view = Application.ActiveDocument.ActiveView; + + // TrySetClippingPlanes 返回 bool 表示是否成功,不会抛出异常 + bool success = view.TrySetClippingPlanes(json); + + if (!success) + { + // 如果 Try 方法失败,尝试使用 SetClippingPlanes(会抛出异常) + view.SetClippingPlanes(json); + } + } + + /// + /// 构建 ClipPlaneSet 的 JSON 字符串 + /// + /// 包围盒 + /// 是否启用 + /// JSON 字符串 + private static string BuildClipPlaneSetJson(BoundingBox3D box, bool enabled) + { + // Navisworks ClipPlaneSet JSON 格式: + // { + // "Type": "ClipPlaneSet", + // "Version": 1, + // "OrientedBox": { + // "Type": "OrientedBox3D", + // "Version": 1, + // "Box": [[minX, minY, minZ], [maxX, maxY, maxZ]], + // "Rotation": [0, 0, 0] + // }, + // "Enabled": true/false + // } + + return string.Format( + "{{\"Type\":\"ClipPlaneSet\",\"Version\":1," + + "\"OrientedBox\":{{\"Type\":\"OrientedBox3D\",\"Version\":1," + + "\"Box\":[[{0},{1},{2}],[{3},{4},{5}]]," + + "\"Rotation\":[0,0,0]}},\"Enabled\":{6}}}", + box.Min.X.ToString("G17"), + box.Min.Y.ToString("G17"), + box.Min.Z.ToString("G17"), + box.Max.X.ToString("G17"), + box.Max.Y.ToString("G17"), + box.Max.Z.ToString("G17"), + enabled.ToString().ToLowerInvariant()); + } + + #endregion + } +}