From 4ced5eb77b85477bd16d783141cabd017af7452d Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 6 Aug 2026 23:20:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20F2=20=E6=98=A0=E5=B0=84=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=BA=AF=20PathId=EF=BC=88=E6=AE=B5=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E7=9B=B4=E6=9F=A5=EF=BC=8C=E5=88=A0=E9=99=A4=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E5=90=8D=E5=AD=97/=E9=93=BE=E5=8C=B9=E9=85=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(经导出代码 + 固定文件树对比验证): - 导出 = document.ExportToNwd(ExcludeHiddenItems=true)(临时隐藏 itemsToHide) - 导出树段序列 = 主模型树跳过 itemsToHide 的可见子编号(模拟 vs 副实例实际树完全一致) - 此前链(名字)匹配在重名模型(Revit 嵌套实例)中映射到错误实例, 导致聚焦错误对象触发 NW 原生视角崩溃 修复: - 导出时 BuildSegmentMap:全树深度优先遍历,跳过 itemsToHide, 段序列 → 主 PathId(CreatePathId)映射(2112 条) - 副实例碰撞对象 PathId(段序列)直查表 → 主 PathId/ModelIndex - 删除 TryMatchChainTail/链尾部匹配/名字候选等全部名字相关代码 (TrimRootFromChain/BuildAncestorChain 保留——移动物体副实例解析用) 验证:Factory 实测映射 1/1、4/4(纯 PathId,无名字);集成测试 25/25 --- src/Core/Services/SectionBoxBatchDetector.cs | 238 +++++++----------- .../Services/TestAutomationHttpService.cs | 206 ++++++++++++++- src/Utils/ViewpointHelper.cs | 28 +++ 3 files changed, 317 insertions(+), 155 deletions(-) diff --git a/src/Core/Services/SectionBoxBatchDetector.cs b/src/Core/Services/SectionBoxBatchDetector.cs index 5910c54..7a48a78 100644 --- a/src/Core/Services/SectionBoxBatchDetector.cs +++ b/src/Core/Services/SectionBoxBatchDetector.cs @@ -196,73 +196,27 @@ namespace NavisworksTransport.Core.Services return false; } - // F2:记录盒内对象映射(层级链去根 → 主模型 PathId) - // 🔥 覆盖盒内对象的完整子孙树 + 祖先链(嵌套实例容器层): - // 副实例碰撞对象可能是盒内几何对象的祖先(IsComposite 嵌套实例容器, - // 如 Revit 门/窗实例——不在 ObjectsInBox 但可被碰撞), - // 也可能是其嵌套子节点——三者都必须建映射 - foreach (ModelItem boxItem in traversal.ObjectsInBox) + // F2:构建模拟导出树映射(段序列 → 主模型 PathId) + // 🔥 纯 PathId 方案(无名字/链匹配):导出树 = 主模型树跳过 itemsToHide 的可见子编号 + // (与 NW 的 ExportToNwd(ExcludeHiddenItems=true) 语义一致,已用固定导出文件对比验证) + // 副实例碰撞对象的 PathId(段序列)可直接查表 → 主模型 PathId + var hiddenSet = new HashSet(); + foreach (var h in traversal.ItemsToHide) { - // 1) 子孙树(含自身) - IEnumerable nodesToMap; - try - { - nodesToMap = boxItem.DescendantsAndSelf; - } - catch - { - nodesToMap = new[] { boxItem }; - } - - foreach (ModelItem node in nodesToMap) - { - if (node == null || string.IsNullOrWhiteSpace(node.DisplayName)) - { - continue; - } - - string chain = BuildAncestorChain(node); - if (string.IsNullOrWhiteSpace(chain)) - { - continue; - } - - try - { - var path = document.Models.CreatePathId(node); - chainToMainPathId[TrimRootFromChain(chain)] = new MainItemPath(path.ModelIndex, path.PathId); - } - catch - { - } - } - - // 2) 祖先链(向上到模型根)——嵌套实例容器节点(IsComposite 无自身几何) - var ancestor = boxItem.Parent; - int ancestorGuard = 0; - while (ancestor != null && ancestorGuard < 15) - { - if (!string.IsNullOrWhiteSpace(ancestor.DisplayName)) - { - string ancestorChain = BuildAncestorChain(ancestor); - if (!string.IsNullOrWhiteSpace(ancestorChain)) - { - try - { - var path = document.Models.CreatePathId(ancestor); - chainToMainPathId[TrimRootFromChain(ancestorChain)] = new MainItemPath(path.ModelIndex, path.PathId); - } - catch - { - } - } - } - - ancestor = ancestor.Parent; - ancestorGuard++; - } + hiddenSet.Add(h); } - LogManager.Info($"[剖面盒批检测] 盒内对象映射: {chainToMainPathId.Count} 条"); + + chainToMainPathId.Clear(); + foreach (var model in document.Models) + { + if (model.RootItem == null) + { + continue; + } + + BuildSegmentMap(model.RootItem, hiddenSet, "", document, chainToMainPathId); + } + LogManager.Info($"[剖面盒批检测] 段序列映射: {chainToMainPathId.Count} 条"); string exportedPath = exporter.ExportToNwd(document, traversal, localModelPath); return !string.IsNullOrEmpty(exportedPath) && File.Exists(exportedPath); @@ -499,6 +453,9 @@ namespace NavisworksTransport.Core.Services /// /// 去掉 ancestorChain 的第一个段(根模型文件名),剩余部分主/副实例一致(导出保留树结构)。 /// + /// + /// 去根:去掉层级链的文档根段(移动物体在副实例中按链解析用)。 + /// private static string TrimRootFromChain(string chain) { if (string.IsNullOrWhiteSpace(chain)) @@ -515,46 +472,6 @@ namespace NavisworksTransport.Core.Services return string.Join(" / ", segments.Skip(1).Select(s => s.Trim())); } - /// - /// 链尾部匹配:从末层(对象名)逐级加深,找到唯一以该后缀结尾的主链映射。 - /// 容忍主/副实例链的头部层数差异(文档根/模型根层)。 - /// - private static MainItemPath TryMatchChainTail(string tailChain, Dictionary chainToMainPathId) - { - string[] segments = tailChain.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .Where(s => s.Length > 0) - .ToArray(); - - if (segments.Length == 0) - { - return null; - } - - // 从末 2 层开始,逐级加深到全链(最深匹配优先,避免重名误配) - int maxDepth = Math.Min(segments.Length, 6); - for (int depth = 2; depth <= maxDepth; depth++) - { - string suffix = string.Join(" / ", segments.Skip(segments.Length - depth)); - var candidates = chainToMainPathId.Keys - .Where(k => k.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (candidates.Count == 1) - { - return chainToMainPathId[candidates[0]]; - } - - // 多候选 → 加深一层继续;0 候选 → 无匹配 - if (candidates.Count == 0) - { - return null; - } - } - - return null; - } - /// /// 构造 ModelItem 的层级链(AncestorsAndSelf 的 DisplayName,'/' 分隔)。 /// @@ -586,6 +503,58 @@ namespace NavisworksTransport.Core.Services } } + /// + /// 模拟导出树段序列编号:深度优先遍历主模型树,跳过 itemsToHide(NW ExcludeHiddenItems 语义), + /// 可见子按序编号——段序列与副实例(导出文件)的 PathId 段序列一致(已用固定导出文件对比验证)。 + /// 构建 段序列 → 主模型 PathId 映射(纯 PathId,无名字/链匹配)。 + /// + private static void BuildSegmentMap( + ModelItem node, + HashSet hiddenSet, + string segmentPath, + Document document, + Dictionary chainToMainPathId) + { + if (node == null) + { + return; + } + + // 当前节点映射(文档根 segmentPath 为空——不被碰撞,跳过) + if (!string.IsNullOrEmpty(segmentPath)) + { + try + { + var path = document.Models.CreatePathId(node); + chainToMainPathId[segmentPath] = new MainItemPath(path.ModelIndex, path.PathId); + } + catch + { + } + } + + try + { + int visibleIndex = 0; + foreach (var child in node.Children) + { + if (child == null || hiddenSet.Contains(child)) + { + continue; + } + + string childPath = string.IsNullOrEmpty(segmentPath) + ? visibleIndex.ToString() + : segmentPath + "/" + visibleIndex; + BuildSegmentMap(child, hiddenSet, childPath, document, chainToMainPathId); + visibleIndex++; + } + } + catch + { + } + } + private static async Task RunDetectionAsync( HttpClient client, PathRoute route, @@ -707,56 +676,17 @@ namespace NavisworksTransport.Core.Services SecondaryPathId = (string)obj["pathId"] }; - // F2:按层级链(去根)查盒内对象映射 → 主模型 PathId - string chain = (string)obj["ancestorChain"]; - if (!string.IsNullOrWhiteSpace(chain)) + // F2:纯 PathId 映射——副实例 PathId(段序列)直查模拟导出树表 + string secondaryPathId = mapping.SecondaryPathId; + if (!string.IsNullOrWhiteSpace(secondaryPathId) && chainToMainPathId != null && + chainToMainPathId.TryGetValue(secondaryPathId, out MainItemPath mainItem)) { - string tail = TrimRootFromChain(chain); - if (tail != null && chainToMainPathId != null && chainToMainPathId.TryGetValue(tail, out MainItemPath mainItem)) - { - mapping.MainPathId = mainItem.PathId; - mapping.MainModelIndex = mainItem.ModelIndex; - } - else if (tail != null && chainToMainPathId != null) - { - // 🔥 精确匹配失败 → 尾部匹配(容忍根/模型根层数差异): - // 主链根=文档名(Factory.nwd)+模型根,副链根=导出模型根,去根后差一层 - MainItemPath tailMatched = TryMatchChainTail(tail, chainToMainPathId); - if (tailMatched != null) - { - mapping.MainPathId = tailMatched.PathId; - mapping.MainModelIndex = tailMatched.ModelIndex; - } - else - { - // 🔥 映射诊断:打印碰撞对象链与盒内映射候选,定位匹配失败原因 - string last3 = string.Join(" / ", tail.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()).Reverse().Take(3).Reverse()); - var nearCandidates = chainToMainPathId.Keys - .Where(k => k.EndsWith(last3, StringComparison.OrdinalIgnoreCase)) - .Take(3) - .ToList(); - var nameCandidates = chainToMainPathId.Keys - .Where(k => k.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0) - .Take(3) - .ToList(); - // 等长精确候选:与副链完全相等(Trim 后)的字典 key——验证隐藏字符差异 - string tailTrimmed = string.Join(" /", tail.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim())); - var equalCandidates = chainToMainPathId.Keys - .Where(k => - { - string kTrimmed = string.Join(" /", k.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim())); - return string.Equals(kTrimmed, tailTrimmed, StringComparison.OrdinalIgnoreCase); - }) - .Take(3) - .ToList(); - LogManager.Warning($"[F2映射诊断] {name}: 副链去根=[{tail}] 等长候选={equalCandidates.Count} 例: {string.Join(" || ", equalCandidates)} | 末3候选={nearCandidates.Count} | 名候选={nameCandidates.Count}"); - } - } - else - { - LogManager.Warning($"[F2映射诊断] {name}: 链为空或字典为空, chain=[{chain}]"); - } + mapping.MainPathId = mainItem.PathId; + mapping.MainModelIndex = mainItem.ModelIndex; + } + else + { + LogManager.Warning($"[F2映射] {name}: 副实例PathId未匹配主模型段序列表 (pathId={secondaryPathId})"); } result.CollidedObjects.Add(name); diff --git a/src/Core/Services/TestAutomationHttpService.cs b/src/Core/Services/TestAutomationHttpService.cs index 71c8fe7..a2461cc 100644 --- a/src/Core/Services/TestAutomationHttpService.cs +++ b/src/Core/Services/TestAutomationHttpService.cs @@ -600,6 +600,85 @@ namespace NavisworksTransport.Core.Services return new { error = "无活动文档" }; } + if (string.Equals(GetOptionalQueryValue(query, "mode"), "simulate", StringComparison.OrdinalIgnoreCase)) + { + // 模拟导出树:按盒重算盒内对象 + itemsToHide,遍历主树跳过隐藏节点, + // 输出模拟导出树的段序列 + 名字(与副实例实际导出树对比验证) + try + { + double x1 = ParseRequiredDouble(query, "x1"); + double y1 = ParseRequiredDouble(query, "y1"); + double z1 = ParseRequiredDouble(query, "z1"); + double x2 = ParseRequiredDouble(query, "x2"); + double y2 = ParseRequiredDouble(query, "y2"); + double z2 = ParseRequiredDouble(query, "z2"); + + var bounds = new BoundingBox3D( + new Point3D(Math.Min(x1, x2), Math.Min(y1, y2), Math.Min(z1, z2)), + new Point3D(Math.Max(x1, x2), Math.Max(y1, y2), Math.Max(z1, z2))); + + var exporter = new NavisworksTransport.Core.SectionBoxExporter(); + var traversal = exporter.GetObjectsAndHiddenItems(doc, bounds); + + var hiddenSet = new HashSet(); + foreach (var h in traversal.ItemsToHide) + { + hiddenSet.Add(h); + } + + var tree = new List(); + int maxNodes = int.TryParse(GetOptionalQueryValue(query, "maxNodes"), out int m) && m > 0 ? m : 300; + + foreach (var model in doc.Models) + { + if (model.RootItem == null) + { + continue; + } + + // 模拟导出:跳过隐藏节点(含子树),可见子按序编号 + // PathId 格式:从文档根的子开始(文档根/模型根无段) + SimulateExportTree(model.RootItem, hiddenSet, "", tree, maxNodes); + } + + return new + { + documentName = doc.FileName, + nodeCount = tree.Count, + objectsInBox = traversal.ObjectsInBox.Count, + itemsToHide = traversal.ItemsToHide.Count, + tree + }; + } + catch (Exception ex) + { + return new { error = ex.Message }; + } + } + + if (string.Equals(GetOptionalQueryValue(query, "mode"), "full", StringComparison.OrdinalIgnoreCase)) + { + // 完整树输出(名字 + PathId)——导出模型 vs 主模型树关系研究 + var tree = new List(); + int maxNodes = int.TryParse(GetOptionalQueryValue(query, "maxNodes"), out int m) && m > 0 ? m : 500; + foreach (var model in doc.Models) + { + if (model.RootItem == null) + { + continue; + } + + DumpTree(model.RootItem, 0, tree, maxNodes); + } + + return new + { + documentName = doc.FileName, + nodeCount = tree.Count, + tree + }; + } + string matchName = GetOptionalQueryValue(query, "name"); if (!string.IsNullOrWhiteSpace(matchName)) { @@ -626,13 +705,62 @@ namespace NavisworksTransport.Core.Services } } + var nameObjects = new List(); + foreach (var model in doc.Models) + { + if (model.RootItem == null) + { + continue; + } + + foreach (var item in model.RootItem.DescendantsAndSelf) + { + if (string.Equals(item.DisplayName, matchName, StringComparison.OrdinalIgnoreCase)) + { + try + { + var b = item.BoundingBox(); + string pathId = ""; + int modelIndex = -1; + try + { + var path = doc.Models.CreatePathId(item); + pathId = path.PathId; + modelIndex = path.ModelIndex; + } + catch + { + } + + nameObjects.Add(new + { + name = item.DisplayName, + pathId = $"{modelIndex}/{pathId}", + bbox = $"({b.Min.X:F2},{b.Min.Y:F2},{b.Min.Z:F2})~({b.Max.X:F2},{b.Max.Y:F2},{b.Max.Z:F2})", + size = $"{b.Max.X - b.Min.X:F2} x {b.Max.Y - b.Min.Y:F2} x {b.Max.Z - b.Min.Z:F2}" + }); + } + catch (Exception bEx) + { + nameObjects.Add(new { name = item.DisplayName, bbox = $"ERROR: {bEx.Message}" }); + } + + if (nameObjects.Count >= 3) + { + break; + } + } + } + } + return new { documentName = doc.FileName, modelCount = doc.Models.Count, nameMatch = matchName, matchCount, - chains + chains, + objects = nameObjects }; } @@ -669,6 +797,82 @@ namespace NavisworksTransport.Core.Services }; } + private static void SimulateExportTree(ModelItem node, HashSet hiddenSet, string segmentPath, List lines, int maxNodes) + { + if (node == null || lines.Count >= maxNodes) + { + return; + } + + // NW ExcludeHiddenItems 语义:隐藏节点(含子树)不导出;可见子按序编号 + try + { + lines.Add($"{node.DisplayName ?? ""} [{segmentPath}]"); + } + catch + { + } + + try + { + int visibleIndex = 0; + foreach (var child in node.Children) + { + if (child == null || hiddenSet.Contains(child)) + { + continue; + } + + string childPath = string.IsNullOrEmpty(segmentPath) + ? visibleIndex.ToString() + : segmentPath + "/" + visibleIndex; + SimulateExportTree(child, hiddenSet, childPath, lines, maxNodes); + visibleIndex++; + } + } + catch + { + } + } + + private static void DumpTree(ModelItem node, int depth, List lines, int maxNodes) + { + if (node == null || lines.Count >= maxNodes) + { + return; + } + + try + { + string pathId = ""; + try + { + var path = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(node); + pathId = path.PathId; + } + catch + { + } + + lines.Add($"{new string(' ', depth * 2)}{node.DisplayName ?? ""} [{pathId}]"); + } + catch + { + lines.Add($"{new string(' ', depth * 2)}"); + } + + try + { + foreach (var child in node.Children) + { + DumpTree(child, depth + 1, lines, maxNodes); + } + } + catch + { + } + } + private object BuildTraversableItemsPayload() { // 【实验端点】查询当前文档的可通行物流属性对象(验证通道属性是否在模型文件中) diff --git a/src/Utils/ViewpointHelper.cs b/src/Utils/ViewpointHelper.cs index 5afb99a..68e4d67 100644 --- a/src/Utils/ViewpointHelper.cs +++ b/src/Utils/ViewpointHelper.cs @@ -556,6 +556,15 @@ namespace NavisworksTransport.Utils throw new InvalidOperationException("没有活动的文档"); } + // 🔥 非法值防御:相机/目标坐标非有限值(NaN/Infinity)会导致 + // AlignDirection(NaN) 触发 NW 原生崩溃(非托管,无法 try-catch)—— + // 异常包围盒(巨大/无效实例)是常见来源,防御后跳过本次视角调整。 + if (!IsFinite(cameraPosition) || !IsFinite(targetPoint)) + { + LogManager.Warning($"[ViewpointHelper] 视角参数非有限值,跳过视角调整: camera=({cameraPosition.X},{cameraPosition.Y},{cameraPosition.Z}), target=({targetPoint.X},{targetPoint.Y},{targetPoint.Z})"); + return; + } + Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy(); newViewpoint.Position = cameraPosition; @@ -567,6 +576,15 @@ namespace NavisworksTransport.Utils targetPoint.Y - cameraPosition.Y, targetPoint.Z - cameraPosition.Z ); + + // 🔥 零方向防御:相机与目标重合(距离近零)→ Normalize 产生 NaN → NW 崩溃 + double lookLengthSq = lookDirection.X * lookDirection.X + lookDirection.Y * lookDirection.Y + lookDirection.Z * lookDirection.Z; + if (lookLengthSq < 1e-12) + { + LogManager.Warning($"[ViewpointHelper] 相机与目标距离近零,跳过视角调整: camera=({cameraPosition.X},{cameraPosition.Y},{cameraPosition.Z})"); + return; + } + lookDirection.Normalize(); newViewpoint.AlignDirection(lookDirection); newViewpoint.AlignUp(upVector); @@ -581,6 +599,16 @@ namespace NavisworksTransport.Utils doc.CurrentViewpoint.CopyFrom(newViewpoint); } + private static bool IsFinite(Point3D p) + { + return IsFinite(p.X) && IsFinite(p.Y) && IsFinite(p.Z); + } + + private static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } + /// /// 应用视角并缩放视野(使用 ZoomBox) ///