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
+ }
+}