diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs index 46d14b0a8..4dfabdc83 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs @@ -5,6 +5,7 @@ using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Conversion; using Speckle.Connectors.Common.Operations; +using Speckle.Converter.Navisworks.Helpers; using Speckle.Converter.Navisworks.Settings; using Speckle.Converters.Common; using Speckle.Objects.Data; @@ -194,12 +195,23 @@ HashSet processedPaths finalElements.Add(collection); break; default: - finalElements.Add(CreateNavisworksObject(kvp.Value)); + if (CreateNavisworksObject(kvp.Value) is { } navisworksObject) + { + finalElements.Add(navisworksObject); + } + break; } } } + private (string name, string path) GetContext(string applicationId) + { + var modelItem = elementSelectionService.GetModelItemFromPath(applicationId); + var context = HierarchyHelper.ExtractContext(modelItem); + return (context.Name, context.Path); + } + /// /// Processes and adds any remaining non-grouped elements. /// @@ -207,30 +219,45 @@ HashSet processedPaths /// Handles both Collection and Base type elements differently. /// Only processes elements that weren't handled in grouped processing. /// - private NavisworksObject CreateNavisworksObject(string groupKey, List siblingBases) => - new() + private NavisworksObject CreateNavisworksObject(string groupKey, List siblingBases) + { + (string name, string path) = GetContext(groupKey); + + return new NavisworksObject { - name = elementSelectionService.GetModelItemFromPath(groupKey).DisplayName ?? string.Empty, + name = name, displayValue = siblingBases.SelectMany(b => b["displayValue"] as List ?? []).ToList(), properties = siblingBases.First()["properties"] as Dictionary ?? [], units = converterSettings.Current.Derived.SpeckleUnits, - applicationId = groupKey + applicationId = groupKey, + ["path"] = path }; + } /// /// Creates a NavisworksObject from a single converted base. /// /// The converted Speckle Base object. /// A new NavisworksObject containing the converted data. - private NavisworksObject CreateNavisworksObject(Base convertedBase) => - new() + private NavisworksObject? CreateNavisworksObject(Base convertedBase) + { + if (convertedBase.applicationId == null) { - name = convertedBase["name"] as string ?? string.Empty, + return null; + } + + (string name, string path) = GetContext(convertedBase.applicationId); + + return new NavisworksObject + { + name = name, displayValue = convertedBase["displayValue"] as List ?? [], properties = convertedBase["properties"] as Dictionary ?? [], units = converterSettings.Current.Derived.SpeckleUnits, - applicationId = convertedBase.applicationId + applicationId = convertedBase.applicationId, + ["path"] = path }; + } private Task AddProxiesToCollection( Collection rootCollection, diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/HierarchyHelper.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/HierarchyHelper.cs new file mode 100644 index 000000000..47113e2c8 --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/HierarchyHelper.cs @@ -0,0 +1,59 @@ +namespace Speckle.Converter.Navisworks.Helpers; + +/// +/// Helper class for extracting hierarchical context from Navisworks model items in a single traversal. +/// +public static class HierarchyHelper +{ + private const char PATH_DELIMITER = '>'; + + /// + /// Extracts the meaningful name and path from a ModelItem in a single traversal. + /// + public static (string Name, string Path) ExtractContext(NAV.ModelItem modelItem) + { + if (modelItem == null) + { + throw new ArgumentNullException(nameof(modelItem)); + } + + var ancestors = new List(); + var meaningfulName = string.Empty; + var current = modelItem; + + // Start with the root document name if available + if (modelItem.HasModel && !string.IsNullOrEmpty(modelItem.Model.FileName)) + { + ancestors.Add(Path.GetFileNameWithoutExtension(modelItem.Model.FileName)); + } + + // Traverse up the tree once, collecting both name and path information + while (current != null) + { + if (IsMeaningfulNode(current)) + { + // First meaningful name we find is our object name (if we haven't found one yet) + if (string.IsNullOrEmpty(meaningfulName)) + { + meaningfulName = current.DisplayName; + } + // Add to ancestors if it's not a duplicate + else if (ancestors.Count == 0 || ancestors.Last() != current.DisplayName) + { + ancestors.Add(current.DisplayName); + } + } + current = current.Parent; + } + + // Build path excluding the name we found + ancestors.Reverse(); + + var path = string.Join($" {PATH_DELIMITER} ", ancestors); + + return (meaningfulName, path); + } + + private static bool IsMeaningfulNode(NAV.ModelItem item) => + !string.IsNullOrEmpty(item.DisplayName) && (!item.HasGeometry || !string.IsNullOrEmpty(item.ClassDisplayName)); +} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems index 121b8a9a4..d791e58b9 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems @@ -22,6 +22,7 @@ +