docs: 更新别名树实施方案 — 新增显示原则 + 修正标识/定位/联动描述
- 新增第7节「别名树显示原则」:二条件OR逻辑、深度限制、别名特殊规则、按需加载生命周期 - 第4节:标识改为 CreatePathId格式,定位改为 Models.ResolvePathId - 第10节:JSON导出格式改为 key 字段 - 第11节:双向联动改为 ModelItem.Parent链 + ExpandAndSelect - 清理 CleanupOnDemandNodes 逻辑文档化
This commit is contained in:
parent
6260c175d4
commit
fddefde1ed
@ -60,63 +60,57 @@
|
||||
|
||||
Revit 重新导出 NWC 再 append 进 NWD 后,`InstanceGuid` 可能整体变化。别名必须跨文件更新存活。
|
||||
|
||||
### 4.2 复合标识方案
|
||||
### 4.2 复合标识方案(双字段)
|
||||
|
||||
```csharp
|
||||
public struct AliasNodeIdentity
|
||||
public readonly struct AliasNodeIdentity
|
||||
{
|
||||
/// <summary>沿 Parent 链拼接的路径字符串</summary>
|
||||
public string HierarchicalPath { get; }
|
||||
/// <summary>Navisworks 官方 PathId(CreatePathId 格式: "modelIndex:0/1/2")</summary>
|
||||
public string IndexPath { get; }
|
||||
|
||||
/// <summary>同级同名节点的去重索引(0-based)</summary>
|
||||
public int SiblingIndex { get; }
|
||||
/// <summary>父链 DisplayName 拼接路径(可读、调试用)</summary>
|
||||
public string DisplayPath { get; }
|
||||
|
||||
/// <summary>序列化为 DB 主键</summary>
|
||||
public string ToKey() => $"{HierarchicalPath}#{SiblingIndex}";
|
||||
/// <summary>序列化为 DB 主键: "IndexPath||DisplayPath"</summary>
|
||||
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<string>();
|
||||
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`)
|
||||
|
||||
@ -30,6 +30,8 @@ namespace NavisworksTransport.UI.WPF.Views
|
||||
/// <summary>保存别名前触发(确保数据库已创建)</summary>
|
||||
internal event Action BeforeSaveAlias;
|
||||
private Dictionary<string, AliasNodeViewModel> _nodeMap = new Dictionary<string, AliasNodeViewModel>();
|
||||
/// <summary>记录 ExpandAndSelect 按需加载但无别名的深层节点,下次选择时清理</summary>
|
||||
private HashSet<string> _onDemandNodeKeys = new HashSet<string>();
|
||||
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<Autodesk.Navisworks.Api.ModelItem>();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理上次按需加载的、无别名的深层节点。
|
||||
/// ExpandAndSelect 每次调用前执行,确保前一次超限无别名节点被移除。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保所有已存别名的节点路径在树中可见。
|
||||
/// 在 RebuildTree 后调用,对深层别名节点逐级加载其祖先。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user