From fddefde1ed89d325e7f615a5f0b845f507f736df Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 1 Jun 2026 09:10:47 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=88=AB=E5=90=8D?= =?UTF-8?q?=E6=A0=91=E5=AE=9E=E6=96=BD=E6=96=B9=E6=A1=88=20=E2=80=94=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=98=BE=E7=A4=BA=E5=8E=9F=E5=88=99=20+=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=A0=87=E8=AF=86/=E5=AE=9A=E4=BD=8D/?= =?UTF-8?q?=E8=81=94=E5=8A=A8=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增第7节「别名树显示原则」:二条件OR逻辑、深度限制、别名特殊规则、按需加载生命周期 - 第4节:标识改为 CreatePathId格式,定位改为 Models.ResolvePathId - 第10节:JSON导出格式改为 key 字段 - 第11节:双向联动改为 ModelItem.Parent链 + ExpandAndSelect - 清理 CleanupOnDemandNodes 逻辑文档化 --- doc/design/2026/别名树实施方案.md | 206 ++++++++++++---------- src/UI/WPF/Views/AliasTreeControl.xaml.cs | 27 +++ 2 files changed, 140 insertions(+), 93 deletions(-) diff --git a/doc/design/2026/别名树实施方案.md b/doc/design/2026/别名树实施方案.md index d5032df..0038a5f 100644 --- a/doc/design/2026/别名树实施方案.md +++ b/doc/design/2026/别名树实施方案.md @@ -60,63 +60,57 @@ Revit 重新导出 NWC 再 append 进 NWD 后,`InstanceGuid` 可能整体变化。别名必须跨文件更新存活。 -### 4.2 复合标识方案 +### 4.2 复合标识方案(双字段) ```csharp -public struct AliasNodeIdentity +public readonly struct AliasNodeIdentity { - /// 沿 Parent 链拼接的路径字符串 - public string HierarchicalPath { get; } + /// Navisworks 官方 PathId(CreatePathId 格式: "modelIndex:0/1/2") + public string IndexPath { get; } - /// 同级同名节点的去重索引(0-based) - public int SiblingIndex { get; } + /// 父链 DisplayName 拼接路径(可读、调试用) + public string DisplayPath { get; } - /// 序列化为 DB 主键 - public string ToKey() => $"{HierarchicalPath}#{SiblingIndex}"; + /// 序列化为 DB 主键: "IndexPath||DisplayPath" + public string ToKey() => $"{IndexPath}||{DisplayPath}"; public static AliasNodeIdentity FromModelItem(ModelItem item) { + // IndexPath: 使用 Navisworks 官方 API CreatePathId + var pid = Application.ActiveDocument.Models.CreatePathId(item); + string indexPath = $"{pid.ModelIndex}:{pid.PathId}"; + + // DisplayPath: 父链 DisplayName 拼接(如 "模型/楼层/墙#0") var parts = new List(); var current = item; - while (current != null) - { - parts.Insert(0, current.DisplayName ?? "?"); - current = current.Parent; - } + while (current != null) { parts.Insert(0, current.DisplayName ?? "?"); current = current.Parent; } + string displayPath = string.Join("/", parts); - var path = string.Join("/", parts); - // 计算同级同名节点中的位置 - var parent = item.Parent; - int siblingIndex = 0; - int matchCount = 0; - bool foundSelf = false; - if (parent != null) - { - foreach (var child in parent.Children) - { - if (child.DisplayName == item.DisplayName) - { - if (child.Equals(item)) { foundSelf = true; break; } - matchCount++; - } - siblingIndex++; - } - } - int index = foundSelf ? matchCount : 0; + // 同级同名去重 + int siblingIndex = ComputeSiblingIndex(item); + if (siblingIndex > 0) displayPath += $"#{siblingIndex}"; - return new AliasNodeIdentity(path, index); + return new AliasNodeIdentity(indexPath, displayPath); } } ``` ### 4.3 回溯定位 +使用 Navisworks 官方 API `Models.ResolvePathId()`,与项目路径功能(`BatchQueueManager`、`PathAnimationManager`)完全一致: + ```csharp -// DB 中只有 HierarchicalPath + SiblingIndex。 -// 在树中定位时:沿 path 逐层匹配 DisplayName, -// 到叶子层级时用 SiblingIndex 在同名子节点中定位。 +// IndexPath 格式: "modelIndex:PathId"(如 "0:0/0/35") +int colonIdx = indexPath.IndexOf(':'); +int modelIndex = int.Parse(indexPath.Substring(0, colonIdx)); +string pathId = indexPath.Substring(colonIdx + 1); + +var pid = new ModelItemPathId { ModelIndex = modelIndex, PathId = pathId }; +var item = doc.Models.ResolvePathId(pid); ``` +不做名前缀匹配、不做手动树遍历。 + ### 4.4 容错 如果路径匹配不到目标节点(模型结构变化),标记为「孤儿别名」: @@ -327,13 +321,51 @@ XAML 中的使用方式(参考 `MediaControlIcons.xaml` 模式): --- -## 7. 批量编辑子节点(新增功能) +## 7. 别名树显示原则(核心规则) -### 7.1 场景 +别名树不是 Selection Tree 的完整镜像。显示内容按以下规则决定: + +### 7.1 二条件 OR 逻辑 + +| 条件 | 说明 | 示例 | +|------|------|------| +| **A: 层级限制内** | 深度 ≤ `MaxFullDisplayDepth`(默认 2)的节点 | 根 → 第一层子节点 → 第二层子节点 | +| **B: 用户选中** | 在 Selection Tree 中选中的节点,无论多深 | 选中第 5 层节点 → 其完整路径显示在别名树中 | + +满足任意一个条件即显示。两个条件都不满足的节点不显示。 + +### 7.2 深度限制 + +- `MaxFullDisplayDepth = 2`:初始构建只到第 2 层(根为第 0 层) +- 超限节点不添加展开箭头(无占位节点),用户无法手动展开 +- 展开路径中的节点懒加载:有占位符 → `LazyLoadChildren`(全部子节点);无占位符 → `BuildNode`(只创建路径上的单个节点) + +### 7.3 别名节点特殊规则 + +- **有别名的节点**:无论深度多少,启动时自动加载其完整路径并展开。由 `EnsureAliasedPathsVisible()` 在 `RebuildTree` / `SetDataStore` 后执行 +- 超限别名节点的祖先会被展开,但祖先无别名则不保留占位符子节点 + +### 7.4 按需加载节点的生命周期 + +- 用户在 Selection Tree 中选择超限无别名节点 → 路径加载到别名树 +- **下次选择另一个节点时**,上一次按需加载的无别名超限节点自动清理(`CleanupOnDemandNodes`) +- 有别名的节点不受清理影响,始终保留 +- `_onDemandNodeKeys` HashSet 追踪每次 `ExpandAndSelect` 中通过 `BuildNode` 创建的深层无别名节点 + +### 7.5 例外 + +- 开启别名面板后,树中已有节点被展开过的,其子节点保留(属用户主动操作) +- 通过内联编辑设置别名后,该节点自动变为「条件 A」的别名节点,退出按需追踪 + +--- + +## 8. 批量编辑子节点(新增功能) + +### 8.1 场景 Navisworks 中常见:一个父节点下 20 个子节点全叫「楼层」或「管道」,需要统一加前缀。 -### 7.2 批量编辑对话框 +### 8.2 批量编辑对话框 ``` ┌──────────────────────────────────┐ @@ -356,7 +388,7 @@ Navisworks 中常见:一个父节点下 20 个子节点全叫「楼层」或 └──────────────────────────────────┘ ``` -### 7.3 四种编辑模式 +### 8.3 四种编辑模式 | 模式 | 输入 | 结果 | |------|------|------| @@ -365,27 +397,27 @@ Navisworks 中常见:一个父节点下 20 个子节点全叫「楼层」或 | 替换原名 | "自定义名称" | 所有子节点用同一名称 + 去重后缀 `_001` | | 序号模板 | "管道_{0:D2}" | `管道_01`, `管道_02`... | -### 7.4 去重保证 +### 8.4 去重保证 `MakeUnique()` 逻辑与 `AliasGenerator` 复用。预览中提前展示最终名称(含去重后缀)。 --- -## 8. 别名节点 3D 高亮 +## 9. 别名节点 3D 高亮 -### 8.1 触发时机 +### 9.1 触发时机 - 给节点设置别名时,自动在该节点上加永久颜色覆盖 - 清除别名时,同时清除颜色覆盖 -### 8.2 颜色方案 +### 9.2 颜色方案 ```csharp // 别名高亮色:半透明青色 static readonly Color AliasHighlightColor = Color.FromArgb(80, 0, 150, 200); ``` -### 8.3 实现 +### 9.3 实现 ```csharp public void ApplyAliasHighlight(ModelItem item) @@ -403,13 +435,13 @@ public void RemoveAliasHighlight(ModelItem item) } ``` -### 8.4 清理 +### 9.4 清理 - 文档关闭时自然清理(颜色覆盖不持久化到 NWD) - 清除别名时同时清除颜色 - 全部别名清除提供「清除所有别名高亮」操作 -### 8.5 与其他高亮的兼容 +### 9.5 与其他高亮的兼容 项目已有 `ModelHighlightHelper`,使用 category 机制管理高亮。别名高亮用专用 category `"AliasHighlight"`: @@ -421,9 +453,9 @@ ModelHighlightHelper.HighlightItems("AliasHighlight", itemCollection); --- -## 9. JSON 导入/导出 +## 10. JSON 导入/导出 -### 9.1 格式 +### 10.1 格式 ```json { @@ -432,24 +464,18 @@ ModelHighlightHelper.HighlightItems("AliasHighlight", itemCollection); "documentName": "Project.nwd", "aliases": [ { - "path": "模型名/一楼/楼层", - "siblingIndex": 0, - "alias": "1F-办公层" - }, - { - "path": "模型名/一楼/楼层", - "siblingIndex": 1, - "alias": "1F-大堂" + "key": "0:0/0/0||Floor2_mobile_yup.nwd/Architecture/2ND FLOOR/3' - 3\" x 4' - 3\"", + "alias": "第一个节点" } ] } ``` -### 9.2 导入策略 +### 10.2 导入策略 - **合并模式**:JSON 中的别名覆盖现有同名 key;JSON 中没有的不动。 -### 9.3 字段说明 +### 10.3 字段说明 | 字段 | 用途 | |------|------| @@ -459,57 +485,51 @@ ModelHighlightHelper.HighlightItems("AliasHighlight", itemCollection); --- -## 10. 双向联动机制 +## 11. 双向联动机制 -### 10.1 点击别名树 → 选中模型 +### 11.1 点击别名树 → 选中模型 ```csharp -private void OnAliasTreeSelected(AliasNodeViewModel node) +private void OnTreeViewSelected(AliasNodeViewModel node) { if (node.ModelItem == null) return; - - _isInternalSelection = true; // 防止循环触发 + _isInternalSelection = true; var coll = new ModelItemCollection { node.ModelItem }; - Application.ActiveDocument.CurrentSelection.SelectedItems = coll; + doc.CurrentSelection.Clear(); + doc.CurrentSelection.CopyFrom(coll); _isInternalSelection = false; } ``` -### 10.2 Navisworks 选中变化 → 别名树高亮 +### 11.2 Navisworks 选中变化 → 别名树导航 ```csharp private void OnNavisSelectionChanged(object sender, EventArgs e) { - if (_isInternalSelection) return; // 防止循环 - - Dispatcher.BeginInvoke(() => - { - var selected = Application.ActiveDocument.CurrentSelection.SelectedItems; - if (selected.Count == 1) - { - var item = selected.First(); - TbCurrentName.Text = item.DisplayName; - ExpandAndHighlight(item); - } - }); + if (_isInternalSelection) return; + var selected = doc.CurrentSelection.SelectedItems; + if (selected.Count == 1) + ExpandAndSelect(selected.First()); } ``` -### 10.3 展开路径 +### 11.3 展开路径(沿 ModelItem.Parent 链) ```csharp -private void ExpandAndHighlight(ModelItem item) +private void ExpandAndSelect(ModelItem item) { - var identity = AliasNodeIdentity.FromModelItem(item); - // 1. 逐层沿 path 找 TreeViewItem,展开 + 延时加载 - // 2. 到目标层用 SiblingIndex 定位 - // 3. IsSelected = true + BringIntoView() + // 1. 沿 Parent 链构建祖先列表(根 → 目标) + // 2. 在 rootNodes 中按 ModelItem.Equals 匹配根节点 + // 3. 逐层向下:有占位 → LazyLoadChildren / 无占位 → BuildNode 单点创建 + // 4. SelectTreeNode 选中最终节点 } ``` +不使用 IndexPath 做运行时导航。IndexPath 仅在保存/加载别名时用于持久化(`CreatePathId` / `ResolvePathId`)。 + --- -## 11. 线程安全 +## 12. 线程安全 **所有 Navisworks API 调用必须在主 STA 线程**(项目第 8 节硬约束)。 @@ -530,9 +550,9 @@ private void ExpandAndHighlight(ModelItem item) --- -## 12. 文件组织与集成 +## 13. 文件组织与集成 -### 12.1 目录结构 +### 13.1 目录结构 ``` src/ @@ -552,13 +572,13 @@ src/ │ └── AliasNodeViewModel.cs # 单节点 VM ``` -### 12.2 命名空间 +### 13.2 命名空间 - `NavisworksTransport.Core.AliasTree` — 插件入口、数据层 - `NavisworksTransport.UI.WPF.Views` — XAML 视图 - `NavisworksTransport.UI.WPF.ViewModels` — ViewModel -### 12.3 插件注册 +### 13.3 插件注册 ```csharp // src/Core/AliasTree/AliasTreePlugin.cs @@ -585,7 +605,7 @@ public class AliasTreePlugin : DockPanePlugin } ``` -### 12.4 用户操作步骤 +### 13.4 用户操作步骤 1. 启动 Navisworks → 打开模型 2. **View → Docking Windows → 别名导航树** 打开面板 @@ -595,7 +615,7 @@ public class AliasTreePlugin : DockPanePlugin --- -## 13. 实施步骤(按优先级) +## 14. 实施步骤(按优先级) ### 阶段 1:数据层(2 天) @@ -640,7 +660,7 @@ public class AliasTreePlugin : DockPanePlugin --- -## 14. 风险与已知限制 +## 15. 风险与已知限制 | 风险 | 缓解 | |------|------| @@ -652,7 +672,7 @@ public class AliasTreePlugin : DockPanePlugin --- -## 15. 与现有功能的隔离 +## 16. 与现有功能的隔离 - 不修改任何现有 `PathPlanningManager` / `PathDatabase` 核心逻辑(只在 DB 中加表) - 不侵入 `LogisticsControlPanel`(独立 `DockPanePlugin`) diff --git a/src/UI/WPF/Views/AliasTreeControl.xaml.cs b/src/UI/WPF/Views/AliasTreeControl.xaml.cs index a02383e..e38a78b 100644 --- a/src/UI/WPF/Views/AliasTreeControl.xaml.cs +++ b/src/UI/WPF/Views/AliasTreeControl.xaml.cs @@ -30,6 +30,8 @@ namespace NavisworksTransport.UI.WPF.Views /// 保存别名前触发(确保数据库已创建) internal event Action BeforeSaveAlias; private Dictionary _nodeMap = new Dictionary(); + /// 记录 ExpandAndSelect 按需加载但无别名的深层节点,下次选择时清理 + private HashSet _onDemandNodeKeys = new HashSet(); private bool _isInternalSelection = false; private Autodesk.Navisworks.Api.ModelItem _currentSelectionItem; private bool _built = false; @@ -448,6 +450,9 @@ namespace NavisworksTransport.UI.WPF.Views LogManager.Info($"[别名树] ExpandAndSelect: target={item.DisplayName}"); + // 0. 清理上次按需加载的无别名深层节点 + CleanupOnDemandNodes(); + // 1. 沿 Parent 链构建祖先列表(根 → 目标) var chain = new System.Collections.Generic.List(); var current = item; @@ -518,6 +523,9 @@ namespace NavisworksTransport.UI.WPF.Views { child.Parent = treeNode; treeNode.Children.Add(child); + // 超限且无别名 → 下次选择时清理 + if (child.Depth >= MaxFullDisplayDepth && !child.HasAlias) + _onDemandNodeKeys.Add(child.NodeKey); } } @@ -590,6 +598,25 @@ namespace NavisworksTransport.UI.WPF.Views } } + /// + /// 清理上次按需加载的、无别名的深层节点。 + /// ExpandAndSelect 每次调用前执行,确保前一次超限无别名节点被移除。 + /// + private void CleanupOnDemandNodes() + { + if (_onDemandNodeKeys.Count == 0) return; + + foreach (var key in _onDemandNodeKeys) + { + if (_nodeMap.TryGetValue(key, out var node)) + { + node.Parent?.Children.Remove(node); + _nodeMap.Remove(key); + } + } + _onDemandNodeKeys.Clear(); + } + /// /// 确保所有已存别名的节点路径在树中可见。 /// 在 RebuildTree 后调用,对深层别名节点逐级加载其祖先。