diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 9a35c0e..485740f 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -1963,7 +1963,8 @@ namespace NavisworksTransport.Core.Animation /// - /// 安全获取ModelItem的DisplayName,避免访问已释放对象的警告 + /// 安全获取ModelItem的显示名称,避免访问已释放对象的警告 + /// 优先使用ClassName,其次DisplayName /// /// ModelItem对象 /// 安全的显示名称 @@ -1976,8 +1977,9 @@ namespace NavisworksTransport.Core.Animation return "NULL"; } - // 尝试访问DisplayName,如果对象已被释放会抛出异常 - return item.DisplayName ?? "未命名对象"; + // 优先使用ClassName,其次DisplayName + // 某些几何体DisplayName为空但ClassName有值(如"Pipe", "Column"等) + return item.ClassName ?? item.DisplayName ?? "未命名对象"; } catch (System.ObjectDisposedException) { @@ -3952,17 +3954,31 @@ namespace NavisworksTransport.Core.Animation continue; // 跳过该节点及其子节点(剪枝) } - // 相交:如果是复合对象或有几何数据,加入列表 + // 相交:处理对象 + // 🔥 策略:复合对象作为整体添加(有名字),不遍历子节点 + // 只有独立几何对象(无复合父级)才添加 if (item.IsComposite) { - objectsInClipBox.Add(item); - intersectCount++; - // 复合对象不遍历子节点 + // 复合对象:用棱上采样精筛,排除地基等(自适应采样) + bool edgeIntersects = SectionClipHelper.IntersectsByEdgeSampling(itemBox, clipBox, minSamplesPerEdge: 3); + + if (edgeIntersects) + { + objectsInClipBox.Add(item); + intersectCount++; + } + // 复合对象不遍历子节点(已作为整体添加) } else if (item.HasGeometry) { - objectsInClipBox.Add(item); - intersectCount++; + // 独立几何对象(叶子节点):精筛后添加(自适应采样) + bool edgeIntersects = SectionClipHelper.IntersectsByEdgeSampling(itemBox, clipBox, minSamplesPerEdge: 3); + + if (edgeIntersects) + { + objectsInClipBox.Add(item); + intersectCount++; + } } else { diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 7d23aad..cb5a8bf 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -2667,7 +2667,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels foreach (var stat in objectStats) { - var objectName = stat.Object.DisplayName ?? "未知对象"; + // 获取对象名称(如果当前节点无名,向上查找有名字的父节点) + var objectName = GetEffectiveObjectName(stat.Object); var reason = GetHotspotReason(objectName, stat.Object); hotspots.Add(new CollisionHotspotInfo diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs index 94adebf..abee541 100644 --- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text; using System.Threading.Tasks; @@ -1907,8 +1908,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels sb.AppendLine("✅ 剖面盒设置成功(10m x 10m x 5m)"); sb.AppendLine(); - // 3. 统计对象数量 - sb.AppendLine("【3. 对象数量统计】"); + // 3. 统计对象数量(包围盒相交) + sb.AppendLine("【3. 对象数量统计(包围盒相交)】"); int totalCount, insideCount, outsideCount; SectionClipHelper.CountObjectsInClipBox(out totalCount, out insideCount, out outsideCount); @@ -1921,6 +1922,46 @@ namespace NavisworksTransport.UI.WPF.ViewModels sb.AppendLine($"过滤率: {filterRate:F1}%"); } sb.AppendLine(); + + // 3b. 统计对象数量(角点检测) + sb.AppendLine("【3b. 对象数量统计(角点检测)】"); + int totalCount2, insideCount2, outsideCount2, largeBoxFiltered; + SectionClipHelper.CountObjectsInClipBoxByCorners(out totalCount2, out insideCount2, out outsideCount2, out largeBoxFiltered); + + sb.AppendLine($"总对象数: {totalCount2}"); + sb.AppendLine($"剖面盒内(角点): {insideCount2}"); + sb.AppendLine($"剖面盒外: {outsideCount2}"); + sb.AppendLine($"大包围盒过滤: {largeBoxFiltered}"); + if (totalCount2 > 0) + { + double filterRate2 = (double)outsideCount2 / totalCount2 * 100; + sb.AppendLine($"过滤率: {filterRate2:F1}%"); + } + if (largeBoxFiltered > 0) + { + sb.AppendLine($"⚠️ 发现 {largeBoxFiltered} 个大包围盒对象被过滤(如地基等)"); + } + sb.AppendLine(); + + // 3c. 统计对象数量(棱上采样) + sb.AppendLine("【3c. 对象数量统计(棱上采样 - 用于检测穿过剖面盒的物体)】"); + int totalCount3, insideCount3, outsideCount3, edgeMissed; + CountObjectsInClipBoxByEdgeSampling(out totalCount3, out insideCount3, out outsideCount3, out edgeMissed); + + sb.AppendLine($"总对象数: {totalCount3}"); + sb.AppendLine($"剖面盒内(棱采样): {insideCount3}"); + sb.AppendLine($"剖面盒外: {outsideCount3}"); + sb.AppendLine($"8角点漏检: {edgeMissed}"); + if (totalCount3 > 0) + { + double filterRate3 = (double)outsideCount3 / totalCount3 * 100; + sb.AppendLine($"过滤率: {filterRate3:F1}%"); + } + if (edgeMissed > 0) + { + sb.AppendLine($"✅ 发现 {edgeMissed} 个物体被8角点漏检,但被棱采样检出(如穿过剖面盒的柱子)"); + } + sb.AppendLine(); // 4. 测试相交检测 sb.AppendLine("【4. 相交检测测试】"); @@ -1957,9 +1998,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Info("剖面盒功能测试完成"); UpdateMainStatus("剖面盒测试完成"); - // 显示结果 - System.Windows.MessageBox.Show(sb.ToString(), "剖面盒功能测试", - System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information); + // 显示结果(使用可复制的窗口) + ShowCopyableMessageBox(sb.ToString(), "剖面盒功能测试"); } catch (Exception ex) { @@ -1971,6 +2011,169 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "剖面盒测试"); } + /// + /// 显示可复制文本的消息框 + /// + private void ShowCopyableMessageBox(string message, string title) + { + var window = new System.Windows.Window + { + Title = title, + Width = 600, + Height = 700, + WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen, + ResizeMode = System.Windows.ResizeMode.CanResize + }; + + var grid = new System.Windows.Controls.Grid(); + grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = new System.Windows.GridLength(1, System.Windows.GridUnitType.Star) }); + grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = System.Windows.GridLength.Auto }); + + // 可滚动的文本框 + var scrollViewer = new System.Windows.Controls.ScrollViewer + { + VerticalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Auto, + HorizontalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Auto, + Margin = new System.Windows.Thickness(10) + }; + + // 使用 TextBox 以便选择和复制 + var textBox = new System.Windows.Controls.TextBox + { + Text = message, + IsReadOnly = true, + TextWrapping = System.Windows.TextWrapping.Wrap, + AcceptsReturn = true, + VerticalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Disabled, + BorderThickness = new System.Windows.Thickness(0), + Background = System.Windows.Media.Brushes.White, + FontFamily = new System.Windows.Media.FontFamily("Consolas"), + FontSize = 12 + }; + + scrollViewer.Content = textBox; + System.Windows.Controls.Grid.SetRow(scrollViewer, 0); + grid.Children.Add(scrollViewer); + + // 确定按钮 + var buttonPanel = new System.Windows.Controls.StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + Margin = new System.Windows.Thickness(10) + }; + + var okButton = new System.Windows.Controls.Button + { + Content = "确定", + Width = 80, + Height = 28, + Margin = new System.Windows.Thickness(5, 0, 0, 0) + }; + okButton.Click += (s, e) => window.Close(); + + // 复制按钮 + var copyButton = new System.Windows.Controls.Button + { + Content = "复制全部", + Width = 80, + Height = 28, + Margin = new System.Windows.Thickness(0, 0, 5, 0) + }; + copyButton.Click += (s, e) => + { + System.Windows.Clipboard.SetText(message); + copyButton.Content = "已复制!"; + System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => + { + System.Threading.Thread.Sleep(1000); + copyButton.Content = "复制全部"; + }), System.Windows.Threading.DispatcherPriority.Background); + }; + + buttonPanel.Children.Add(copyButton); + buttonPanel.Children.Add(okButton); + System.Windows.Controls.Grid.SetRow(buttonPanel, 1); + grid.Children.Add(buttonPanel); + + window.Content = grid; + window.ShowDialog(); + } + + /// + /// 统计对象数量(使用棱上采样检测) + /// 能检测穿过剖面盒的物体(如柱子),而8角点检测可能漏检 + /// + private void CountObjectsInClipBoxByEdgeSampling(out int totalCount, out int insideCount, out int outsideCount, out int edgeMissed) + { + totalCount = 0; + insideCount = 0; + outsideCount = 0; + edgeMissed = 0; + + try + { + if (!SectionClipHelper.TryGetCurrentClipBox(out var clipBox)) + { + LogManager.Warning("[剖面盒棱采样统计] 剖面盒未启用"); + return; + } + + var stack = new Stack(); + foreach (var model in Autodesk.Navisworks.Api.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; + } + + // 8角点检测 + bool hasCornerInside = SectionClipHelper.IntersectsByCornerPoints(itemBox, clipBox); + // 棱上采样检测(自适应) + bool hasEdgeSampleInside = SectionClipHelper.IntersectsByEdgeSampling(itemBox, clipBox, minSamplesPerEdge: 3); + + if (hasEdgeSampleInside) + { + insideCount++; + // 记录被棱采样检出但8角点漏检的物体 + if (!hasCornerInside) + { + edgeMissed++; + LogManager.Debug($"[棱采样检出] 8角点漏检但被棱采样检出: {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}]"); + } + } + else + { + outsideCount++; + } + + // 继续遍历子节点 + foreach (var child in item.Children) + { + stack.Push(child); + } + } + + LogManager.Info($"[剖面盒棱采样统计] 总数: {totalCount}, 盒内(棱采样): {insideCount}, 盒外: {outsideCount}, 8角点漏检: {edgeMissed}"); + } + catch (Exception ex) + { + LogManager.Error($"[剖面盒棱采样统计] 失败: {ex.Message}"); + } + } + #endregion #region 清理资源和IDisposable实现 diff --git a/src/Utils/SectionClipHelper.cs b/src/Utils/SectionClipHelper.cs index 13c6fd8..d756d9a 100644 --- a/src/Utils/SectionClipHelper.cs +++ b/src/Utils/SectionClipHelper.cs @@ -270,6 +270,201 @@ namespace NavisworksTransport.Utils } } + /// + /// 使用采样点(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}"); + } + } + /// /// 测试包围盒相交检测(用于验证) ///