NavisworksTransport/src/Core/AliasTree/AliasNodeIdentity.cs
tian 4feb12f6b9 refactor: AliasNodeIdentity dual-field + reuse ModelItemAnalysisHelper
- 新增 IndexPath(COM 树索引路径),复用 ModelItemAnalysisHelper.GetModelItemIndexPaths
- 保留 DisplayPath(父链 DisplayName 拼接)作为降级匹配
- 序列化格式:"IndexPath||DisplayPath",向后兼容纯 DisplayPath
- FindModelItemByNodeKey: IndexPath 优先定位,DisplayPath 降级
- BuildNode: 新增旧格式兼容查找
- 单元测试全面更新覆盖新/旧格式、双字段解析、前缀匹配
2026-05-31 12:52:26 +08:00

212 lines
7.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NavisworksTransport.Core.AliasTree
{
/// <summary>
/// 稳定的复合节点标识 — 替代 InstanceGuid 用于持久化。
///
/// 双字段设计:
/// - <see cref="IndexPath"/>COM API 树索引路径(如 "0,2,5,1"
/// 复用 <see cref="ModelItemAnalysisHelper.GetModelItemIndexPaths"/> 生成。
/// 定位时优先使用,精度高、速度快。
/// - <see cref="DisplayPath"/>:父链 DisplayName 拼接路径(如 "模型/楼层/墙#0"
/// 可读可调试IndexPath 失效时作为降级定位手段。
///
/// 序列化格式ToKey "IndexPath||DisplayPath"
/// 示例: "0,2,5,1||模型/楼层/墙#0"
///
/// 向后兼容:不含 "||" 的 key 按旧格式(纯 DisplayPath解析。
/// </summary>
public readonly struct AliasNodeIdentity : IEquatable<AliasNodeIdentity>
{
/// <summary>COM 树索引路径(如 "0,2,5,1"),优先用于定位</summary>
public string IndexPath { get; }
/// <summary>父链 DisplayName 拼接路径 + 同级索引(如 "模型/楼层/墙#0"</summary>
public string DisplayPath { get; }
/// <summary>
/// 从全量信息构造
/// </summary>
public AliasNodeIdentity(string indexPath, string displayPath)
{
IndexPath = indexPath ?? string.Empty;
DisplayPath = displayPath ?? throw new ArgumentNullException(nameof(displayPath));
}
// ── 工厂方法 ──
/// <summary>
/// 从 ModelItem 构造复合标识。
/// 必须在主线程调用(涉及 Navisworks API + COM API
/// </summary>
public static AliasNodeIdentity FromModelItem(ModelItem item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
// 1. IndexPath — 复用 ModelItemAnalysisHelper
var indexPaths = ModelItemAnalysisHelper.GetModelItemIndexPaths(item);
string indexPath = indexPaths.FirstOrDefault() ?? string.Empty;
// 2. DisplayPath — 父链 DisplayName + 同级索引
var parts = new List<string>();
var current = item;
while (current != null)
{
parts.Insert(0, current.DisplayName ?? "?");
current = current.Parent;
}
string displayPath = string.Join("/", parts);
// 3. 同级同名节点去重索引
int siblingIndex = ComputeSiblingIndex(item);
string displayPathWithIndex = siblingIndex > 0
? $"{displayPath}#{siblingIndex}"
: displayPath;
return new AliasNodeIdentity(indexPath, displayPathWithIndex);
}
/// <summary>
/// 计算同名兄弟节点中的位置
/// </summary>
private static int ComputeSiblingIndex(ModelItem item)
{
var parent = item.Parent;
if (parent == null) return 0;
string targetName = item.DisplayName;
int matchIndex = 0;
foreach (var child in parent.Children)
{
if (child.Equals(item))
return matchIndex;
if (string.Equals(child.DisplayName, targetName, StringComparison.Ordinal))
matchIndex++;
}
return 0;
}
// ── 序列化 ──
/// <summary>
/// 序列化为 DB 主键 / JSON key
/// 格式: "IndexPath||DisplayPath"
/// </summary>
public string ToKey()
{
if (string.IsNullOrEmpty(IndexPath))
return DisplayPath;
return $"{IndexPath}||{DisplayPath}";
}
/// <summary>
/// 从 key 反序列化
/// </summary>
public static AliasNodeIdentity FromKey(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("key 不能为空", nameof(key));
// 检查是否有双字段分隔符
int separatorIndex = key.IndexOf("||", StringComparison.Ordinal);
if (separatorIndex >= 0)
{
string indexPath = key.Substring(0, separatorIndex);
string displayPath = key.Substring(separatorIndex + 2);
if (string.IsNullOrEmpty(displayPath))
throw new FormatException($"DisplayPath 为空: {key}");
return new AliasNodeIdentity(indexPath, displayPath);
}
// 向后兼容:纯 DisplayPath 格式(旧数据)
return new AliasNodeIdentity(string.Empty, key);
}
/// <summary>
/// 提前验证 key 字符串格式是否合法
/// </summary>
public static bool TryParseKey(string key, out AliasNodeIdentity identity)
{
identity = default;
if (string.IsNullOrEmpty(key)) return false;
try
{
identity = FromKey(key);
return true;
}
catch
{
return false;
}
}
// ── 查询辅助 ──
/// <summary>
/// 获取 DisplayPath 部分中的 HierarchicalPath去掉尾部 #索引)
/// </summary>
public string GetHierarchicalPath()
{
int hashIdx = DisplayPath.LastIndexOf('#');
return hashIdx > 0 ? DisplayPath.Substring(0, hashIdx) : DisplayPath;
}
/// <summary>
/// 获取 DisplayPath 部分中的 SiblingIndex
/// </summary>
public int GetSiblingIndex()
{
int hashIdx = DisplayPath.LastIndexOf('#');
if (hashIdx < 0 || hashIdx >= DisplayPath.Length - 1) return 0;
string indexStr = DisplayPath.Substring(hashIdx + 1);
int.TryParse(indexStr, out int idx);
return idx;
}
/// <summary>
/// 检查 DisplayPath 是否以指定前缀开头(用于"清除某分支下全部别名"
/// </summary>
public bool HasPathPrefix(string prefix)
{
return GetHierarchicalPath().StartsWith(prefix, StringComparison.Ordinal);
}
// ── 值语义 ──
public bool Equals(AliasNodeIdentity other)
{
return string.Equals(IndexPath, other.IndexPath, StringComparison.Ordinal)
&& string.Equals(DisplayPath, other.DisplayPath, StringComparison.Ordinal);
}
public override bool Equals(object obj)
=> obj is AliasNodeIdentity other && Equals(other);
public override int GetHashCode()
{
unchecked
{
return (IndexPath?.GetHashCode() ?? 0 * 397) ^ (DisplayPath?.GetHashCode() ?? 0);
}
}
public static bool operator ==(AliasNodeIdentity left, AliasNodeIdentity right)
=> left.Equals(right);
public static bool operator !=(AliasNodeIdentity left, AliasNodeIdentity right)
=> !left.Equals(right);
public override string ToString() => ToKey();
}
}