- AliasNodeIdentity: stable composite node identifier (path + sibling index) - AliasDataStore: CRUD operations on SQLite AliasMap table - PathDatabase: add AliasMap table creation + index - Unit tests: 30 tests for identity parsing/equality, store CRUD/batch/import - Both projects compile successfully
724 lines
28 KiB
Markdown
724 lines
28 KiB
Markdown
原生树无法扩展,自定义别名树面板就是唯一可靠的载体。在那个面板里,你可以完全自由地定制右键菜单,比如:
|
||
表格 菜单项 功能
|
||
编辑别名 就地改名
|
||
清除别名 恢复原名
|
||
在视图中聚焦 3D 定位
|
||
批量设置前缀 对当前节点及其子节点统一加前缀
|
||
导出此分支 只导出当前节点下的别名 CSV
|
||
|
||
实际工作流画面
|
||
想象你的屏幕布局:
|
||
plain
|
||
┌─────────────────┬──────────────────┐
|
||
│ 内置 Selection │ 3D 模型视图 │
|
||
│ Tree │ │
|
||
│ (原始名字) │ [框选操作区] │
|
||
│ │ │
|
||
├─────────────────┤ │
|
||
│ 别名导航树 │ │
|
||
│ (自定义名字) │ │
|
||
│ │ │
|
||
└─────────────────┴──────────────────┘
|
||
找东西:用户在别名树里看到"二楼-空调水管-123",一眼认出。
|
||
点一下:别名树高亮,3D 视图聚焦到那根管子,内置 Selection Tree 也同步跳转到对应节点。
|
||
反过来:用户在 3D 视图里框选一根管子,内置树亮了,别名树也自动展开高亮显示"二楼-空调水管-123"。
|
||
用户完全不需要记住原始命名规则,两个面板互相印证。
|
||
一个小提醒:首次加载性能
|
||
如果模型有 10万+ 节点,且你需要一次性把所有别名加载进字典,建议在插件初始化时做一个后台线程 + 进度条:
|
||
csharp
|
||
// 异步构建别名字典,避免 UI 卡死
|
||
private async void BuildAliasMapAsync()
|
||
{
|
||
await Task.Run(() =>
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
// 遍历所有节点生成 Guid->Alias 映射
|
||
// ...
|
||
});
|
||
RefreshTree();
|
||
}
|
||
结论
|
||
这个"双面板联动"方案是性价比最高的落地方式:
|
||
没有破坏 Navisworks 原生结构(稳定、升级兼容)
|
||
没有重写复杂树控件(100 多行代码搞定)
|
||
** alias 数据完全由你掌控**(Excel、数据库、规则生成均可)
|
||
用户体验几乎等同于原生支持别名
|
||
|
||
下面是可直接编译的完整方案,共 6 个文件。核心思路:
|
||
AliasTreePane:Navisworks DockPane 外壳。
|
||
AliasTreeControl:WPF 别名树面板,支持悬停铅笔编辑、顶部快捷栏、右键菜单、CSV 导入导出、自动生成前缀别名。
|
||
双向联动:点击别名树选中模型;在 3D 视图/内置树选中后,别名树自动展开并高亮。
|
||
延迟加载:大模型下只加载可见节点,展开时才填充子节点。
|
||
|
||
1. AliasNode.cs(数据模型)
|
||
|
||
using Autodesk.Navisworks.Api;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.ComponentModel;
|
||
|
||
namespace AliasTree
|
||
{
|
||
public class AliasNode : INotifyPropertyChanged
|
||
{
|
||
public ModelItem ModelItem { get; set; }
|
||
public Guid Guid => ModelItem?.InstanceGuid ?? Guid.Empty;
|
||
|
||
private string _alias;
|
||
public string Alias
|
||
{
|
||
get => _alias;
|
||
set
|
||
{
|
||
_alias = value;
|
||
OnPropertyChanged(nameof(Alias));
|
||
OnPropertyChanged(nameof(DisplayText));
|
||
}
|
||
}
|
||
|
||
private bool _isEditing;
|
||
public bool IsEditing
|
||
{
|
||
get => _isEditing;
|
||
set { _isEditing = value; OnPropertyChanged(nameof(IsEditing)); }
|
||
}
|
||
|
||
public string DisplayText => string.IsNullOrEmpty(Alias) ? (ModelItem?.DisplayName ?? "未命名") : Alias;
|
||
|
||
public List<<AliasNode> Children { get; set; } = new List<<AliasNode>();
|
||
|
||
public event PropertyChangedEventHandler PropertyChanged;
|
||
protected void OnPropertyChanged(string name) =>
|
||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||
}
|
||
}
|
||
|
||
2. AliasTreePlugin.cs(插件入口)
|
||
|
||
using Autodesk.Navisworks.Api.Plugins;
|
||
|
||
namespace AliasTree
|
||
{
|
||
[Plugin("AliasTreePane", "YourCompany", DisplayName = "别名导航树")]
|
||
[DockPanePlugin(400, 600, FixedSize = false)]
|
||
public class AliasTreePlugin : DockPanePlugin
|
||
{
|
||
public override Pane CreatePane() => new AliasTreePane();
|
||
}
|
||
}
|
||
|
||
3. AliasTreePane.cs(Pane 外壳)
|
||
|
||
using Autodesk.Navisworks.Api.Plugins;
|
||
|
||
namespace AliasTree
|
||
{
|
||
public class AliasTreePane : Pane
|
||
{
|
||
public AliasTreePane()
|
||
{
|
||
this.Content = new AliasTreeControl();
|
||
}
|
||
}
|
||
}
|
||
|
||
4. AliasTreeControl.xaml(界面布局)
|
||
|
||
<UserControl x:Class="AliasTree.AliasTreeControl"
|
||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||
xmlns:local="clr-namespace:AliasTree">
|
||
<DockPanel>
|
||
<!-- 顶部快捷栏 -->
|
||
<ToolBar DockPanel.Dock="Top" Height="32">
|
||
<TextBlock Text="当前选中:" VerticalAlignment="Center" Margin="4,0"/>
|
||
<TextBlock x:Name="TbCurrentName" Text="无" VerticalAlignment="Center"
|
||
FontWeight="Bold" Margin="4,0" MaxWidth="150" TextTrimming="CharacterEllipsis"/>
|
||
<Separator/>
|
||
<TextBox x:Name="TxtQuickAlias" Width="120" ToolTip="输入别名后回车"
|
||
KeyDown="TxtQuickAlias_KeyDown"/>
|
||
<Button Content="✓ 保存" Click="BtnQuickSave_Click" ToolTip="为当前选中节点保存别名"/>
|
||
<Separator/>
|
||
<Button Content="导出CSV" Click="BtnExport_Click"/>
|
||
<Button Content="导入CSV" Click="BtnImport_Click"/>
|
||
<Separator/>
|
||
<Button Content="自动生成" Click="BtnAutoGen_Click" ToolTip="按父节点路径自动生成别名"/>
|
||
</ToolBar>
|
||
|
||
<!-- 别名树 -->
|
||
<TreeView x:Name="AliasTreeView"
|
||
VirtualizingPanel.IsVirtualizing="True"
|
||
VirtualizingPanel.VirtualizationMode="Recycling">
|
||
<TreeView.ItemTemplate>
|
||
<HierarchicalDataTemplate DataType="{x:Type local:AliasNode}" ItemsSource="{Binding Children}">
|
||
<StackPanel Orientation="Horizontal" Margin="0,1">
|
||
<!-- 显示模式:无别名时灰色斜体显示原名 -->
|
||
<TextBlock Text="{Binding DisplayText}" VerticalAlignment="Center" MinWidth="60">
|
||
<TextBlock.Style>
|
||
<Style TargetType="TextBlock">
|
||
<Setter Property="Foreground" Value="Black"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding Alias}" Value="">
|
||
<Setter Property="Foreground" Value="Gray"/>
|
||
<Setter Property="FontStyle" Value="Italic"/>
|
||
</DataTrigger>
|
||
<DataTrigger Binding="{Binding Alias}" Value="{x:Null}">
|
||
<Setter Property="Foreground" Value="Gray"/>
|
||
<Setter Property="FontStyle" Value="Italic"/>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</TextBlock.Style>
|
||
</TextBlock>
|
||
|
||
<!-- 编辑模式:默认隐藏,IsEditing=True 时显示 -->
|
||
<TextBox Text="{Binding Alias, UpdateSourceTrigger=PropertyChanged}"
|
||
Width="120" Visibility="Collapsed" VerticalAlignment="Center"
|
||
KeyDown="AliasTextBox_KeyDown" LostFocus="AliasTextBox_LostFocus">
|
||
<TextBox.Style>
|
||
<Style TargetType="TextBox">
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding IsEditing}" Value="True">
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</TextBox.Style>
|
||
</TextBox>
|
||
|
||
<!-- 铅笔图标:鼠标悬停该行时显示 -->
|
||
<Button Content="✎" Margin="4,0,0,0" Padding="2,0"
|
||
Click="BtnEdit_Click" Tag="{Binding}" VerticalAlignment="Center">
|
||
<Button.Style>
|
||
<Style TargetType="Button">
|
||
<Setter Property="Visibility" Value="Collapsed"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=IsMouseOver}" Value="True">
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Button.Style>
|
||
</Button>
|
||
</StackPanel>
|
||
</HierarchicalDataTemplate>
|
||
</TreeView.ItemTemplate>
|
||
|
||
<TreeView.ItemContainerStyle>
|
||
<Style TargetType="TreeViewItem">
|
||
<Setter Property="IsExpanded" Value="False"/>
|
||
<EventSetter Event="Expanded" Handler="TreeViewItem_Expanded"/>
|
||
<EventSetter Event="Selected" Handler="TreeViewItem_Selected"/>
|
||
<Setter Property="ContextMenu">
|
||
<Setter.Value>
|
||
<ContextMenu>
|
||
<MenuItem Header="编辑别名" Click="CtxEdit_Click"/>
|
||
<MenuItem Header="清除别名" Click="CtxClear_Click"/>
|
||
<Separator/>
|
||
<MenuItem Header="在视图中聚焦" Click="CtxFocus_Click"/>
|
||
</ContextMenu>
|
||
</Setter.Value>
|
||
</Setter>
|
||
</Style>
|
||
</TreeView.ItemContainerStyle>
|
||
</TreeView>
|
||
</DockPanel>
|
||
</UserControl>
|
||
|
||
5. AliasTreeControl.xaml.cs(核心逻辑)
|
||
|
||
using Autodesk.Navisworks.Api;
|
||
using Microsoft.Win32;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using System.Windows.Input;
|
||
using System.Windows.Media;
|
||
|
||
namespace AliasTree
|
||
{
|
||
public partial class AliasTreeControl : UserControl
|
||
{
|
||
private Dictionary<<Guid, string> _aliasMap = new Dictionary<<Guid, string>();
|
||
private List<<AliasNode> _rootNodes = new List<<AliasNode>();
|
||
|
||
public AliasTreeControl()
|
||
{
|
||
InitializeComponent();
|
||
AliasTreeView.ItemsSource = _rootNodes;
|
||
|
||
Autodesk.Navisworks.Api.Application.MainDocumentChanged += (s, e) =>
|
||
{
|
||
HookSelection();
|
||
RefreshTree();
|
||
};
|
||
|
||
HookSelection();
|
||
RefreshTree();
|
||
}
|
||
|
||
private void HookSelection()
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc != null)
|
||
doc.CurrentSelection.Changed += OnNavisSelectionChanged;
|
||
}
|
||
|
||
// ==================== 树构建与延迟加载 ====================
|
||
private void RefreshTree()
|
||
{
|
||
_rootNodes.Clear();
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc == null) return;
|
||
|
||
foreach (var root in doc.Models.RootItems)
|
||
{
|
||
var node = CreateAliasNode(root, true);
|
||
if (node != null) _rootNodes.Add(node);
|
||
}
|
||
}
|
||
|
||
private AliasNode CreateAliasNode(ModelItem item, bool lazyLoad)
|
||
{
|
||
if (item == null) return null;
|
||
|
||
var node = new AliasNode
|
||
{
|
||
ModelItem = item,
|
||
Alias = GetAlias(item.InstanceGuid)
|
||
};
|
||
|
||
if (item.Children.Any())
|
||
{
|
||
if (lazyLoad)
|
||
{
|
||
// 添加占位节点,使 TreeView 显示展开箭头
|
||
node.Children.Add(new AliasNode { ModelItem = null, Alias = "Loading..." });
|
||
}
|
||
else
|
||
{
|
||
foreach (var child in item.Children)
|
||
{
|
||
var childNode = CreateAliasNode(child, true);
|
||
if (childNode != null) node.Children.Add(childNode);
|
||
}
|
||
}
|
||
}
|
||
return node;
|
||
}
|
||
|
||
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
|
||
{
|
||
if (e.OriginalSource is TreeViewItem tvi && tvi.DataContext is AliasNode node)
|
||
{
|
||
// 检查是否是占位节点(尚未加载真实子节点)
|
||
if (node.Children.Count == 1 && node.Children[0].ModelItem == null)
|
||
{
|
||
node.Children.Clear();
|
||
if (node.ModelItem != null)
|
||
{
|
||
foreach (var child in node.ModelItem.Children)
|
||
{
|
||
var childNode = CreateAliasNode(child, true);
|
||
if (childNode != null) node.Children.Add(childNode);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 双向联动 ====================
|
||
private void TreeViewItem_Selected(object sender, RoutedEventArgs e)
|
||
{
|
||
if (e.OriginalSource is TreeViewItem tvi && tvi.DataContext is AliasNode node)
|
||
{
|
||
if (node.ModelItem == null) return;
|
||
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc == null) return;
|
||
|
||
var coll = new ModelItemCollection();
|
||
coll.Add(node.ModelItem);
|
||
doc.CurrentSelection.SelectedItems = coll;
|
||
}
|
||
}
|
||
|
||
private void OnNavisSelectionChanged(object sender, EventArgs e)
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc == null) return;
|
||
|
||
var selected = doc.CurrentSelection.SelectedItems;
|
||
|
||
Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
if (selected.Count == 1)
|
||
{
|
||
var item = selected.First();
|
||
TbCurrentName.Text = item.DisplayName;
|
||
TbCurrentName.Tag = item;
|
||
ExpandAndHighlight(item.InstanceGuid);
|
||
}
|
||
else
|
||
{
|
||
TbCurrentName.Text = selected.Count > 1 ? $"({selected.Count} 个选中)" : "无";
|
||
TbCurrentName.Tag = null;
|
||
}
|
||
}));
|
||
}
|
||
|
||
private void ExpandAndHighlight(Guid guid)
|
||
{
|
||
// 找到路径
|
||
var path = new List<<AliasNode>();
|
||
if (!FindPath(_rootNodes, guid, path)) return;
|
||
|
||
// 逐层展开并加载数据
|
||
ItemsControl current = AliasTreeView;
|
||
foreach (var node in path)
|
||
{
|
||
// 强制加载延迟加载的节点数据
|
||
if (node.Children.Count == 1 && node.Children[0].ModelItem == null)
|
||
{
|
||
node.Children.Clear();
|
||
if (node.ModelItem != null)
|
||
{
|
||
foreach (var child in node.ModelItem.Children)
|
||
{
|
||
var childNode = CreateAliasNode(child, true);
|
||
if (childNode != null) node.Children.Add(childNode);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (current is TreeViewItem tvi)
|
||
{
|
||
tvi.IsExpanded = true;
|
||
tvi.UpdateLayout();
|
||
}
|
||
|
||
current = (TreeViewItem)current.ItemContainerGenerator.ContainerFromItem(node);
|
||
if (current == null) break; // 虚拟化下可能未生成,中断
|
||
}
|
||
|
||
// 高亮目标
|
||
if (current is TreeViewItem target && target.DataContext is AliasNode targetNode && targetNode.Guid == guid)
|
||
{
|
||
target.IsSelected = true;
|
||
target.BringIntoView();
|
||
}
|
||
}
|
||
|
||
private bool FindPath(List<<AliasNode> nodes, Guid guid, List<<AliasNode> path)
|
||
{
|
||
foreach (var n in nodes)
|
||
{
|
||
path.Add(n);
|
||
if (n.Guid == guid) return true;
|
||
if (FindPath(n.Children, guid, path)) return true;
|
||
path.RemoveAt(path.Count - 1);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ==================== 内联编辑(铅笔图标) ====================
|
||
private void BtnEdit_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if ((sender as Button)?.Tag is not AliasNode node) return;
|
||
ClearAllEditing(_rootNodes);
|
||
node.IsEditing = true;
|
||
|
||
Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
var container = AliasTreeView.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
|
||
if (container == null) return;
|
||
var txt = FindVisualChild<TextBox>(container);
|
||
if (txt != null)
|
||
{
|
||
txt.Focus();
|
||
txt.SelectAll();
|
||
}
|
||
}), System.Windows.Threading.DispatcherPriority.Render);
|
||
}
|
||
|
||
private void AliasTextBox_KeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
if (e.Key == Key.Enter)
|
||
(sender as TextBox)?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
|
||
}
|
||
|
||
private void AliasTextBox_LostFocus(object sender, RoutedEventArgs e)
|
||
{
|
||
if ((sender as TextBox)?.DataContext is not AliasNode node) return;
|
||
SaveAlias(node.Guid, node.Alias);
|
||
node.IsEditing = false;
|
||
node.Alias = GetAlias(node.Guid); // 同步最终值
|
||
}
|
||
|
||
// ==================== 右键菜单 ====================
|
||
private void CtxEdit_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (AliasTreeView.SelectedItem is AliasNode node)
|
||
BtnEdit_Click(new Button { Tag = node }, null);
|
||
}
|
||
|
||
private void CtxClear_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (AliasTreeView.SelectedItem is not AliasNode node) return;
|
||
SaveAlias(node.Guid, null);
|
||
node.Alias = null;
|
||
}
|
||
|
||
private void CtxFocus_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (AliasTreeView.SelectedItem is not AliasNode node || node.ModelItem == null) return;
|
||
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc == null) return;
|
||
|
||
var coll = new ModelItemCollection();
|
||
coll.Add(node.ModelItem);
|
||
doc.CurrentSelection.SelectedItems = coll;
|
||
doc.ActiveView.FocusOnItem(node.ModelItem, false);
|
||
}
|
||
|
||
// ==================== 顶部快捷栏 ====================
|
||
private void TxtQuickAlias_KeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
if (e.Key == Key.Enter) BtnQuickSave_Click(null, null);
|
||
}
|
||
|
||
private void BtnQuickSave_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (TbCurrentName.Tag is not ModelItem item) return;
|
||
if (string.IsNullOrWhiteSpace(TxtQuickAlias.Text)) return;
|
||
|
||
SaveAlias(item.InstanceGuid, TxtQuickAlias.Text.Trim());
|
||
TxtQuickAlias.Clear();
|
||
RefreshNodeDisplay(item.InstanceGuid);
|
||
}
|
||
|
||
// ==================== 自动生成别名 ====================
|
||
private void BtnAutoGen_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc == null) return;
|
||
|
||
var gen = new AliasGenerator();
|
||
var generated = gen.Generate(doc.Models.RootItems.FirstOrDefault(), "/");
|
||
|
||
foreach (var kv in generated)
|
||
_aliasMap[kv.Key] = kv.Value;
|
||
|
||
RefreshTree();
|
||
}
|
||
|
||
// ==================== CSV 导入导出 ====================
|
||
private void BtnExport_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var dlg = new SaveFileDialog { Filter = "CSV 文件|*.csv", Title = "导出别名映射" };
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
var lines = new List<string> { "Guid,OriginalName,Alias" };
|
||
foreach (var kv in _aliasMap)
|
||
{
|
||
var item = Autodesk.Navisworks.Api.Application.ActiveDocument?.Models.FindItemByGuid(kv.Key);
|
||
string orig = item?.DisplayName ?? "";
|
||
lines.Add($"{kv.Key},\"{orig}\",\"{kv.Value}\"");
|
||
}
|
||
File.WriteAllLines(dlg.FileName, lines);
|
||
MessageBox.Show($"已导出 {_aliasMap.Count} 条别名记录。", "导出完成",
|
||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||
}
|
||
|
||
private void BtnImport_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var dlg = new OpenFileDialog { Filter = "CSV 文件|*.csv", Title = "导入别名映射" };
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
int count = 0;
|
||
foreach (var line in File.ReadLines(dlg.FileName).Skip(1))
|
||
{
|
||
var parts = ParseCsvLine(line);
|
||
if (parts.Length >= 3 && Guid.TryParse(parts[0], out var guid))
|
||
{
|
||
string alias = parts[2].Trim('"');
|
||
if (!string.IsNullOrWhiteSpace(alias))
|
||
{
|
||
_aliasMap[guid] = alias;
|
||
count++;
|
||
}
|
||
}
|
||
}
|
||
RefreshTree();
|
||
MessageBox.Show($"已导入 {count} 条别名记录。", "导入完成",
|
||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||
}
|
||
|
||
// ==================== 辅助方法 ====================
|
||
private string GetAlias(Guid guid) =>
|
||
_aliasMap.ContainsKey(guid) ? _aliasMap[guid] : null;
|
||
|
||
private void SaveAlias(Guid guid, string alias)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(alias))
|
||
{
|
||
if (_aliasMap.ContainsKey(guid)) _aliasMap.Remove(guid);
|
||
}
|
||
else
|
||
{
|
||
_aliasMap[guid] = alias;
|
||
}
|
||
}
|
||
|
||
private void RefreshNodeDisplay(Guid guid)
|
||
{
|
||
var node = FindNode(_rootNodes, guid);
|
||
if (node != null) node.Alias = GetAlias(guid);
|
||
}
|
||
|
||
private AliasNode FindNode(List<<AliasNode> nodes, Guid guid)
|
||
{
|
||
foreach (var n in nodes)
|
||
{
|
||
if (n.Guid == guid) return n;
|
||
var found = FindNode(n.Children, guid);
|
||
if (found != null) return found;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private void ClearAllEditing(List<<AliasNode> nodes)
|
||
{
|
||
foreach (var n in nodes)
|
||
{
|
||
n.IsEditing = false;
|
||
ClearAllEditing(n.Children);
|
||
}
|
||
}
|
||
|
||
private static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
|
||
{
|
||
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||
{
|
||
var child = VisualTreeHelper.GetChild(parent, i);
|
||
if (child is T t) return t;
|
||
var result = FindVisualChild<T>(child);
|
||
if (result != null) return result;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static string[] ParseCsvLine(string line)
|
||
{
|
||
var result = new List<string>();
|
||
bool inQuotes = false;
|
||
var sb = new System.Text.StringBuilder();
|
||
foreach (char c in line)
|
||
{
|
||
if (c == '"')
|
||
{
|
||
inQuotes = !inQuotes;
|
||
}
|
||
else if (c == ',' && !inQuotes)
|
||
{
|
||
result.Add(sb.ToString());
|
||
sb.Clear();
|
||
}
|
||
else
|
||
{
|
||
sb.Append(c);
|
||
}
|
||
}
|
||
result.Add(sb.ToString());
|
||
return result.ToArray();
|
||
}
|
||
}
|
||
}
|
||
|
||
6. AliasGenerator.cs(批量自动生成别名)
|
||
|
||
using Autodesk.Navisworks.Api;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace AliasTree
|
||
{
|
||
public class AliasGenerator
|
||
{
|
||
private Dictionary<<Guid, string> _result = new Dictionary<<Guid, string>();
|
||
private HashSet<string> _usedNames = new HashSet<string>();
|
||
|
||
public Dictionary<<Guid, string> Generate(ModelItem root, string separator = "/")
|
||
{
|
||
_result.Clear();
|
||
_usedNames.Clear();
|
||
if (root != null)
|
||
Traverse(root, "", separator, 0);
|
||
return _result;
|
||
}
|
||
|
||
private void Traverse(ModelItem item, string parentPath, string sep, int depth)
|
||
{
|
||
string rawName = item.DisplayName?.Trim() ?? "未命名";
|
||
|
||
// 一级节点保持原名;二级开始加父前缀
|
||
string alias = depth == 0
|
||
? rawName
|
||
: (string.IsNullOrEmpty(parentPath) ? rawName : $"{parentPath}{sep}{rawName}");
|
||
|
||
string uniqueAlias = MakeUnique(alias);
|
||
_result[item.InstanceGuid] = uniqueAlias;
|
||
|
||
// 下一级前缀:最多保留最近一级父名,避免路径无限膨胀
|
||
string nextPrefix = depth < 1 ? uniqueAlias : rawName;
|
||
|
||
foreach (var child in item.Children)
|
||
Traverse(child, nextPrefix, sep, depth + 1);
|
||
}
|
||
|
||
private string MakeUnique(string name)
|
||
{
|
||
if (!_usedNames.Contains(name))
|
||
{
|
||
_usedNames.Add(name);
|
||
return name;
|
||
}
|
||
|
||
int idx = 1;
|
||
string candidate;
|
||
do
|
||
{
|
||
candidate = $"{name}_{idx:D3}";
|
||
idx++;
|
||
} while (_usedNames.Contains(candidate));
|
||
|
||
_usedNames.Add(candidate);
|
||
return candidate;
|
||
}
|
||
}
|
||
}
|
||
|
||
编译与部署说明
|
||
|
||
| 项目 | 内容 |
|
||
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **项目类型** | Visual Studio → C# → 类库(.NET Framework) |
|
||
| **目标框架** | .NET Framework 4.8(与 Navisworks 版本匹配) |
|
||
| **引用 DLL** | `Autodesk.Navisworks.Api.dll`<br>`Autodesk.Navisworks.Automation.dll`<br>位置通常在:`C:\Program Files\Autodesk\Navisworks Manage 2024\` |
|
||
| **输出路径** | 编译后,将生成的 DLL 与 XAML/BAML 一起放入:<br>`%APPDATA%\Autodesk Navisworks Manage 2024\Plugins\AliasTree\` |
|
||
| **启动** | 重启 Navisworks,在 **View → Docking Windows** 中找到 **别名导航树** |
|
||
|
||
使用流程(假设一个"八个楼层"的模型节点)
|
||
打开插件面板,dock 到内置 Selection Tree 旁边。
|
||
在内置树或 3D 视图中点选一个名叫"楼层"的节点。
|
||
别名树自动高亮该节点,顶部工具栏显示原名。
|
||
任选一种方式打别名:
|
||
鼠标悬停别名树节点 → 点 ✎ 铅笔 → 就地输入"8F-办公层" → 回车。
|
||
看 3D 视图确认位置 → 在顶部输入框敲"8F-办公层" → 点 ✓ 保存。
|
||
右键节点 → 编辑别名。
|
||
如果八个子节点全叫"楼层",可点 自动生成 按钮,一键生成 楼层/楼层_001 … 楼层/楼层_007,你再精修成 1F大堂、2F办公 等。
|
||
工作完成后 导出 CSV,下次打开 Navisworks 时 导入 CSV 即可恢复所有别名。 |