- 新增 IndexPath(COM 树索引路径),复用 ModelItemAnalysisHelper.GetModelItemIndexPaths - 保留 DisplayPath(父链 DisplayName 拼接)作为降级匹配 - 序列化格式:"IndexPath||DisplayPath",向后兼容纯 DisplayPath - FindModelItemByNodeKey: IndexPath 优先定位,DisplayPath 降级 - BuildNode: 新增旧格式兼容查找 - 单元测试全面更新覆盖新/旧格式、双字段解析、前缀匹配
212 lines
7.2 KiB
C#
212 lines
7.2 KiB
C#
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();
|
||
}
|
||
}
|