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 后调用,对深层别名节点逐级加载其祖先。