- 新增第7节「别名树显示原则」:二条件OR逻辑、深度限制、别名特殊规则、按需加载生命周期 - 第4节:标识改为 CreatePathId格式,定位改为 Models.ResolvePathId - 第10节:JSON导出格式改为 key 字段 - 第11节:双向联动改为 ModelItem.Parent链 + ExpandAndSelect - 清理 CleanupOnDemandNodes 逻辑文档化
681 lines
26 KiB
Markdown
681 lines
26 KiB
Markdown
# 别名树(节点重命名)实施方案
|
||
|
||
> 基于 [节点重命名参考方案.md](./节点重命名参考方案.md) 的评估与完善。
|
||
> 对应评估结论中 P0/P1/P2 的全部整改点。
|
||
|
||
---
|
||
|
||
## 1. 目标
|
||
|
||
为用户提供「给 Selection Tree 中任意节点打别名」的能力,解决 Revit 导入后 DisplayName 不可读(全叫"楼层""墙""管道")、无法靠名字快速定位的问题。
|
||
|
||
不做原生树扩展,走 **独立别名面板 + 自管数据层** 路线。
|
||
|
||
---
|
||
|
||
## 2. 架构概览
|
||
|
||
```
|
||
┌──────────────────────────┐
|
||
│ Navisworks 主窗口 │
|
||
│ ┌──────────┬──────────┐ │
|
||
│ │ Selection │ 3D View │ │
|
||
│ │ Tree │ │ │
|
||
│ │(原生只读) │ │ │
|
||
│ ├──────────┤ │ │
|
||
│ │ 别名导航树 │ │ │ ← 新增独立 DockPanePlugin
|
||
│ │(可编辑别名)│ │ │
|
||
│ └──────────┴──────────┘ │
|
||
│ │
|
||
│ SQLite: DocName.db │
|
||
│ ├─ Paths (已有) │
|
||
│ ├─ Collisions (已有) │
|
||
│ └─ AliasMap (新增) │ ← 别名持久化
|
||
└──────────────────────────┘
|
||
```
|
||
|
||
独立 `DockPanePlugin`,不并入 `LogisticsControlPanel`。用户在 **View → Docking Windows → 别名导航树** 打开后可手动 dock 到 Selection Tree 下方。
|
||
|
||
---
|
||
|
||
## 3. 文件清单(新增 9 个文件)
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `src/Core/AliasTree/AliasTreePlugin.cs` | DockPanePlugin 入口 + ElementHost |
|
||
| `src/Core/AliasTree/AliasNodeIdentity.cs` | 稳定的复合节点标识(替代 InstanceGuid) |
|
||
| `src/Core/AliasTree/AliasNodeViewModel.cs` | 单节点 VM,含 INotifyPropertyChanged |
|
||
| `src/Core/AliasTree/AliasDataStore.cs` | 别名数据的 CRUD + 与 PathDatabase 对接 |
|
||
| `src/UI/WPF/Views/AliasTreeControl.xaml` | TreeView 布局 + 图标工具栏 + 右键菜单 |
|
||
| `src/UI/WPF/Views/AliasTreeControl.xaml.cs` | 核心逻辑:联动、编辑、批量、高亮、导入导出 |
|
||
| `src/UI/WPF/Views/BatchAliasDialog.xaml` | 批量编辑对话框 |
|
||
| `src/UI/WPF/Views/BatchAliasDialog.xaml.cs` | 批量编辑逻辑 |
|
||
| `src/UI/WPF/Resources/AliasTreeIcons.xaml` | 工具栏图标(Path Geometry 矢量图标) |
|
||
|
||
---
|
||
|
||
## 4. 稳定节点标识设计(核心创新点)
|
||
|
||
### 4.1 为什么不用 InstanceGuid
|
||
|
||
Revit 重新导出 NWC 再 append 进 NWD 后,`InstanceGuid` 可能整体变化。别名必须跨文件更新存活。
|
||
|
||
### 4.2 复合标识方案(双字段)
|
||
|
||
```csharp
|
||
public readonly struct AliasNodeIdentity
|
||
{
|
||
/// <summary>Navisworks 官方 PathId(CreatePathId 格式: "modelIndex:0/1/2")</summary>
|
||
public string IndexPath { get; }
|
||
|
||
/// <summary>父链 DisplayName 拼接路径(可读、调试用)</summary>
|
||
public string DisplayPath { get; }
|
||
|
||
/// <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; }
|
||
string displayPath = string.Join("/", parts);
|
||
|
||
// 同级同名去重
|
||
int siblingIndex = ComputeSiblingIndex(item);
|
||
if (siblingIndex > 0) displayPath += $"#{siblingIndex}";
|
||
|
||
return new AliasNodeIdentity(indexPath, displayPath);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 回溯定位
|
||
|
||
使用 Navisworks 官方 API `Models.ResolvePathId()`,与项目路径功能(`BatchQueueManager`、`PathAnimationManager`)完全一致:
|
||
|
||
```csharp
|
||
// 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 容错
|
||
|
||
如果路径匹配不到目标节点(模型结构变化),标记为「孤儿别名」:
|
||
- 在别名树中以灰色 + 删除线显示
|
||
- 提供"清除孤儿别名"按钮
|
||
|
||
---
|
||
|
||
## 5. 别名数据持久化(PathDatabase 扩展)
|
||
|
||
### 5.1 新增 SQLite 表
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS AliasMap (
|
||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
NodeKey TEXT NOT NULL UNIQUE, -- AliasNodeIdentity.ToKey()
|
||
AliasText TEXT NOT NULL,
|
||
CreatedAt TEXT DEFAULT (datetime('now')),
|
||
ModifiedAt TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_alias_key ON AliasMap(NodeKey);
|
||
```
|
||
|
||
### 5.2 AliasDataStore 接口
|
||
|
||
```csharp
|
||
public class AliasDataStore
|
||
{
|
||
// 生命周期跟随 PathDatabase(文档打开时连接,关闭时断开)
|
||
|
||
string GetAlias(AliasNodeIdentity id); // 读
|
||
void SetAlias(AliasNodeIdentity id, string alias); // 写(upsert)
|
||
void DeleteAlias(AliasNodeIdentity id); // 删
|
||
void DeleteAliasesByPrefix(string pathPrefix); // 批量删(清子节点)
|
||
Dictionary<string, string> LoadAll(); // 全量加载到内存缓存
|
||
int Count { get; } // 统计
|
||
|
||
// 孤儿别名
|
||
List<string> FindOrphans(HashSet<string> validKeys); // 检测
|
||
void PurgeOrphans(HashSet<string> validKeys); // 清理
|
||
}
|
||
```
|
||
|
||
### 5.3 与 PathDatabase 的关系
|
||
|
||
`AliasDataStore` 不持有独立 SQLiteConnection。通过 `PathDatabase.Connection` 操作,使别名表与路径/碰撞数据共享同一事务生命周期和备份机制。
|
||
|
||
---
|
||
|
||
## 6. 面板 UI 设计
|
||
|
||
### 6.1 布局
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ 顶部图标工具栏(24px 高) │
|
||
│ [当前: 未命名节点标题] [💾] [⏬] [⏫] [⚙] [🗑] │
|
||
│ 保存 导出 导入 自动 清除 │
|
||
│ │
|
||
│ [别名输入框 ] [✓] │
|
||
├──────────────────────────────────────────┤
|
||
│ TreeView(别名导航树) │
|
||
│ ├─ 模型名(灰色斜体=无别名) │
|
||
│ │ ├─ 一楼 → "1F-大厅" ◆蓝色粗体 │
|
||
│ │ ├─ 一楼 → 灰色斜体 │
|
||
│ │ ├─ 一楼_001 → 灰色斜体 │
|
||
│ │ └─ ... │
|
||
│ └─ 另一个模型 │
|
||
│ └─ ... │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
视觉规范:
|
||
- **有别名**:蓝色 (#1565C0) 粗体,加 ◆ 前缀图标
|
||
- **无别名**:灰色 (#888) 斜体,显示原 DisplayName
|
||
- **编辑中**:`TextBox` 覆盖 TextBlock,背景浅黄
|
||
- **孤儿别名**:灰色删除线
|
||
- **鼠标悬停行**:背景浅蓝 + 显示铅笔按钮 ✎
|
||
- **工具栏**:图标按钮 24×24,Navisworks 蓝色系,鼠标悬停高亮
|
||
|
||
### 6.2 顶部图标工具栏
|
||
|
||
按钮统一使用 WPF `Path` 几何图形(与项目 `MediaControlIcons.xaml` 同模式),不依赖外部图片文件。
|
||
工具栏风格参考 Visual Studio / Navisworks 内置面板:紧凑横向排列,仅图标,悬停显示 ToolTip。
|
||
|
||
| 图标 | Geometry(Path Data) | ToolTip | 功能 |
|
||
|------|----------------------|---------|------|
|
||
| 💾 保存 | `M15,9H5V5H15M15,19H5V15H15M17,3H3V21H17V16L21,12L17,8V3Z` | 保存别名 | 保存顶部输入框中的别名到当前选中节点 |
|
||
| ⏬ 导出 | `M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z` | 导出别名 (JSON) | 导出全量别名到 `.alias.json` |
|
||
| ⏫ 导入 | `M5,10H19V12H5M12,5L5,12H9V21H15V12H19L12,5Z` | 导入别名 (JSON) | 从 `.alias.json` 导入(合并模式) |
|
||
| ⚙ 自动 | `M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.97 19.05,5.05L16.56,6.05C16.04,5.65 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.65 7.44,6.05L4.95,5.05C4.73,4.97 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.95C7.96,18.35 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.35 16.56,17.95L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z` | 自动生成 | 按父链路径自动生成前缀别名 |
|
||
| 🗑 清除 | `M6,19V7H18V19H6M9,4H15L14.5,3H9.5L9,4M19,5V7H5V5H8L8.5,4H15.5L16,5H19Z` | 清除全部别名 | 清除所有别名和高亮,恢复原名显示 |
|
||
|
||
第二行保留文本输入区:
|
||
- `「当前: xxx」` — 选中节点的 DisplayName
|
||
- 别名输入框 — 直接输入别名
|
||
- `✓` 确认按钮 — 与回车等效,保存并刷新
|
||
|
||
### 6.3 图标按钮实现
|
||
|
||
沿用项目现有 `MediaControlIcons.xaml` 的模式 — 纯 WPF `Path` 几何图形,不依赖外部 .ico/.png 文件。
|
||
|
||
图标资源文件 `AliasTreeIcons.xaml`:
|
||
|
||
```xml
|
||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||
|
||
<!-- 保存图标(磁盘) -->
|
||
<Geometry x:Key="SaveIconGeometry">
|
||
M15,9H5V5H15M15,19H5V15H15M17,3H3V21H17V16L21,12L17,8V3Z
|
||
</Geometry>
|
||
|
||
<!-- 导出图标(向上箭头出盒子) -->
|
||
<Geometry x:Key="ExportIconGeometry">
|
||
M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z
|
||
</Geometry>
|
||
|
||
<!-- 导入图标(向下箭头入盒子) -->
|
||
<Geometry x:Key="ImportIconGeometry">
|
||
M5,10H19V12H5M12,5L5,12H9V21H15V12H19L12,5Z
|
||
</Geometry>
|
||
|
||
<!-- 自动生成图标(齿轮) -->
|
||
<Geometry x:Key="AutoGenIconGeometry">
|
||
M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12
|
||
A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 ...
|
||
</Geometry>
|
||
|
||
<!-- 清除图标(垃圾桶) -->
|
||
<Geometry x:Key="ClearIconGeometry">
|
||
M6,19V7H18V19H6M9,4H15L14.5,3H9.5L9,4M19,5V7H5V5H8L8.5,4H15.5L16,5H19Z
|
||
</Geometry>
|
||
|
||
<!-- 别名工具栏图标按钮样式(紧凑,24×24) -->
|
||
<Style x:Key="AliasToolbarButtonStyle" TargetType="Button">
|
||
<Setter Property="Width" Value="24"/>
|
||
<Setter Property="Height" Value="24"/>
|
||
<Setter Property="Margin" Value="1,0"/>
|
||
<Setter Property="Padding" Value="2"/>
|
||
<Setter Property="Background" Value="Transparent"/>
|
||
<Setter Property="BorderThickness" Value="0"/>
|
||
<Setter Property="Foreground" Value="{StaticResource NavisworksTextBrush}"/>
|
||
<Setter Property="Cursor" Value="Hand"/>
|
||
<Setter Property="ToolTipService.ShowOnDisabled" Value="True"/>
|
||
<Setter Property="Template">
|
||
<Setter.Value>
|
||
<ControlTemplate TargetType="Button">
|
||
<Border x:Name="border"
|
||
Background="{TemplateBinding Background}"
|
||
BorderThickness="0"
|
||
CornerRadius="2">
|
||
<ContentPresenter HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"/>
|
||
</Border>
|
||
<ControlTemplate.Triggers>
|
||
<Trigger Property="IsMouseOver" Value="True">
|
||
<Setter TargetName="border" Property="Background"
|
||
Value="{StaticResource NavisworksLightBrush}"/>
|
||
</Trigger>
|
||
<Trigger Property="IsPressed" Value="True">
|
||
<Setter TargetName="border" Property="Background"
|
||
Value="{StaticResource NavisworksButtonBrush}"/>
|
||
</Trigger>
|
||
<Trigger Property="IsEnabled" Value="False">
|
||
<Setter Property="Foreground" Value="#FFBBBBBB"/>
|
||
<Setter Property="Opacity" Value="0.5"/>
|
||
</Trigger>
|
||
</ControlTemplate.Triggers>
|
||
</ControlTemplate>
|
||
</Setter.Value>
|
||
</Setter>
|
||
</Style>
|
||
|
||
</ResourceDictionary>
|
||
```
|
||
|
||
XAML 中的使用方式(参考 `MediaControlIcons.xaml` 模式):
|
||
|
||
```xml
|
||
<Button Style="{StaticResource AliasToolbarButtonStyle}"
|
||
ToolTip="保存别名" Click="OnSaveClick">
|
||
<Path Data="{StaticResource SaveIconGeometry}"
|
||
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
|
||
Width="16" Height="16" Stretch="Uniform"/>
|
||
</Button>
|
||
```
|
||
|
||
合并到控件 XAML 的 `ResourceDictionary.MergedDictionaries`:
|
||
|
||
```xml
|
||
<ResourceDictionary.MergedDictionaries>
|
||
<ResourceDictionary Source="../Resources/NavisworksStyles.xaml"/>
|
||
<ResourceDictionary Source="../Resources/AliasTreeIcons.xaml"/>
|
||
</ResourceDictionary.MergedDictionaries>
|
||
```
|
||
|
||
### 6.4 右键菜单
|
||
|
||
| 菜单项 | 快捷键 | 功能 |
|
||
|--------|--------|------|
|
||
| 编辑别名 | F2 | 就地编辑 TextBox |
|
||
| 清除别名 | Del | 删除别名,恢复显示原名 |
|
||
| **批量编辑子节点…** | — | 弹出批量编辑对话框(见第 7 节) |
|
||
| 在视图中聚焦 | — | 3D 视图中选中 + 聚焦该节点 |
|
||
| 复制原名 | — | 复制 DisplayName 到剪贴板 |
|
||
|
||
---
|
||
|
||
## 7. 别名树显示原则(核心规则)
|
||
|
||
别名树不是 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 个子节点全叫「楼层」或「管道」,需要统一加前缀。
|
||
|
||
### 8.2 批量编辑对话框
|
||
|
||
```
|
||
┌──────────────────────────────────┐
|
||
│ 批量编辑子节点 — 父节点: "办公楼层" │
|
||
│ │
|
||
│ 编辑模式: │
|
||
│ ○ 统一前缀 [2F- ] │
|
||
│ ● 统一后缀 [ _办公区] │
|
||
│ ○ 替换原名 [自定义名称 ] │
|
||
│ ○ 序号模板 [管道_{0:D2} ] │ ← 自动编号
|
||
│ │
|
||
│ 预览: │
|
||
│ 楼层 → 楼层_办公区 │
|
||
│ 楼层 → 楼层_001_办公区 │ ← 同名自动去重
|
||
│ 走廊 → 走廊_办公区 │
|
||
│ 电梯间 → 电梯间_办公区 │
|
||
│ ...共 12 个子节点 │
|
||
│ │
|
||
│ [应用] [取消] │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
### 8.3 四种编辑模式
|
||
|
||
| 模式 | 输入 | 结果 |
|
||
|------|------|------|
|
||
| 统一前缀 | "2F-" | `2F-原名` |
|
||
| 统一后缀 | "_办公区" | `原名_办公区` |
|
||
| 替换原名 | "自定义名称" | 所有子节点用同一名称 + 去重后缀 `_001` |
|
||
| 序号模板 | "管道_{0:D2}" | `管道_01`, `管道_02`... |
|
||
|
||
### 8.4 去重保证
|
||
|
||
`MakeUnique()` 逻辑与 `AliasGenerator` 复用。预览中提前展示最终名称(含去重后缀)。
|
||
|
||
---
|
||
|
||
## 9. 别名节点 3D 高亮
|
||
|
||
### 9.1 触发时机
|
||
|
||
- 给节点设置别名时,自动在该节点上加永久颜色覆盖
|
||
- 清除别名时,同时清除颜色覆盖
|
||
|
||
### 9.2 颜色方案
|
||
|
||
```csharp
|
||
// 别名高亮色:半透明青色
|
||
static readonly Color AliasHighlightColor = Color.FromArgb(80, 0, 150, 200);
|
||
```
|
||
|
||
### 9.3 实现
|
||
|
||
```csharp
|
||
public void ApplyAliasHighlight(ModelItem item)
|
||
{
|
||
var items = new ModelItemCollection { item };
|
||
var doc = Application.ActiveDocument;
|
||
doc.Models.OverridePermanentColor(items, AliasHighlightColor);
|
||
}
|
||
|
||
public void RemoveAliasHighlight(ModelItem item)
|
||
{
|
||
var items = new ModelItemCollection { item };
|
||
var doc = Application.ActiveDocument;
|
||
doc.Models.OverridePermanentColor(items, Color.Empty); // 恢复默认
|
||
}
|
||
```
|
||
|
||
### 9.4 清理
|
||
|
||
- 文档关闭时自然清理(颜色覆盖不持久化到 NWD)
|
||
- 清除别名时同时清除颜色
|
||
- 全部别名清除提供「清除所有别名高亮」操作
|
||
|
||
### 9.5 与其他高亮的兼容
|
||
|
||
项目已有 `ModelHighlightHelper`,使用 category 机制管理高亮。别名高亮用专用 category `"AliasHighlight"`:
|
||
|
||
```csharp
|
||
ModelHighlightHelper.HighlightItems("AliasHighlight", itemCollection);
|
||
```
|
||
|
||
可与物流分类高亮、碰撞高亮共存,互不覆盖。
|
||
|
||
---
|
||
|
||
## 10. JSON 导入/导出
|
||
|
||
### 10.1 格式
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"exportedAt": "2026-05-31T10:30:00",
|
||
"documentName": "Project.nwd",
|
||
"aliases": [
|
||
{
|
||
"key": "0:0/0/0||Floor2_mobile_yup.nwd/Architecture/2ND FLOOR/3' - 3\" x 4' - 3\"",
|
||
"alias": "第一个节点"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 10.2 导入策略
|
||
|
||
- **合并模式**:JSON 中的别名覆盖现有同名 key;JSON 中没有的不动。
|
||
|
||
### 10.3 字段说明
|
||
|
||
| 字段 | 用途 |
|
||
|------|------|
|
||
| `path` | 父链 DisplayName 拼接(对应 DB 的 HierarchicalPath) |
|
||
| `siblingIndex` | 同级去重索引 |
|
||
| `alias` | 用户别名 |
|
||
|
||
---
|
||
|
||
## 11. 双向联动机制
|
||
|
||
### 11.1 点击别名树 → 选中模型
|
||
|
||
```csharp
|
||
private void OnTreeViewSelected(AliasNodeViewModel node)
|
||
{
|
||
if (node.ModelItem == null) return;
|
||
_isInternalSelection = true;
|
||
var coll = new ModelItemCollection { node.ModelItem };
|
||
doc.CurrentSelection.Clear();
|
||
doc.CurrentSelection.CopyFrom(coll);
|
||
_isInternalSelection = false;
|
||
}
|
||
```
|
||
|
||
### 11.2 Navisworks 选中变化 → 别名树导航
|
||
|
||
```csharp
|
||
private void OnNavisSelectionChanged(object sender, EventArgs e)
|
||
{
|
||
if (_isInternalSelection) return;
|
||
var selected = doc.CurrentSelection.SelectedItems;
|
||
if (selected.Count == 1)
|
||
ExpandAndSelect(selected.First());
|
||
}
|
||
```
|
||
|
||
### 11.3 展开路径(沿 ModelItem.Parent 链)
|
||
|
||
```csharp
|
||
private void ExpandAndSelect(ModelItem item)
|
||
{
|
||
// 1. 沿 Parent 链构建祖先列表(根 → 目标)
|
||
// 2. 在 rootNodes 中按 ModelItem.Equals 匹配根节点
|
||
// 3. 逐层向下:有占位 → LazyLoadChildren / 无占位 → BuildNode 单点创建
|
||
// 4. SelectTreeNode 选中最终节点
|
||
}
|
||
```
|
||
|
||
不使用 IndexPath 做运行时导航。IndexPath 仅在保存/加载别名时用于持久化(`CreatePathId` / `ResolvePathId`)。
|
||
|
||
---
|
||
|
||
## 12. 线程安全
|
||
|
||
**所有 Navisworks API 调用必须在主 STA 线程**(项目第 8 节硬约束)。
|
||
|
||
| 操作 | 线程 |
|
||
|------|------|
|
||
| 遍历 ModelItem(构建树/生成别名) | 主线程(必须) |
|
||
| `Application.ActiveDocument` 访问 | 主线程(必须) |
|
||
| `OverridePermanentColor` | 主线程(必须) |
|
||
| `CurrentSelection.SelectedItems` | 主线程(必须) |
|
||
| 读/写 SQLite(AliasDataStore) | 可通过 `Task.Run` 在后台 + 锁保护 |
|
||
| JSON 解析/序列化 | 后台线程安全 |
|
||
| `Dispatcher.BeginInvoke` 延迟 UI 更新 | 主线程回调 |
|
||
|
||
大模型优化:
|
||
- 树构建分批次:前 200 个根节点立即构建,后续通过 `Dispatcher.BeginInvoke(DispatcherPriority.Background, ...)` 分批追加
|
||
- 首屏只加载 Viewport 内可见项(`VirtualizingPanel` 已覆盖)
|
||
- 展开时才真正递归子节点(占位符机制)
|
||
|
||
---
|
||
|
||
## 13. 文件组织与集成
|
||
|
||
### 13.1 目录结构
|
||
|
||
```
|
||
src/
|
||
├── Core/
|
||
│ └── AliasTree/
|
||
│ ├── AliasTreePlugin.cs # DockPanePlugin
|
||
│ ├── AliasNodeIdentity.cs # 复合节点标识
|
||
│ └── AliasDataStore.cs # CRUD + SQLite
|
||
├── UI/
|
||
│ └── WPF/
|
||
│ ├── Views/
|
||
│ │ ├── AliasTreeControl.xaml
|
||
│ │ ├── AliasTreeControl.xaml.cs
|
||
│ │ ├── BatchAliasDialog.xaml
|
||
│ │ └── BatchAliasDialog.xaml.cs
|
||
│ └── ViewModels/
|
||
│ └── AliasNodeViewModel.cs # 单节点 VM
|
||
```
|
||
|
||
### 13.2 命名空间
|
||
|
||
- `NavisworksTransport.Core.AliasTree` — 插件入口、数据层
|
||
- `NavisworksTransport.UI.WPF.Views` — XAML 视图
|
||
- `NavisworksTransport.UI.WPF.ViewModels` — ViewModel
|
||
|
||
### 13.3 插件注册
|
||
|
||
```csharp
|
||
// src/Core/AliasTree/AliasTreePlugin.cs
|
||
[Plugin("NavisworksTransport.AliasTree", "Tian",
|
||
DisplayName = "别名导航树",
|
||
ToolTip = "自定义节点别名管理面板")]
|
||
[DockPanePlugin(300, 500, FixedSize = false)]
|
||
public class AliasTreePlugin : DockPanePlugin
|
||
{
|
||
public override Control CreateControlPane()
|
||
{
|
||
ElementHost eh = new ElementHost { AutoSize = true };
|
||
eh.Child = new AliasTreeControl();
|
||
eh.CreateControl();
|
||
return eh;
|
||
}
|
||
|
||
public override void DestroyControlPane(Control pane)
|
||
{
|
||
if (pane is ElementHost eh && eh.Child is AliasTreeControl ctrl)
|
||
ctrl.Cleanup();
|
||
pane?.Dispose();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 13.4 用户操作步骤
|
||
|
||
1. 启动 Navisworks → 打开模型
|
||
2. **View → Docking Windows → 别名导航树** 打开面板
|
||
3. 手动拖动面板,dock 到内置 Selection Tree 下方
|
||
4. 选中节点后,在顶部输入框或内联编辑打别名
|
||
5. 别名自动持久化到 `DocName.db`,下次打开模型自动恢复
|
||
|
||
---
|
||
|
||
## 14. 实施步骤(按优先级)
|
||
|
||
### 阶段 1:数据层(2 天)
|
||
|
||
1. ✅ `AliasNodeIdentity` — 复合标识 + 测试
|
||
2. ✅ `AliasDataStore` — CRUD + 集成到 PathDatabase.Connection
|
||
3. ✅ PathDatabase 新增 `AliasMap` 表 + 迁移逻辑
|
||
4. ✅ 单元测试:identity 生成、去重、回溯定位
|
||
|
||
### 阶段 2:VM + TreeView 骨架(2 天)
|
||
|
||
5. ✅ `AliasNodeViewModel` — INotifyPropertyChanged
|
||
6. ✅ `AliasTreeControl.xaml` — TreeView + 工具栏布局
|
||
7. ✅ 延迟加载(占位节点 + Expanded 事件)
|
||
8. ✅ 顶部工具栏(当前名 + 输入框 + 保存)
|
||
|
||
### 阶段 3:编辑功能(1.5 天)
|
||
|
||
9. ✅ 内联编辑(文本框 + 铅笔按钮)
|
||
10. ✅ 右键菜单(编辑/清除/聚焦/复制原名)
|
||
11. ✅ `BatchAliasDialog` — 四种批量模式
|
||
12. ✅ `AliasGenerator` — 前缀生成(复用参考方案逻辑)
|
||
|
||
### 阶段 4:联动 + 高亮(1.5 天)
|
||
|
||
13. ✅ 点击别名树 → 选中模型(防循环)
|
||
14. ✅ 选中模型 → 展开别名树并高亮
|
||
15. ✅ 别名节点 3D 颜色高亮 + 清别名时清除
|
||
|
||
### 阶段 5:导入导出 + 清理(1 天)
|
||
|
||
16. ✅ JSON 导出
|
||
17. ✅ JSON 导入(合并模式)
|
||
18. ✅ 孤儿别名检测 + 清理
|
||
19. ✅ Cleanup 方法
|
||
|
||
### 阶段 6:集成测试(1 天)
|
||
|
||
20. ✅ 大模型性能测试(10 万+ 节点)
|
||
21. ✅ 文档切换生命周期测试
|
||
22. ✅ 同名字兄弟节点去重测试
|
||
23. ✅ 回归:不影响现有物流分类/碰撞检测
|
||
|
||
---
|
||
|
||
## 15. 风险与已知限制
|
||
|
||
| 风险 | 缓解 |
|
||
|------|------|
|
||
| 父链路径在模型结构变化后断裂 | 孤儿别名标记 + 清理功能 |
|
||
| 同名兄弟节点计数漂移 | 同级插入/删除后索引可能偏移,需提供「刷新别名索引」 |
|
||
| `Children.Any()` 触发集合枚举 | 用 `Children.Count > 0` 代替,或从 COM API 取 path 来判空 |
|
||
| 大模型首次加载 TreeView 卡顿 | 分批构建 + Dispatcher.Background + 虚拟化 |
|
||
| Navisworks 不支持编程控制 dock 位置 | 首次使用文档中明确引导用户手动拖动面板 |
|
||
|
||
---
|
||
|
||
## 16. 与现有功能的隔离
|
||
|
||
- 不修改任何现有 `PathPlanningManager` / `PathDatabase` 核心逻辑(只在 DB 中加表)
|
||
- 不侵入 `LogisticsControlPanel`(独立 `DockPanePlugin`)
|
||
- 别名高亮使用独立 `ModelHighlightHelper` category,不干扰物流分类高亮
|
||
- `AliasDataStore` 只读 `PathDatabase.Connection`,不持有自己的连接
|