diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksBasicConnectorBinding.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksBasicConnectorBinding.cs index 8fec2b940..a52feef90 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksBasicConnectorBinding.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksBasicConnectorBinding.cs @@ -6,32 +6,22 @@ using Speckle.Sdk; namespace Speckle.Connector.Navisworks.Bindings; -public class NavisworksBasicConnectorBinding : IBasicConnectorBinding +public class NavisworksBasicConnectorBinding( + IBrowserBridge parent, + DocumentModelStore store, + ISpeckleApplication speckleApplication +) : IBasicConnectorBinding { public string Name => "baseBinding"; - public IBrowserBridge Parent { get; } - public BasicConnectorBindingCommands Commands { get; } + public IBrowserBridge Parent { get; } = parent; - private readonly DocumentModelStore _store; - private readonly ISpeckleApplication _speckleApplication; + public BasicConnectorBindingCommands Commands { get; } = new(parent); - public NavisworksBasicConnectorBinding( - IBrowserBridge parent, - DocumentModelStore store, - ISpeckleApplication speckleApplication - ) - { - Parent = parent; - _store = store; - _speckleApplication = speckleApplication; - Commands = new BasicConnectorBindingCommands(parent); - } + public string GetSourceApplicationName() => speckleApplication.Slug; - public string GetSourceApplicationName() => _speckleApplication.Slug; + public string GetSourceApplicationVersion() => speckleApplication.HostApplicationVersion; - public string GetSourceApplicationVersion() => _speckleApplication.HostApplicationVersion; - - public string GetConnectorVersion() => _speckleApplication.SpeckleVersion; + public string GetConnectorVersion() => speckleApplication.SpeckleVersion; public DocumentInfo? GetDocumentInfo() => NavisworksApp.ActiveDocument is null || NavisworksApp.ActiveDocument.Models.Count == 0 @@ -42,15 +32,15 @@ public class NavisworksBasicConnectorBinding : IBasicConnectorBinding NavisworksApp.ActiveDocument.GetHashCode().ToString() ); - public DocumentModelStore GetDocumentState() => _store; + public DocumentModelStore GetDocumentState() => store; - public void AddModel(ModelCard model) => _store.AddModel(model); + public void AddModel(ModelCard model) => store.AddModel(model); - public void UpdateModel(ModelCard model) => _store.UpdateModel(model); + public void UpdateModel(ModelCard model) => store.UpdateModel(model); - public void RemoveModel(ModelCard model) => _store.RemoveModel(model); + public void RemoveModel(ModelCard model) => store.RemoveModel(model); - public void RemoveModels(List models) => _store.RemoveModels(models); + public void RemoveModels(List models) => store.RemoveModels(models); public Task HighlightModel(string modelCardId) => Task.CompletedTask; diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSelectionBinding.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSelectionBinding.cs index c0579ff8a..71854ff61 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSelectionBinding.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSelectionBinding.cs @@ -1,6 +1,6 @@ -using Speckle.Connector.Navisworks.Services; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Converter.Navisworks.Services; namespace Speckle.Connector.Navisworks.Bindings; diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs index 2dbf1352b..5af592f58 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs @@ -12,6 +12,7 @@ using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.Settings; +using Speckle.Converter.Navisworks.Services; using Speckle.Converter.Navisworks.Settings; using Speckle.Converters.Common; using Speckle.Sdk.Common; @@ -58,12 +59,12 @@ public class NavisworksSendBinding : ISendBinding private static void SubscribeToNavisworksEvents() { } - // Do not change the behavior/scope of this class on send binding unless make sure the behavior is same. Otherwise, we might not be able to update list of saved sets. + // WARNING: Changes to filter behavior here must match everywhere filters are used, or saved sets won't update correctly public List GetSendFilters() => [ new NavisworksSelectionFilter() { IsDefault = true }, - new NavisworksSavedSetsFilter(new ElementSelectionService()), - new NavisworksSavedViewsFilter(new ElementSelectionService()) + new NavisworksSavedSetsFilter(new ConnectorElementSelectionService()), + new NavisworksSavedViewsFilter(new ConnectorElementSelectionService()) ]; public List GetSendSettings() => @@ -105,6 +106,7 @@ public class NavisworksSendBinding : ISendBinding ) { var selectedPaths = modelCard.SendFilter.NotNull().RefreshObjectIds(); + var convertHiddenElementsSetting = modelCard.Settings!.FirstOrDefault(s => s.Id == "convertHiddenElements")?.Value as bool? ?? false; var message = convertHiddenElementsSetting @@ -115,30 +117,78 @@ public class NavisworksSendBinding : ISendBinding { throw new SpeckleSendFilterException(message); } + onOperationProgressed.Report(new CardProgress("Getting selection...", null)); await Task.CompletedTask; - var modelItems = new List(); + int estimatedCapacity = selectedPaths.Count * 10; + var modelItems = new List(estimatedCapacity); double count = 0; + foreach (var path in selectedPaths) { onOperationProgressed.Report(new CardProgress("Getting selection...", count / selectedPaths.Count)); await Task.CompletedTask; + var modelItem = _selectionService.GetModelItemFromPath(path); - modelItems.AddRange(_selectionService.GetGeometryNodes(modelItem).Where(_selectionService.IsVisible)); + var hasChildren = modelItem.Children.Any(); + + if (hasChildren) + { + int nodesVisited = 0; + int hiddenBranchesPruned = 0; + const int REPORT_INTERVAL = 1000; + + void TraverseWithProgress(NAV.ModelItem node) + { + nodesVisited++; + + if (nodesVisited % REPORT_INTERVAL == 0) + { + onOperationProgressed.Report( + new CardProgress( + $"Expanding tree: {nodesVisited} visited, {modelItems.Count} with geometry, {hiddenBranchesPruned} hidden", + null + ) + ); + Task.Delay(1).Wait(); + } + + if (!_selectionService.IsVisible(node)) + { + hiddenBranchesPruned++; + return; + } + + if (node.HasGeometry) + { + modelItems.Add(node); + } + + foreach (var child in node.Children) + { + TraverseWithProgress(child); + } + } + + TraverseWithProgress(modelItem); + } + else + { + if (modelItem.HasGeometry && _selectionService.IsVisible(modelItem)) + { + modelItems.Add(modelItem); + } + } + count++; } + return modelItems.Count == 0 ? throw new SpeckleSendFilterException(message) : modelItems; } public void CancelSend(string modelCardId) => _cancellationManager.CancelOperation(modelCardId); - /// - /// Cancels all outstanding send operations for the current document. - /// This method is called when the active document changes, to ensure - /// that any in-progress send operations are properly canceled before - /// the new document is loaded. - /// public void CancelAllSendOperations() { foreach (var modelCardId in _store.GetSenders().Select(m => m.ModelCardId)) diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs index a88aa46d8..845b6ed62 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs @@ -15,7 +15,7 @@ using Speckle.Connectors.DUI.Bridge; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.WebView; -using Speckle.Converter.Navisworks.Services; +using Speckle.Converter.Navisworks.Constants.Registers; using Speckle.Converter.Navisworks.Settings; using Speckle.Converters.Common; using Speckle.Sdk.Models.GraphTraversal; @@ -53,9 +53,6 @@ public static class NavisworksConnectorServiceRegistration serviceCollection.AddScoped(); serviceCollection.AddScoped(); - // Register dual shared geometry stores for instancing pattern - serviceCollection.AddScoped(); - serviceCollection.AddSingleton(); // Sending operations @@ -64,6 +61,9 @@ public static class NavisworksConnectorServiceRegistration serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc()); serviceCollection.AddSingleton(); + // Registers and caches + serviceCollection.AddScoped(); + // Register Intercom/interop serviceCollection.AddSingleton(); serviceCollection.AddSingleton(sp => sp.GetRequiredService()); @@ -73,6 +73,9 @@ public static class NavisworksConnectorServiceRegistration serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); - serviceCollection.AddScoped(); + serviceCollection.AddScoped< + Converter.Navisworks.Services.IElementSelectionService, + ConnectorElementSelectionService + >(); } } diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Extensions/ElementSelectionService.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Extensions/ElementSelectionService.cs index aeb84aecb..b8072cf8b 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Extensions/ElementSelectionService.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Extensions/ElementSelectionService.cs @@ -1,31 +1,12 @@ -using Speckle.InterfaceGenerator; -using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper; +namespace Speckle.Connector.Navisworks.Services; -namespace Speckle.Connector.Navisworks.Services; - -[GenerateAutoInterface] -public class ElementSelectionService : IElementSelectionService +/// +/// Connector-specific element selection service that extends the converter's base implementation. +/// Inherits the cached visibility checking and path resolution from the converter layer. +/// +public class ConnectorElementSelectionService : Converter.Navisworks.Services.ElementSelectionService { - private readonly Dictionary _visibleCache = new(); - - public string GetModelItemPath(NAV.ModelItem modelItem) => ResolveModelItemToIndexPath(modelItem); - - public NAV.ModelItem GetModelItemFromPath(string path) => ResolveIndexPathToModelItem(path); - - public bool IsVisible(NAV.ModelItem modelItem) - { - var key = modelItem.InstanceGuid; - if (_visibleCache.TryGetValue(key, out var isVisible)) - { - return isVisible; - } - //same as ElementSelectionHelper.IsElementVisible - foreach (var item in modelItem.AncestorsAndSelf) - { - _visibleCache[item.InstanceGuid] = !item.IsHidden; - } - return _visibleCache[key]; - } - - public IEnumerable GetGeometryNodes(NAV.ModelItem modelItem) => ResolveGeometryLeafNodes(modelItem); + // This inherits all functionality from the converter's ElementSelectionService + // including cached IsVisible, GetModelItemPath, GetModelItemFromPath, and GetGeometryNodes + // Connector-specific extensions can be added here if needed in the future } diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksColorUnpacker.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksColorUnpacker.cs index 66977f8fa..36b1f70db 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksColorUnpacker.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksColorUnpacker.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -using Speckle.Connector.Navisworks.Services; using Speckle.Converter.Navisworks.Helpers; +using Speckle.Converter.Navisworks.Services; using Speckle.Converter.Navisworks.Settings; using Speckle.Converters.Common; using Speckle.Sdk; @@ -130,16 +130,20 @@ public class NavisworksColorUnpacker( var comSelection = ComBridge.ToInwOpSelection([modelItem]); try { - var pathsCollection = comSelection.Paths(); + var paths = comSelection.Paths(); try { - foreach (ComApi.InwOaPath path in pathsCollection) + foreach (ComApi.InwOaPath path in paths) { - var fragmentsCollection = path.Fragments(); + GC.KeepAlive(path); + + var fragments = path.Fragments(); try { - foreach (ComApi.InwOaFragment3 fragment in fragmentsCollection.OfType()) + foreach (ComApi.InwOaFragment3 fragment in fragments) { + GC.KeepAlive(fragment); + fragment.GenerateSimplePrimitives(ComApi.nwEVertexProperty.eNORMAL, primitiveChecker); if (primitiveChecker.HasTriangles) @@ -150,9 +154,9 @@ public class NavisworksColorUnpacker( } finally { - if (fragmentsCollection != null) + if (fragments != null) { - System.Runtime.InteropServices.Marshal.ReleaseComObject(fragmentsCollection); + System.Runtime.InteropServices.Marshal.ReleaseComObject(fragments); } } } @@ -161,9 +165,9 @@ public class NavisworksColorUnpacker( } finally { - if (pathsCollection != null) + if (paths != null) { - System.Runtime.InteropServices.Marshal.ReleaseComObject(pathsCollection); + System.Runtime.InteropServices.Marshal.ReleaseComObject(paths); } } } diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksDocumentEvents.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksDocumentEvents.cs index 04255e6f1..21e47c1d6 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksDocumentEvents.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksDocumentEvents.cs @@ -120,17 +120,6 @@ public sealed class NavisworksDocumentEvents } } - private void UnsubscribeFromDocumentModelEvents(object _) - { - var activeDocument = NavisworksApp.ActiveDocument; - if (activeDocument != null) - { - UnsubscribeFromModelEvents(activeDocument); - } - - _isSubscribed = false; - } - private void UnsubscribeFromModelEvents(NAV.Document document) { document.Models.CollectionChanged -= HandleDocumentModelCountChanged; diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksMaterialUnpacker.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksMaterialUnpacker.cs index 1f75a9504..dfe97f8bd 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksMaterialUnpacker.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/HostApp/NavisworksMaterialUnpacker.cs @@ -1,22 +1,18 @@ -using Autodesk.Navisworks.Api.ComApi; -using Autodesk.Navisworks.Api.Interop.ComApi; using Microsoft.Extensions.Logging; -using Speckle.Connector.Navisworks.Services; -using Speckle.Converter.Navisworks.Constants; using Speckle.Converter.Navisworks.Helpers; +using Speckle.Converter.Navisworks.Services; using Speckle.Converter.Navisworks.Settings; -using Speckle.Converter.Navisworks.ToSpeckle; using Speckle.Converters.Common; using Speckle.Objects.Other; using Speckle.Sdk; +using static Speckle.Converter.Navisworks.Constants.MaterialConstants; namespace Speckle.Connector.Navisworks.HostApp; public class NavisworksMaterialUnpacker( ILogger logger, IConverterSettingsStore converterSettings, - IElementSelectionService selectionService, - GeometryToSpeckleConverter converter + IElementSelectionService selectionService ) { private static T SelectByRepresentationMode( @@ -74,66 +70,6 @@ public class NavisworksMaterialUnpacker( var navisworksObjectId = selectionService.GetModelItemPath(navisworksObject); var finalId = mergedIds.TryGetValue(navisworksObjectId, out var mergedId) ? mergedId : navisworksObjectId; - string hashId = ""; - try - { - var item = selectionService.GetModelItemFromPath(finalId); - var comSelection = ComApiBridge.ToInwOpSelection([item]); - try - { - var paths = comSelection.Paths(); - try - { - if (paths.Count > 0) - { - var firstPath = paths.OfType().FirstOrDefault(); - if (firstPath != null) - { - var fragments = firstPath.Fragments(); - try - { - if (fragments.Count > 1) - { - var fragmentId = converter.GenerateFragmentId(paths); - hashId = $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}"; - } - } - finally - { - if (fragments != null) - { - System.Runtime.InteropServices.Marshal.ReleaseComObject(fragments); - } - } - } - } - } - finally - { - if (paths != null) - { - System.Runtime.InteropServices.Marshal.ReleaseComObject(paths); - } - } - } - finally - { - if (comSelection != null) - { - System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection); - } - } - } - catch (Exception ex) when (!ex.IsFatal()) - { // If COM interop fails during hash generation, fall back to using finalId - logger.LogWarning( - ex, - "Failed to generate fragment hash ID for item {ItemId}, using finalId as fallback", - finalId - ); - hashId = ""; - } - var geometry = navisworksObject.Geometry; var mode = converterSettings.Current.User.VisualRepresentationMode; @@ -162,7 +98,7 @@ public class NavisworksMaterialUnpacker( ); var materialName = - $"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}"; + $"{DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}"; var itemCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Item"); if (itemCategory != null) @@ -188,14 +124,14 @@ public class NavisworksMaterialUnpacker( if (renderMaterialProxies.TryGetValue(renderMaterialId.ToString(), out RenderMaterialProxy? value)) { - value.objects.Add(!string.IsNullOrEmpty(hashId) ? hashId : finalId); + value.objects.Add(finalId); } else { renderMaterialProxies[renderMaterialId.ToString()] = new RenderMaterialProxy() { value = CreateRenderMaterial(materialName, renderTransparency, renderColor, renderMaterialId), - objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId] + objects = [finalId] }; } } @@ -219,9 +155,7 @@ public class NavisworksMaterialUnpacker( var speckleRenderMaterial = new RenderMaterial() { - name = !string.IsNullOrEmpty(name) - ? name - : $"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}", + name = !string.IsNullOrEmpty(name) ? name : $"{DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}", opacity = 1 - transparency, metalness = 0, roughness = 1, diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedSetsFilter.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedSetsFilter.cs index 58911cb7c..67e5b4e9d 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedSetsFilter.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedSetsFilter.cs @@ -1,7 +1,7 @@ -using Speckle.Connector.Navisworks.Services; -using Speckle.Connectors.DUI.Exceptions; +using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.Utils; +using Speckle.Converter.Navisworks.Services; namespace Speckle.Connector.Navisworks.Operations.Send.Filters; diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedViewsFilter.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedViewsFilter.cs index ba47884a6..fda965b2e 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedViewsFilter.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/NavisworksSavedViewsFilter.cs @@ -1,7 +1,7 @@ -using Speckle.Connector.Navisworks.Services; -using Speckle.Connectors.DUI.Exceptions; +using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.Utils; +using Speckle.Converter.Navisworks.Services; namespace Speckle.Connector.Navisworks.Operations.Send.Filters; @@ -48,8 +48,6 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect return objectIds; } - var savedViews = NavisworksApp.ActiveDocument.SavedViewpoints; - foreach (var savedViewItem in SelectedItems.Select(item => ResolveSavedView(item.Id))) { // Get the visible elements in the saved view. @@ -82,12 +80,12 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect { var objectIds = new List(); - // THIS IS COMMENTED OUT AS IT IS LEGACY DEFENSIVE BEHAVIOUR - DISCUSSION REQUIRED + // THIS IS COMMENTED OUT AS IT IS LEGACY DEFENSIVE BEHAVIOR - DISCUSSION REQUIRED // if (!savedView.ContainsVisibilityOverrides) // { // // We check this again as the view settings may have changed in the saved card. // // If the saved view does not contain visibility overrides, this is effectively everything in the model. - // // This will need to be the documented behaviour. + // // This will need to be the documented behavior. // throw new SpeckleSendFilterException( // "Saved view does not contain visibility overrides. This would effectively publish everything in the model." // ); @@ -154,7 +152,7 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect switch (item) { // case NAV.SavedViewpoint { ContainsVisibilityOverrides: false }: - // Legacy defensive behaviour: skip viewpoints without visibility overrides. + // Legacy defensive behavior: skip viewpoints without visibility overrides. // Essentially, send everything, or whatever the current view state for hidden elements is. // break; case NAV.SavedViewpointAnimationCut: diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/SavedItemHelpers.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/SavedItemHelpers.cs index f04696f41..6f814ef7d 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/SavedItemHelpers.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Filters/SavedItemHelpers.cs @@ -1,4 +1,4 @@ -using Speckle.Converter.Navisworks.Constants; +using static Speckle.Converter.Navisworks.Constants.PathConstants; namespace Speckle.Connector.Navisworks.Operations.Send.Filters; @@ -15,6 +15,6 @@ public static class SavedItemHelpers current = current.Parent; } - return string.Join(PathConstants.SET_SEPARATOR, pathParts); + return string.Join(SET_SEPARATOR, pathParts); } } diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/GeometryNodeMerger.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/GeometryNodeMerger.cs index c7747bee3..adacf1d2f 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/GeometryNodeMerger.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/GeometryNodeMerger.cs @@ -1,5 +1,5 @@ using Speckle.Connector.Navisworks.Services; -using Speckle.Converter.Navisworks.Constants; +using static Speckle.Converter.Navisworks.Constants.PathConstants; namespace Speckle.Connector.Navisworks.Operations.Send; @@ -10,29 +10,29 @@ public static class GeometryNodeMerger { /// /// Groups sibling geometry nodes based on material properties for merging. - /// Only merges nodes that share the same parent and have identical material properties. + /// This only merges nodes that share the same parent and have identical material properties. /// /// The collection of ModelItems to process /// Dictionary mapping parent paths (with material signature suffix) to their mergeable child nodes public static Dictionary> GroupSiblingGeometryNodes(IReadOnlyList nodes) { - var selectionService = new ElementSelectionService(); + var selectionService = new ConnectorElementSelectionService(); // Group nameless geometry nodes by parent path and material signature var mergeableGroups = nodes .Where(node => node.HasGeometry && string.IsNullOrEmpty(node.DisplayName)) // Only anonymous geometry nodes .GroupBy(node => { - // Get parent path + // Get the parent path var path = selectionService.GetModelItemPath(node); - var lastSeparatorIndex = path.LastIndexOf(PathConstants.SEPARATOR); + var lastSeparatorIndex = path.LastIndexOf(SEPARATOR); var parentPath = lastSeparatorIndex == -1 ? path : path[..lastSeparatorIndex]; // Generate material signature string signature = GenerateSignature(node); // Combine parent path with signature - return $"{parentPath}{PathConstants.MATERIAL_SEPARATOR}{signature}"; + return $"{parentPath}{MATERIAL_SEPARATOR}{signature}"; }) .Where(group => group.Count() > 1) // Only include groups with multiple children .ToDictionary(group => group.Key, group => group.ToList()); @@ -95,7 +95,7 @@ public static class GeometryNodeMerger // Build a consistent string representation of all properties var hashInput = new System.Text.StringBuilder(); - // Sort keys to ensure consistent order + // Sort keys to ensure a consistent order var sortedKeys = properties.Keys.OrderBy(k => k).ToList(); foreach (var key in sortedKeys) @@ -139,7 +139,7 @@ public static class GeometryNodeMerger /// private static string GetMaterialName(NAV.ModelItem node) { - // Check Item category for material name + // Check the Item category for material name var itemCategory = node.PropertyCategories.FindCategoryByDisplayName("Item"); if (itemCategory != null) { diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksHierarchyBuilder.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksHierarchyBuilder.cs index 68749a1a9..587539acd 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksHierarchyBuilder.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksHierarchyBuilder.cs @@ -1,8 +1,8 @@ -using Speckle.Connector.Navisworks.Services; -using Speckle.Converter.Navisworks.Constants; +using Speckle.Converter.Navisworks.Services; using Speckle.Converters.Common; using Speckle.Sdk.Models; using Speckle.Sdk.Models.Collections; +using static Speckle.Converter.Navisworks.Constants.PathConstants; namespace Speckle.Connector.Navisworks.Operations.Send; @@ -59,8 +59,8 @@ public class NavisworksHierarchyBuilder allPaths.Sort( (a, b) => { - var depthA = a.Count(c => c == PathConstants.SEPARATOR); - var depthB = b.Count(c => c == PathConstants.SEPARATOR); + var depthA = a.Count(c => c == SEPARATOR); + var depthB = b.Count(c => c == SEPARATOR); return depthB.CompareTo(depthA); // <- Sort in ascending order of path length } ); @@ -126,7 +126,7 @@ public class NavisworksHierarchyBuilder private static string GetParentPath(string path) { - var idx = path.LastIndexOf(PathConstants.SEPARATOR); + var idx = path.LastIndexOf(SEPARATOR); return idx == -1 ? string.Empty : path[..idx]; } diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs index 39e38df80..1ce4398cd 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/NavisworksRootObjectBuilder.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Speckle.Connector.Navisworks.HostApp; -using Speckle.Connector.Navisworks.Services; using Speckle.Connectors.Common.Builders; using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Conversion; @@ -14,7 +13,10 @@ using Speckle.Sdk; using Speckle.Sdk.Logging; using Speckle.Sdk.Models; using Speckle.Sdk.Models.Collections; +using Speckle.Sdk.Models.Instances; using static Speckle.Connector.Navisworks.Operations.Send.GeometryNodeMerger; +using static Speckle.Connectors.Common.Operations.ProxyKeys; +using static Speckle.Converter.Navisworks.Constants.InstanceConstants; namespace Speckle.Connector.Navisworks.Operations.Send; @@ -26,14 +28,17 @@ public class NavisworksRootObjectBuilder( ISdkActivityFactory activityFactory, NavisworksMaterialUnpacker materialUnpacker, NavisworksColorUnpacker colorUnpacker, + Speckle.Converter.Navisworks.Constants.Registers.IInstanceFragmentRegistry instanceRegistry, IElementSelectionService elementSelectionService, IUiUnitsCache uiUnitsCache, - InstanceStoreManager instanceStoreManager + bool disableGroupingForInstanceTesting, + bool skipNodeMerging ) : IRootObjectBuilder { - private bool SkipNodeMerging { get; set; } - - internal NavisworksConversionSettings GetCurrentSettings() => converterSettings.Current; +#pragma warning disable CA1823 +#pragma warning restore CA1823 + private bool SkipNodeMerging { get; } = skipNodeMerging; + private bool DisableGroupingForInstanceTesting { get; } = disableGroupingForInstanceTesting; public async Task Build( IReadOnlyList navisworksModelItems, @@ -43,14 +48,14 @@ public class NavisworksRootObjectBuilder( ) { #if DEBUG - SkipNodeMerging = true; + SkipNodeMerging = false; + DisableGroupingForInstanceTesting = false; #endif using var activity = activityFactory.Start("Build"); ValidateInputs(navisworksModelItems, projectId, onOperationProgressed); var rootCollection = InitializeRootCollection(); - (Dictionary convertedElements, List conversionResults) = await ConvertModelItemsAsync(navisworksModelItems, projectId, onOperationProgressed, cancellationToken); @@ -58,30 +63,17 @@ public class NavisworksRootObjectBuilder( var groupedNodes = SkipNodeMerging ? [] : GroupSiblingGeometryNodes(navisworksModelItems); var finalElements = BuildFinalElements(convertedElements, groupedNodes); - List geometryDefinitions = instanceStoreManager.GetGeometryDefinitions(); await AddProxiesToCollection(rootCollection, navisworksModelItems, groupedNodes); - var geometryDefinitionsCollection = new Collection - { - name = "Geometry Definitions", - ["units"] = converterSettings.Current.Derived.SpeckleUnits, - elements = geometryDefinitions - }; - - var mainElementsCollection = new Collection - { - name = rootCollection.name, - ["units"] = converterSettings.Current.Derived.SpeckleUnits, - elements = finalElements - }; - - rootCollection.elements = [mainElementsCollection]; - if (geometryDefinitions.Count > 0) - { - rootCollection.elements.Add(geometryDefinitionsCollection); - } + AddInstanceDefinitionsToCollection(rootCollection, ref finalElements); + int finalInstanceProxyCount = CountInstanceProxiesRecursive(finalElements); + logger.LogInformation( + "Final output contains {count} InstanceProxy objects in displayValues", + finalInstanceProxyCount + ); + rootCollection.elements = finalElements; return new RootObjectBuilderResult(rootCollection, conversionResults); } @@ -127,16 +119,32 @@ public class NavisworksRootObjectBuilder( var convertedBases = new Dictionary(); int processedCount = 0; int totalCount = navisworksModelItems.Count; + int instanceProxyCount = 0; foreach (var item in navisworksModelItems) { cancellationToken.ThrowIfCancellationRequested(); var converted = ConvertNavisworksItem(item, convertedBases, projectId); results.Add(converted); + + if ( + converted.Status == Status.SUCCESS + && convertedBases.TryGetValue(elementSelectionService.GetModelItemPath(item), out var convertedBase) + && convertedBase?["displayValue"] is List displayValues + ) + { + instanceProxyCount += displayValues.Count(dv => dv.GetType().Name == "InstanceProxy"); + } + processedCount++; onOperationProgressed.Report(new CardProgress("Converting", (double)processedCount / totalCount)); } + logger.LogInformation( + "Converted {total} items, found {instanceProxies} InstanceProxy objects", + totalCount, + instanceProxyCount + ); return Task.FromResult((convertedBases, results)); } @@ -155,10 +163,24 @@ public class NavisworksRootObjectBuilder( { var finalElements = new List(); var processedPaths = new HashSet(); - AddGroupedElements(finalElements, convertedBases, groupedNodes, processedPaths); + + if (!DisableGroupingForInstanceTesting) + { + AddGroupedElements(finalElements, convertedBases, groupedNodes, processedPaths); + logger.LogInformation( + "After grouping: {grouped} paths processed, {elements} elements in collection", + processedPaths.Count, + finalElements.Count + ); + } + else + { + logger.LogInformation("Grouping disabled for instance testing"); + } if (converterSettings.Current.User.PreserveModelHierarchy) { + logger.LogInformation("Building hierarchy (PreserveModelHierarchy=true)"); var hierarchyBuilder = new NavisworksHierarchyBuilder( convertedBases, rootToSpeckleConverter, @@ -168,7 +190,10 @@ public class NavisworksRootObjectBuilder( return hierarchyBuilder.BuildHierarchy(); } + logger.LogInformation("Adding remaining elements (flat mode)"); AddRemainingElements(finalElements, convertedBases, processedPaths); + + logger.LogInformation("Final elements count: {count}", finalElements.Count); return finalElements; } @@ -181,7 +206,7 @@ public class NavisworksRootObjectBuilder( { foreach (var group in groupedNodes) { - var siblingBases = new List(); + var siblingBases = new List(group.Value.Count); foreach (var itemPath in group.Value.Select(elementSelectionService.GetModelItemPath)) { processedPaths.Add(itemPath); @@ -236,10 +261,29 @@ public class NavisworksRootObjectBuilder( string cleanParentPath = ElementSelectionHelper.GetCleanPath(groupKey); (string name, string path) = GetElementNameAndPath(cleanParentPath); + int estimatedCapacity = siblingBases.Sum(b => (b["displayValue"] as List)?.Count ?? 0); + var displayValues = new List(estimatedCapacity); + displayValues.AddRange( + siblingBases + .Where(sibling => sibling["displayValue"] is List) + .SelectMany(sibling => (List)sibling["displayValue"]!) + ); + + var instanceProxyCount = displayValues.Count(dv => dv.GetType().Name == "InstanceProxy"); + if (instanceProxyCount > 0) + { + logger.LogDebug( + "Group {groupKey} merging {siblings} siblings with {proxies} InstanceProxy objects", + groupKey, + siblingBases.Count, + instanceProxyCount + ); + } + return new NavisworksObject { name = name, - displayValue = siblingBases.SelectMany(b => b["displayValue"] as List ?? []).ToList(), + displayValue = displayValues, properties = siblingBases.First()["properties"] as Dictionary ?? [], units = converterSettings.Current.Derived.SpeckleUnits, applicationId = groupKey, @@ -280,25 +324,100 @@ public class NavisworksRootObjectBuilder( var renderMaterials = materialUnpacker.UnpackRenderMaterial(navisworksModelItems, groupedNodes); if (renderMaterials.Count > 0) { - rootCollection[ProxyKeys.RENDER_MATERIAL] = renderMaterials; + rootCollection[RENDER_MATERIAL] = renderMaterials; } var colors = colorUnpacker.UnpackColor(navisworksModelItems, groupedNodes); if (colors.Count > 0) { - rootCollection[ProxyKeys.COLOR] = colors; - } - - var instanceDefinitionProxies = instanceStoreManager.GetInstanceDefinitionProxies(); - - if (instanceDefinitionProxies.Count > 0) - { - rootCollection[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies.ToList(); + rootCollection[COLOR] = colors; } return Task.CompletedTask; } + private void AddInstanceDefinitionsToCollection(Collection rootCollection, ref List finalElements) + { + using var _ = activityFactory.Start("BuildInstanceDefinitions"); + + // Get all definition geometries from the registry + var allDefinitions = instanceRegistry.GetAllDefinitionGeometries(); + + if (allDefinitions.Count == 0) + { + logger.LogInformation("No instance definitions found - instancing may be disabled"); + return; + } + + logger.LogInformation("Building instance structure for {count} definition groups", allDefinitions.Count); + + if (allDefinitions.Count > 100) + { + logger.LogWarning( + "Large number of definition groups ({count}) detected - this may indicate instance grouping is not working effectively", + allDefinitions.Count + ); + } + + var instanceDefinitionProxies = new List(allDefinitions.Count); + + int estimatedGeometryCount = allDefinitions.Sum(kvp => kvp.Value.Count); + var allDefinitionGeometries = new List(estimatedGeometryCount); + + foreach (var kvp in allDefinitions) + { + var groupKey = kvp.Key; + var geometries = kvp.Value; + var groupKeyHash = groupKey.ToHashString(); + + var defProxy = new InstanceDefinitionProxy + { + name = $"Shared Geometry {groupKeyHash}", + objects = geometries.Select(g => g.applicationId ?? "").Where(id => !string.IsNullOrEmpty(id)).ToList(), + applicationId = $"{DEFINITION_ID_PREFIX}{groupKeyHash}", + maxDepth = 0 + }; + + instanceDefinitionProxies.Add(defProxy); + allDefinitionGeometries.AddRange(geometries); + } + + rootCollection[INSTANCE_DEFINITION] = instanceDefinitionProxies; + var geometryDefinitionsCollection = new Collection + { + name = "Geometry Definitions", + elements = allDefinitionGeometries + }; + + var objectCollection = new Collection { name = "", elements = finalElements }; + + finalElements = [geometryDefinitionsCollection, objectCollection]; + + logger.LogInformation( + "Added {proxyCount} instance definition proxies and {geomCount} definition geometries", + instanceDefinitionProxies.Count, + allDefinitionGeometries.Count + ); + } + + private int CountInstanceProxiesRecursive(List elements) + { + int count = 0; + foreach (var element in elements) + { + if (element["displayValue"] is List displayValues) + { + count += displayValues.Count(dv => dv.GetType().Name == "InstanceProxy"); + } + + if (element is Collection { elements: not null } collection) + { + count += CountInstanceProxiesRecursive(collection.elements); + } + } + return count; + } + private SendConversionResult ConvertNavisworksItem( NAV.ModelItem navisworksItem, Dictionary convertedBases, diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs index fedfc5b0a..9d92a8180 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using Speckle.Connectors.Common.Caching; +using Speckle.Connectors.Common.Caching; using Speckle.Connectors.DUI.Models.Card; using Speckle.Converter.Navisworks.Settings; using Speckle.InterfaceGenerator; @@ -8,158 +7,106 @@ using Speckle.Sdk.Common; namespace Speckle.Connector.Navisworks.Operations.Send.Settings; [GenerateAutoInterface] -public class ToSpeckleSettingsManagerNavisworks : IToSpeckleSettingsManagerNavisworks +public class ToSpeckleSettingsManagerNavisworks(ISendConversionCache sendConversionCache) + : IToSpeckleSettingsManagerNavisworks { - private readonly ISendConversionCache _sendConversionCache; - - // cache invalidation process run with ModelCardId since the settings are model specific + // cache invalidation process run with ModelCardId since the settings are model-specific private readonly Dictionary _visualRepresentationCache = []; private readonly Dictionary _originModeCache = []; - private readonly Dictionary _convertHiddenElementsCache = []; - private readonly Dictionary _includeInternalPropertiesCache = []; - private readonly Dictionary _preserveModelHierarchyCache = []; - private readonly Dictionary _revitCategoryMappingCache = []; + private readonly Dictionary _convertHiddenElementsCache = []; + private readonly Dictionary _includeInternalPropertiesCache = []; + private readonly Dictionary _preserveModelHierarchyCache = []; + private readonly Dictionary _revitCategoryMappingCache = []; - public ToSpeckleSettingsManagerNavisworks(ISendConversionCache sendConversionCache) - { - _sendConversionCache = sendConversionCache; - } - - public RepresentationMode GetVisualRepresentationMode(SenderModelCard modelCard) + /// + /// Generic helper to get a setting value with caching and cache invalidation. + /// + private T GetCachedSetting( + SenderModelCard modelCard, + string settingId, + Dictionary cache, + Func valueExtractor, + T defaultValue + ) { if (modelCard == null) { throw new ArgumentNullException(nameof(modelCard)); } - var representationString = modelCard.Settings?.First(s => s.Id == "visualRepresentation").Value as string; + var settingValue = modelCard.Settings?.FirstOrDefault(s => s.Id == settingId)?.Value; + var returnValue = settingValue != null ? valueExtractor(settingValue) : defaultValue; if ( - representationString is not null - && VisualRepresentationSetting.VisualRepresentationMap.TryGetValue( - representationString, - out RepresentationMode representation - ) + cache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue) + && !EqualityComparer.Default.Equals(previousValue, returnValue) ) - { - if (_visualRepresentationCache.TryGetValue(modelCard.ModelCardId.NotNull(), out RepresentationMode previousType)) - { - if (previousType != representation) - { - EvictCacheForModelCard(modelCard); - } - } - - _visualRepresentationCache[modelCard.ModelCardId.NotNull()] = representation; - return representation; - } - - throw new ArgumentException($"Invalid visual representation value: {representationString}"); - } - - public OriginMode GetOriginMode(SenderModelCard modelCard) - { - if (modelCard == null) - { - throw new ArgumentNullException(nameof(modelCard)); - } - - var originString = modelCard.Settings?.FirstOrDefault(s => s.Id == "originMode")?.Value as string; - if (!OriginModeSetting.OriginModeMap.TryGetValue(originString ?? string.Empty, out var origin)) - { - return OriginMode.ModelOrigin; // Default to ModelOrigin if not specified or invalid - } - - if (_originModeCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousType) && previousType != origin) { EvictCacheForModelCard(modelCard); } - _originModeCache[modelCard.ModelCardId.NotNull()] = origin; - return origin; - } - - public bool GetMappingToRevitCategories(SenderModelCard modelCard) - { - if (modelCard == null) - { - throw new ArgumentNullException(nameof(modelCard)); - } - - var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "mappingToRevitCategories")?.Value as bool?; - - var returnValue = value != null && value.NotNull(); - if (_revitCategoryMappingCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue)) - { - if (previousValue != returnValue) - { - EvictCacheForModelCard(modelCard); - } - } - - _revitCategoryMappingCache[modelCard.ModelCardId] = returnValue; + cache[modelCard.ModelCardId.NotNull()] = returnValue; return returnValue; } - public bool GetConvertHiddenElements(SenderModelCard modelCard) - { - if (modelCard == null) - { - throw new ArgumentNullException(nameof(modelCard)); - } - - var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "convertHiddenElements")?.Value as bool?; - - var returnValue = value != null && value.NotNull(); - if (_convertHiddenElementsCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue)) - { - if (previousValue != returnValue) + public RepresentationMode GetVisualRepresentationMode(SenderModelCard modelCard) => + GetCachedSetting( + modelCard, + "visualRepresentation", + _visualRepresentationCache, + value => { - EvictCacheForModelCard(modelCard); - } - } + var representationString = value as string; + return + representationString is not null + && VisualRepresentationSetting.VisualRepresentationMap.TryGetValue( + representationString, + out RepresentationMode representation + ) + ? representation + : throw new ArgumentException($"Invalid visual representation value: {representationString}"); + }, + RepresentationMode.Active // default value if setting not found + ); - _convertHiddenElementsCache[modelCard.ModelCardId] = returnValue; - return returnValue; - } - - public bool GetIncludeInternalProperties([NotNull] SenderModelCard modelCard) - { - var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "includeInternalProperties")?.Value as bool?; - - var returnValue = value != null && value.NotNull(); - if (_includeInternalPropertiesCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue)) - { - if (previousValue != returnValue) + public OriginMode GetOriginMode(SenderModelCard modelCard) => + GetCachedSetting( + modelCard, + "originMode", + _originModeCache, + value => { - EvictCacheForModelCard(modelCard); - } - } + var originString = value as string; + if (OriginModeSetting.OriginModeMap.TryGetValue(originString ?? string.Empty, out var origin)) + { + return origin; + } + return OriginMode.ModelOrigin; + }, + OriginMode.ModelOrigin + ); - _includeInternalPropertiesCache[modelCard.ModelCardId] = returnValue; - return returnValue; - } + public bool GetMappingToRevitCategories(SenderModelCard modelCard) => + GetCachedSetting(modelCard, "mappingToRevitCategories", _revitCategoryMappingCache, value => value is true, false); - public bool GetPreserveModelHierarchy([NotNull] SenderModelCard modelCard) - { - var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "preserveModelHierarchy")?.Value as bool?; + public bool GetConvertHiddenElements(SenderModelCard modelCard) => + GetCachedSetting(modelCard, "convertHiddenElements", _convertHiddenElementsCache, value => value is true, false); - var returnValue = value != null && value.NotNull(); - if (_preserveModelHierarchyCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue)) - { - if (previousValue != returnValue) - { - EvictCacheForModelCard(modelCard); - } - } + public bool GetIncludeInternalProperties(SenderModelCard modelCard) => + GetCachedSetting( + modelCard, + "includeInternalProperties", + _includeInternalPropertiesCache, + value => value is true, + false + ); - _preserveModelHierarchyCache[modelCard.ModelCardId] = returnValue; - return returnValue; - } + public bool GetPreserveModelHierarchy(SenderModelCard modelCard) => + GetCachedSetting(modelCard, "preserveModelHierarchy", _preserveModelHierarchyCache, value => value is true, false); private void EvictCacheForModelCard(SenderModelCard modelCard) { var objectIds = modelCard.SendFilter != null ? modelCard.SendFilter.NotNull().SelectedObjectIds : []; - _sendConversionCache.EvictObjects(objectIds); + sendConversionCache.EvictObjects(objectIds); } } diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/Tools/SpeckleV3Tool.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/Tools/SpeckleV3Tool.cs index c27d5cc98..140522d3f 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/Tools/SpeckleV3Tool.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/Tools/SpeckleV3Tool.cs @@ -14,7 +14,7 @@ public static class SpeckleV3Tool public const string RIBBON_STRINGS = "NavisworksRibbon.name"; public const string PLUGIN_SUFFIX = ".Speckle"; - public static Speckle.Sdk.Application App => + public static Sdk.Application App => #if NAVIS HostApplications.Navisworks; #else diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/InstanceConstants.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/InstanceConstants.cs new file mode 100644 index 000000000..cb256775e --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/InstanceConstants.cs @@ -0,0 +1,8 @@ +namespace Speckle.Converter.Navisworks.Constants; + +public static class InstanceConstants +{ + public const string GEOMETRY_ID_PREFIX = "geom_"; + public const string DEFINITION_ID_PREFIX = "def_"; + public const string INSTANCE_ID_PREFIX = "instance_"; +} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/MaterialConstants.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/MaterialConstants.cs new file mode 100644 index 000000000..a7a4b7e87 --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/MaterialConstants.cs @@ -0,0 +1,6 @@ +namespace Speckle.Converter.Navisworks.Constants; + +public static class MaterialConstants +{ + public const string DEFAULT_MATERIAL_NAME_PREFIX = "NavisworksMaterial_"; +} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/PathConstants.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/PathConstants.cs new file mode 100644 index 000000000..0ebde123c --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Constants/PathConstants.cs @@ -0,0 +1,8 @@ +namespace Speckle.Converter.Navisworks.Constants; + +public static class PathConstants +{ + public const char SEPARATOR = '/'; + public const string MATERIAL_SEPARATOR = "::"; + public const string SET_SEPARATOR = ">"; +} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ClassPropertiesExtractor.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ClassPropertiesExtractor.cs index 941d95395..4c1bb63eb 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ClassPropertiesExtractor.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ClassPropertiesExtractor.cs @@ -4,15 +4,8 @@ namespace Speckle.Converter.Navisworks.ToSpeckle; public class ClassPropertiesExtractor { - public Dictionary? GetClassProperties(NAV.ModelItem modelItem) - { - if (modelItem == null) - { - throw new ArgumentNullException(nameof(modelItem)); - } - - return ExtractClassProperties(modelItem); - } + public Dictionary GetClassProperties(NAV.ModelItem modelItem) => + modelItem == null ? throw new ArgumentNullException(nameof(modelItem)) : ExtractClassProperties(modelItem); /// /// Extracts property sets from a NAV.ModelItem and adds them to a dictionary, diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/DisplayValueExtractor.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/DisplayValueExtractor.cs index 539bb4ace..ec721dac1 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/DisplayValueExtractor.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/DisplayValueExtractor.cs @@ -1,19 +1,23 @@ -using Speckle.Sdk.Models; -using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper; +using Speckle.Converter.Navisworks.Services; +using Speckle.Sdk.Models; namespace Speckle.Converter.Navisworks.ToSpeckle; -public class DisplayValueExtractor(GeometryToSpeckleConverter geometryConverter) +public class DisplayValueExtractor( + GeometryToSpeckleConverter geometryConverter, + IElementSelectionService elementSelectionService +) { internal List GetDisplayValue(NAV.ModelItem modelItem) => modelItem == null ? throw new ArgumentNullException(nameof(modelItem)) - : !modelItem.HasGeometry - ? ([]) - : !IsElementVisible(modelItem) - ? [] - : - // this can be meshes or the instance reference objects - // the un transformed objects stored in a separate collection - geometryConverter.Convert(modelItem); + : !modelItem.HasGeometry || !elementSelectionService.IsVisible(modelItem) + ? [] + : GeometryConverter.Convert(modelItem); + + /// + /// Gets the underlying geometry converter for accessing cache statistics. + /// + internal GeometryToSpeckleConverter GeometryConverter { get; } = + geometryConverter ?? throw new ArgumentNullException(nameof(geometryConverter)); } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ModelPropertiesExtractor.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ModelPropertiesExtractor.cs index cd6ae2230..23f497df5 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ModelPropertiesExtractor.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/ModelPropertiesExtractor.cs @@ -3,18 +3,11 @@ using Speckle.Converters.Common; namespace Speckle.Converter.Navisworks.ToSpeckle; -public class ModelPropertiesExtractor +public class ModelPropertiesExtractor(IConverterSettingsStore settingsStore) { - private readonly IConverterSettingsStore _settingsStore; - - public ModelPropertiesExtractor(IConverterSettingsStore settingsStore) - { - _settingsStore = settingsStore; - } - internal Dictionary? GetModelProperties(NAV.Model model) { - if (_settingsStore.Current.User.ExcludeProperties) + if (settingsStore.Current.User.ExcludeProperties) { return null; } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/PropertySetsExtractor.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/PropertySetsExtractor.cs index 485f896e8..ee60810cf 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/PropertySetsExtractor.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/PropertySetsExtractor.cs @@ -35,7 +35,7 @@ public class PropertySetsExtractor( /// /// Extracts property sets from a NAV.ModelItem and adds them to a dictionary, - /// PropertySets are specific to the host application source appended to Navisworks and therefore + /// PropertySets are the specific set per host application source appended to Navisworks and therefore /// arbitrary in nature. /// /// The NAV.ModelItem from which property sets are extracted. diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/RevitBuiltInCategoryExtractor.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/RevitBuiltInCategoryExtractor.cs index 9beba6761..915ab330d 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/RevitBuiltInCategoryExtractor.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataExtractors/RevitBuiltInCategoryExtractor.cs @@ -28,6 +28,12 @@ public class RevitBuiltInCategoryExtractor(IPropertyConverter converter) : IRevi converter.Reset(); + // Check if item.Model is null before accessing Units + if (item.Model == null) + { + return false; + } + // Convert using per-object model units and current UI units var nameObj = converter.ConvertPropertyValue(v, item.Model.Units, item.DisplayName); var name = nameObj?.ToString(); diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/BasePropertyHandler.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/BasePropertyHandler.cs index d75e79910..11885217a 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/BasePropertyHandler.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/BasePropertyHandler.cs @@ -78,15 +78,8 @@ public abstract class BasePropertyHandler( } } - private static Dictionary CreatePropertyDictionary(Dictionary properties) - { - var propertyDict = new Dictionary(); - foreach (var prop in properties.Where(prop => IsValidPropertyValue(prop.Value))) - { - propertyDict[prop.Key] = prop.Value; - } - return propertyDict; - } + private static Dictionary CreatePropertyDictionary(Dictionary properties) => + properties.Where(prop => IsValidPropertyValue(prop.Value)).ToDictionary(prop => prop.Key, prop => prop.Value); protected static bool IsValidPropertyValue(object? value) => value != null && !string.IsNullOrEmpty(value.ToString()); } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/HierarchicalPropertyHandler.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/HierarchicalPropertyHandler.cs index 810547887..a642a735c 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/HierarchicalPropertyHandler.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DataHandlers/HierarchicalPropertyHandler.cs @@ -20,7 +20,7 @@ public class HierarchicalPropertyHandler( public override Dictionary GetProperties(NAV.ModelItem modelItem) { - var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem) ?? []; + var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem); // Interop-lite mapping for Revit built-in categories if (_mapRevit && revitCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory)) diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DependencyInjection/NavisworksConverterServiceRegistration.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DependencyInjection/NavisworksConverterServiceRegistration.cs index ce91654f9..b59206e1b 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/DependencyInjection/NavisworksConverterServiceRegistration.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/DependencyInjection/NavisworksConverterServiceRegistration.cs @@ -1,5 +1,6 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using Speckle.Converter.Navisworks.Constants.Registers; using Speckle.Converter.Navisworks.Helpers; using Speckle.Converter.Navisworks.Services; using Speckle.Converter.Navisworks.Settings; @@ -42,19 +43,17 @@ public static class NavisworksConverterServiceRegistration serviceCollection.AddScoped(); serviceCollection.AddScoped(); + // Register element selection service + serviceCollection.AddScoped(); + // Register geometry conversion serviceCollection.AddScoped(); - serviceCollection.AddScoped(); - - // Register dual shared geometry stores for instancing pattern (.NET Framework compatible) - // Store 1: For geometry definitions (Mesh, Curve, etc.) - Store 2: For InstanceDefinitionProxy objects - serviceCollection.AddScoped(); - - // Register ISharedGeometryStore interface using the geometry definitions store for backward compatibility - serviceCollection.AddScoped(provider => - provider.GetRequiredService().GeometryDefinitionsStore - ); - + serviceCollection.AddScoped(sp => + { + var settingsStore = sp.GetRequiredService>(); + var registry = sp.GetRequiredService(); + return new GeometryToSpeckleConverter(settingsStore.Current, registry); + }); // Register settings resolved from factory serviceCollection.AddScoped(sp => sp.GetRequiredService().Current diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Geometries/Primitives.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Geometries/Primitives.cs index deb59b5bd..4edaeb66e 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Geometries/Primitives.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Geometries/Primitives.cs @@ -1,5 +1,8 @@ -namespace Speckle.Converter.Navisworks.Geometry; +using System.Diagnostics.CodeAnalysis; +namespace Speckle.Converter.Navisworks.Geometry; + +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] public readonly struct SafeBoundingBox { public SafeVertex Center { get; } @@ -103,6 +106,7 @@ public readonly struct SafeVertex } } +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] public readonly struct SafePoint { public SafeVertex Vertex { get; } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/ElementSelectionHelper.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/ElementSelectionHelper.cs index b3069b693..0c4942032 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/ElementSelectionHelper.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/ElementSelectionHelper.cs @@ -1,4 +1,4 @@ -using Speckle.Converter.Navisworks.Constants; +using static Speckle.Converter.Navisworks.Constants.PathConstants; namespace Speckle.Converter.Navisworks.Helpers; @@ -29,7 +29,7 @@ public static class ElementSelectionHelper var pathIndex = modelItemPathId.PathId == "a" ? $"{modelItemPathId.ModelIndex}" // Root-level model item - : $"{modelItemPathId.ModelIndex}{PathConstants.SEPARATOR}{modelItemPathId.PathId}"; // Nested model item + : $"{modelItemPathId.ModelIndex}{SEPARATOR}{modelItemPathId.PathId}"; // Nested model item return pathIndex; } @@ -46,7 +46,7 @@ public static class ElementSelectionHelper throw new ArgumentNullException(nameof(indexPath)); } - int separatorIndex = indexPath.IndexOf(PathConstants.MATERIAL_SEPARATOR, StringComparison.Ordinal); + int separatorIndex = indexPath.IndexOf(MATERIAL_SEPARATOR, StringComparison.Ordinal); return separatorIndex > 0 ? indexPath[..separatorIndex] : indexPath; } @@ -60,10 +60,10 @@ public static class ElementSelectionHelper // Extract just the path part if the indexPath contains a material signature string pathToResolve = GetCleanPath(indexPath); - var indexPathParts = pathToResolve.Split(PathConstants.SEPARATOR); + var indexPathParts = pathToResolve.Split(SEPARATOR); var modelIndex = int.Parse(indexPathParts[0]); - var pathId = string.Join(PathConstants.SEPARATOR.ToString(), indexPathParts.Skip(1)); + var pathId = string.Join(SEPARATOR.ToString(), indexPathParts.Skip(1)); // assign the first part of indexPathParts to modelIndex and parse it to int, the second part to pathId string NAV.DocumentParts.ModelItemPathId modelItemPathId = new() { ModelIndex = modelIndex, PathId = pathId }; @@ -72,23 +72,6 @@ public static class ElementSelectionHelper return modelItem; } - /// - /// Determines whether a Navisworks and all its ancestors are visible. - /// - /// The model item to check for visibility. - /// True if the item and all ancestors are visible; otherwise, false. - /// Thrown if is null. - public static bool IsElementVisible(NAV.ModelItem modelItem) - { - if (modelItem == null) - { - throw new ArgumentNullException(nameof(modelItem)); - } - - // Check visibility status for the item and its ancestors - return modelItem.AncestorsAndSelf.All(item => !item.IsHidden); - } - public static IEnumerable ResolveGeometryLeafNodes(NAV.ModelItem modelItem) => modelItem.DescendantsAndSelf.Where(x => x.HasGeometry); } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/GeometryHelpers.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/GeometryHelpers.cs index 2ca3b3d6f..747fdd103 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/GeometryHelpers.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/GeometryHelpers.cs @@ -1,4 +1,24 @@ -namespace Speckle.Converter.Navisworks.Helpers; +using Speckle.Objects.Geometry; + +// ReSharper disable UnusedMember.Local + +namespace Speckle.Converter.Navisworks.Helpers; + +public readonly record struct Aabb(double MinX, double MinY, double MinZ, double MaxX, double MaxY, double MaxZ) +{ + private static bool IsNearlyZero(double value, double epsilon = 1e-9) => Math.Abs(value) <= epsilon; + + // public bool IsValid => !(MinX == 0 && MinY == 0 && MinZ == 0 && MaxX == 0 && MaxY == 0 && MaxZ == 0); + public bool IsValid => + !( + IsNearlyZero(MinX) + && IsNearlyZero(MinY) + && IsNearlyZero(MinZ) + && IsNearlyZero(MaxX) + && IsNearlyZero(MaxY) + && IsNearlyZero(MaxZ) + ); +} public static class GeometryHelpers { @@ -7,10 +27,292 @@ public static class GeometryHelpers /// /// The first comparison vector. /// The second comparison vector. - /// The tolerance value for the comparison. Default is 1e-9. + /// The tolerance value for the comparison. The default is 1e-9. /// True if the vectors match within the tolerance; otherwise, false. internal static bool VectorMatch(NAV.Vector3D vectorA, NAV.Vector3D vectorB, double tolerance = 1e-9) => Math.Abs(vectorA.X - vectorB.X) < tolerance && Math.Abs(vectorA.Y - vectorB.Y) < tolerance && Math.Abs(vectorA.Z - vectorB.Z) < tolerance; + + internal static double[] InvertRigid(double[] m) + { + // Rigid: [ R 0; t 1 ] in row major + // inv = [ R^T 0; -t*R^T 1 ] + var inv = new double[16]; + + // transpose 3x3 rotation + inv[0] = m[0]; + inv[1] = m[4]; + inv[2] = m[8]; + inv[3] = 0; + inv[4] = m[1]; + inv[5] = m[5]; + inv[6] = m[9]; + inv[7] = 0; + inv[8] = m[2]; + inv[9] = m[6]; + inv[10] = m[10]; + inv[11] = 0; + + var tx = m[12]; + var ty = m[13]; + var tz = m[14]; + + // -t * R^T + inv[12] = -(tx * inv[0] + ty * inv[4] + tz * inv[8]); + inv[13] = -(tx * inv[1] + ty * inv[5] + tz * inv[9]); + inv[14] = -(tx * inv[2] + ty * inv[6] + tz * inv[10]); + inv[15] = 1; + + return inv; + } + + /// + /// Multiplies two 4x4 matrices in row-major order. + /// Used to compute instance transforms: inverse(definitionWorld) × instanceWorld + /// + internal static double[] MultiplyMatrices4X4(double[] a, double[] b) + { + var result = new double[16]; + for (int row = 0; row < 4; row++) + { + for (int col = 0; col < 4; col++) + { + double sum = 0; + for (int k = 0; k < 4; k++) + { + sum += a[row * 4 + k] * b[k * 4 + col]; + } + result[row * 4 + col] = sum; + } + } + return result; + } + + private static void TransformPointInPlace(double[] m, ref double x, ref double y, ref double z) + { + var nx = x * m[0] + y * m[4] + z * m[8] + m[12]; + var ny = x * m[1] + y * m[5] + z * m[9] + m[13]; + var nz = x * m[2] + y * m[6] + z * m[10] + m[14]; + x = nx; + y = ny; + z = nz; + } + + // Used, for instance, validation - unbakes geometry from world space to definition space + internal static void UnbakeMeshVertices(Mesh mesh, double[] invWorld) + { + for (int i = 0; i < mesh.vertices.Count; i += 3) + { + double x = mesh.vertices[i]; + double y = mesh.vertices[i + 1]; + double z = mesh.vertices[i + 2]; + + TransformPointInPlace(invWorld, ref x, ref y, ref z); + + mesh.vertices[i] = x; + mesh.vertices[i + 1] = y; + mesh.vertices[i + 2] = z; + } + } + + // Used, for instance, validation - unbakes geometry from world space to definition space + internal static void UnbakeLine(Line line, double[] invWorld) + { + double sx = line.start.x, + sy = line.start.y, + sz = line.start.z; + double ex = line.end.x, + ey = line.end.y, + ez = line.end.z; + + TransformPointInPlace(invWorld, ref sx, ref sy, ref sz); + TransformPointInPlace(invWorld, ref ex, ref ey, ref ez); + + line.start.x = sx; + line.start.y = sy; + line.start.z = sz; + line.end.x = ex; + line.end.y = ey; + line.end.z = ez; + } + + internal static Aabb Aabb(Mesh mesh) + { + double minX = double.PositiveInfinity, + minY = double.PositiveInfinity, + minZ = double.PositiveInfinity; + double maxX = double.NegativeInfinity, + maxY = double.NegativeInfinity, + maxZ = double.NegativeInfinity; + + for (int i = 0; i < mesh.vertices.Count; i += 3) + { + var x = mesh.vertices[i]; + var y = mesh.vertices[i + 1]; + var z = mesh.vertices[i + 2]; + + if (x < minX) + { + minX = x; + } + + if (y < minY) + { + minY = y; + } + + if (z < minZ) + { + minZ = z; + } + + if (x > maxX) + { + maxX = x; + } + + if (y > maxY) + { + maxY = y; + } + + if (z > maxZ) + { + maxZ = z; + } + } + + return new Aabb(minX, minY, minZ, maxX, maxY, maxZ); + } + + private static bool NearlyEqual(double a, double b, double eps) => Math.Abs(a - b) <= eps; + + private static bool AabbEqual(Aabb a, Aabb b, double eps) => + NearlyEqual(a.MinX, b.MinX, eps) + && NearlyEqual(a.MinY, b.MinY, eps) + && NearlyEqual(a.MinZ, b.MinZ, eps) + && NearlyEqual(a.MaxX, b.MaxX, eps) + && NearlyEqual(a.MaxY, b.MaxY, eps) + && NearlyEqual(a.MaxZ, b.MaxZ, eps); + + internal static Aabb ComputeUnbakedAabb(PrimitiveProcessor processor, double[] invWorld) + { + var hasAny = false; + + double minX = double.PositiveInfinity, + minY = double.PositiveInfinity, + minZ = double.PositiveInfinity; + double maxX = double.NegativeInfinity, + maxY = double.NegativeInfinity, + maxZ = double.NegativeInfinity; + + void AddPoint(double x, double y, double z) + { + TransformPointInPlace(invWorld, ref x, ref y, ref z); + + hasAny = true; + if (x < minX) + { + minX = x; + } + + if (y < minY) + { + minY = y; + } + + if (z < minZ) + { + minZ = z; + } + + if (x > maxX) + { + maxX = x; + } + + if (y > maxY) + { + maxY = y; + } + + if (z > maxZ) + { + maxZ = z; + } + } + + foreach (var t in processor.Triangles) + { + AddPoint(t.Vertex1.X, t.Vertex1.Y, t.Vertex1.Z); + AddPoint(t.Vertex2.X, t.Vertex2.Y, t.Vertex2.Z); + AddPoint(t.Vertex3.X, t.Vertex3.Y, t.Vertex3.Z); + } + + foreach (var l in processor.Lines) + { + AddPoint(l.Start.X, l.Start.Y, l.Start.Z); + AddPoint(l.End.X, l.End.Y, l.End.Z); + } + + return hasAny ? new Aabb(minX, minY, minZ, maxX, maxY, maxZ) : default; + } + + private static void Acc( + double[] m, + double x, + double y, + double z, + ref double minX, + ref double minY, + ref double minZ, + ref double maxX, + ref double maxY, + ref double maxZ + ) + { + // apply transform (row major with translation at 12,13,14 as per your usage) + var nx = x * m[0] + y * m[4] + z * m[8] + m[12]; + var ny = x * m[1] + y * m[5] + z * m[9] + m[13]; + var nz = x * m[2] + y * m[6] + z * m[10] + m[14]; + + if (nx < minX) + { + minX = nx; + } + + if (ny < minY) + { + minY = ny; + } + + if (nz < minZ) + { + minZ = nz; + } + + if (nx > maxX) + { + maxX = nx; + } + + if (ny > maxY) + { + maxY = ny; + } + + if (nz > maxZ) + { + maxZ = nz; + } + } + + internal static bool NearlyEqual(Aabb a, Aabb b, double eps) => + Math.Abs(a.MinX - b.MinX) <= eps + && Math.Abs(a.MinY - b.MinY) <= eps + && Math.Abs(a.MinZ - b.MinZ) <= eps + && Math.Abs(a.MaxX - b.MaxX) <= eps + && Math.Abs(a.MaxY - b.MaxY) <= eps + && Math.Abs(a.MaxZ - b.MaxZ) <= eps; } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/PrimitiveProcessor.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/PrimitiveProcessor.cs index b646184fd..f4debf386 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/PrimitiveProcessor.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/PrimitiveProcessor.cs @@ -5,6 +5,11 @@ using Speckle.DoubleNumerics; namespace Speckle.Converter.Navisworks.Helpers; +/// +/// Callback processor for Navisworks COM primitive generation. +/// WARNING: COM interop bottleneck - fragment.GenerateSimplePrimitives() has significant marshaling overhead. +/// WARNING: InwSimpleVertex.coord returns Array (COM object) requiring 3 GetValue() calls per vertex. +/// public class PrimitiveProcessor : InwSimplePrimitivesCB { private readonly List _coords = []; @@ -52,13 +57,13 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB var safeLine = new SafeLine(vD1, vD2); AddLine(safeLine); } - catch (ArgumentException ex) + catch (ArgumentException) { - Console.WriteLine($"ArgumentException caught: {ex.Message}"); + // Invalid line geometry - skip } - catch (InvalidOperationException ex) + catch (InvalidOperationException) { - Console.WriteLine($"InvalidOperationException caught: {ex.Message}"); + // Invalid line geometry - skip } } @@ -78,7 +83,6 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB AddPoint(safePoint); } - // TODO: Needed for Splines public void SnapPoint(InwSimpleVertex? v1) => Point(v1); public void Triangle(InwSimpleVertex? v1, InwSimpleVertex? v2, InwSimpleVertex? v3) @@ -101,7 +105,6 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB IsUpright ); - // Capture values immediately in our safe struct var safeTriangle = new SafeTriangle(vD1, vD2, vD3); var indexPointer = Faces.Count; @@ -165,6 +168,9 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB return new NAV.Vector3D(vectorDoubleX, vectorDoubleY, vectorDoubleZ); } + /// + /// WARNING: Called for every vertex - COM marshaling overhead from Array cast and 3 GetValue() calls. + /// private static Vector3 VectorFromVertex(InwSimpleVertex v) { var arrayV = (Array)v.coord; diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/PathConstants.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/PathConstants.cs deleted file mode 100644 index 44152ff8d..000000000 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/PathConstants.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Speckle.Converter.Navisworks.Constants; - -public static class PathConstants -{ - public const char SEPARATOR = '/'; - public const string MATERIAL_SEPARATOR = "::"; - public const string SET_SEPARATOR = ">"; -} - -public static class InstanceConstants -{ - public const string GEOMETRY_ID_PREFIX = "geom_"; - public const string DEFINITION_ID_PREFIX = "def_"; -} - -public static class MaterialConstants -{ - public const string DEFAULT_MATERIAL_NAME_PREFIX = "NavisworksMaterial_"; -} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Paths/PathKey.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Paths/PathKey.cs new file mode 100644 index 000000000..2fe1b2ab4 --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Paths/PathKey.cs @@ -0,0 +1,131 @@ +namespace Speckle.Converter.Navisworks.Paths; + +public readonly record struct PathKey +{ + internal readonly int[] Data; + internal readonly int Hash; + + public static readonly IEqualityComparer Comparer = new PathKeyComparer(); + + private PathKey(int[] data) + { + this.Data = data ?? throw new ArgumentNullException(nameof(data)); + Hash = ComputeHash(data); + } + + public static PathKey FromComArray(Array arr) + { + if (arr == null) + { + throw new ArgumentNullException(nameof(arr)); + } + + if (arr.Rank != 1) + { + throw new ArgumentException("Expected 1D array.", nameof(arr)); + } + + int lb = arr.GetLowerBound(0); + int len = arr.GetLength(0); + + var data = new int[len]; + for (int i = 0; i < len; i++) + { + data[i] = (int)arr.GetValue(lb + i); + } + + return new PathKey(data); + } + + private static int ComputeHash(int[] data) + { + unchecked + { + int h = 17; + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (int i = 0; i < data.Length; i++) + { + h = h * 31 + data[i]; + } + + return h; + } + } + + public bool MatchesComArray(Array arr) + { + if (Data == null) + { + return false; + } + + if (arr.Rank != 1) + { + return false; + } + + int lb = arr.GetLowerBound(0); + int len = arr.GetLength(0); + if (len != Data.Length) + { + return false; + } + + for (int i = 0; i < len; i++) + { + if ((int)arr.GetValue(lb + i) != Data[i]) + { + return false; + } + } + + return true; + } + + public override string ToString() + { + if (Data == null || Data.Length == 0) + { + return string.Empty; + } + return string.Join(",", Data); + } + + /// + /// Returns a compact string representation using the hash value as an unsigned integer. + /// Suitable for use as application IDs and definition IDs. + /// This avoids negative numbers in IDs by treating the hash as unsigned. + /// + public string ToHashString() => unchecked((uint)Hash).ToString(); +} + +internal sealed class PathKeyComparer : IEqualityComparer +{ + public bool Equals(PathKey x, PathKey y) + { + if (ReferenceEquals(x.Data, y.Data)) + { + return true; + } + + if (x.Data.Length != y.Data.Length) + { + return false; + } + + // ReSharper disable once LoopCanBeConvertedToQuery + for (int i = 0; i < x.Data.Length; i++) + { + if (x.Data[i] != y.Data[i]) + { + return false; + } + } + + return true; + } + + public int GetHashCode(PathKey obj) => obj.Hash; +} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Registers/InstanceFragmentRegistry.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Registers/InstanceFragmentRegistry.cs new file mode 100644 index 000000000..dd129fabd --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Registers/InstanceFragmentRegistry.cs @@ -0,0 +1,151 @@ +using Speckle.Converter.Navisworks.Helpers; +using Speckle.Converter.Navisworks.Paths; +using Speckle.Sdk.Models; + +namespace Speckle.Converter.Navisworks.Constants.Registers; + +public interface IInstanceFragmentRegistry +{ + bool TryGetGroup(PathKey instancePath, out PathKey groupKey); + void RegisterGroup(PathKey groupKey, HashSet instancePaths); + void MarkConverted(PathKey instancePath); + + IEnumerable GetConvertedPaths(); + Dictionary> BuildGroupToConvertedPaths(); + + bool TryGetDefinitionWorld(PathKey groupKey, out double[] definitionWorld); + void EnsureDefinitionWorld(PathKey groupKey, double[] definitionWorld); + + bool TryGetInstanceWorld(PathKey instancePath, out double[] instanceWorld); + void SetInstanceWorld(PathKey instancePath, double[] instanceWorld); + + bool HasDefinitionGeometry(PathKey groupKey); + void StoreDefinitionGeometry(PathKey groupKey, List geometry); + bool TryGetDefinitionGeometry(PathKey groupKey, out List geometry); + + Dictionary> GetAllDefinitionGeometries(); + List GetAllGroupKeys(); + + void RegisterInstanceObservation( + PathKey groupKey, + PathKey instancePath, + double[] instanceWorld, + PrimitiveProcessor processor + ); +} + +public sealed class InstanceFragmentRegistry : IInstanceFragmentRegistry +{ + private readonly Dictionary _pathToGroup = new(PathKey.Comparer); + private readonly HashSet _converted = new(PathKey.Comparer); + + private readonly Dictionary _groupToDefinitionWorld = new(PathKey.Comparer); + private readonly Dictionary _pathToInstanceWorld = new(PathKey.Comparer); + private readonly Dictionary _groupSignature = new(PathKey.Comparer); + private readonly Dictionary> _groupDefinitions = new(PathKey.Comparer); + + public bool TryGetGroup(PathKey instancePath, out PathKey groupKey) => + _pathToGroup.TryGetValue(instancePath, out groupKey); + + public void RegisterGroup(PathKey groupKey, HashSet instancePaths) + { + foreach (var p in instancePaths) + { + _pathToGroup[p] = groupKey; + } + } + + public void MarkConverted(PathKey instancePath) => _converted.Add(instancePath); + + public IEnumerable GetConvertedPaths() => _converted; + + public Dictionary> BuildGroupToConvertedPaths() + { + var map = new Dictionary>(PathKey.Comparer); + + foreach (var instancePath in _converted) + { + if (!_pathToGroup.TryGetValue(instancePath, out var groupKey)) + { + continue; + } + + if (!map.TryGetValue(groupKey, out var list)) + { + list = []; + map.Add(groupKey, list); + } + + list.Add(instancePath); + } + + return map; + } + + public bool TryGetDefinitionWorld(PathKey groupKey, out double[] definitionWorld) => + _groupToDefinitionWorld.TryGetValue(groupKey, out definitionWorld); + + public void EnsureDefinitionWorld(PathKey groupKey, double[] definitionWorld) + { + if (!_groupToDefinitionWorld.ContainsKey(groupKey)) + { + _groupToDefinitionWorld[groupKey] = definitionWorld; + } + } + + public bool TryGetInstanceWorld(PathKey instancePath, out double[] instanceWorld) => + _pathToInstanceWorld.TryGetValue(instancePath, out instanceWorld); + + public void SetInstanceWorld(PathKey instancePath, double[] instanceWorld) => + _pathToInstanceWorld[instancePath] = instanceWorld; + + public bool HasDefinitionGeometry(PathKey groupKey) => _groupDefinitions.ContainsKey(groupKey); + + public void StoreDefinitionGeometry(PathKey groupKey, List geometry) => _groupDefinitions[groupKey] = geometry; + + public bool TryGetDefinitionGeometry(PathKey groupKey, out List geometry) => + _groupDefinitions.TryGetValue(groupKey, out geometry); + + public Dictionary> GetAllDefinitionGeometries() => new(_groupDefinitions, PathKey.Comparer); + + public List GetAllGroupKeys() => _groupDefinitions.Keys.ToList(); + + public void RegisterInstanceObservation( + PathKey groupKey, + PathKey instancePath, + double[] instanceWorld, + PrimitiveProcessor processor + ) + { + if (instanceWorld == null) + { + throw new ArgumentNullException(nameof(instanceWorld)); + } + + if (instanceWorld.Length != 16) + { + throw new ArgumentException("Expected 16 doubles for a 4x4 matrix.", nameof(instanceWorld)); + } + + // Store instanceWorld for later retrieval (needed for unbaking validation) + SetInstanceWorld(instancePath, instanceWorld); + + var inv = GeometryHelpers.InvertRigid(instanceWorld); + { + var sig = GeometryHelpers.ComputeUnbakedAabb(processor, inv); + + if (!sig.IsValid) + { + return; + } + + if (_groupSignature.TryGetValue(groupKey, out Aabb _)) + { + return; + } + + _groupSignature[groupKey] = sig; + _groupToDefinitionWorld[groupKey] = instanceWorld; + } + } +} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Samples/SpeckleCoreLog20260108.txt b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Samples/SpeckleCoreLog20260108.txt new file mode 100644 index 000000000..0b4209fa8 --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Samples/SpeckleCoreLog20260108.txt @@ -0,0 +1,191 @@ +2026-01-08 10:51:03.776 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 10:51:05.537 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 10:51:05.554 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 10:51:05.554 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 10:58:37.951 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 10:58:39.205 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 11:02:08.672 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 11:02:10.209 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 11:26:14.925 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 11:26:16.553 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 11:26:16.565 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 11:26:16.565 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 11:26:16.565 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 11:26:16.566 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 11:26:16.566 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 11:26:16.566 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 11:37:00.295 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 11:37:01.596 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 11:37:01.604 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 11:37:01.604 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 11:50:22.903 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 11:50:26.086 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 11:50:26.106 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 11:50:26.106 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 11:50:26.108 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 11:50:26.108 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 11:50:26.108 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 11:50:26.109 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 12:30:43.116 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 12:34:11.025 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 12:35:49.650 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 12:37:52.006 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 12:39:59.783 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 12:40:40.564 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 12:41:30.382 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 13:37:46.631 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 13:41:25.630 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 13:45:17.514 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 13:49:52.715 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd +2026-01-08 14:13:08.532 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 14:13:13.320 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 14:19:54.036 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 14:19:55.983 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 14:19:55.997 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 14:19:55.997 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 14:19:55.997 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 14:19:55.998 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 14:19:55.998 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 14:19:55.998 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 14:20:41.471 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming. +2026-01-08 14:20:43.212 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding +2026-01-08 14:20:43.234 +04:00 [INF] Bridge bound to front end name testBinding +2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name configBinding +2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name accountsBinding +2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name selectionBinding +2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name sendBinding +2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name baseBinding +2026-01-08 14:21:39.602 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:21:50.199 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:22:09.387 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:22:19.184 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:22:48.117 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:22:50.623 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:22:51.498 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:23:20.216 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:24:33.297 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:24:34.115 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:24:34.619 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:24:40.552 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:25:02.785 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:29:09.648 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:29:57.011 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:30:35.781 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:31:57.387 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 +2026-01-08 14:33:59.100 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful +System.InvalidOperationException: Sequence contains no matching element + at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) + at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35 + at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89 + at Speckle.Connectors.DUI.Bindings.SendOperationManager.d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71 diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/ElementSelectionService.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/ElementSelectionService.cs new file mode 100644 index 000000000..baa0a864e --- /dev/null +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/ElementSelectionService.cs @@ -0,0 +1,46 @@ +using Speckle.InterfaceGenerator; +using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper; + +namespace Speckle.Converter.Navisworks.Services; + +[GenerateAutoInterface] +public class ElementSelectionService : IElementSelectionService +{ + private readonly Dictionary _visibleCache = new(); + + public string GetModelItemPath(NAV.ModelItem modelItem) => ResolveModelItemToIndexPath(modelItem); + + public NAV.ModelItem GetModelItemFromPath(string path) => ResolveIndexPathToModelItem(path); + + public bool IsVisible(NAV.ModelItem modelItem) + { + var key = modelItem.InstanceGuid; + if (_visibleCache.TryGetValue(key, out var isVisible)) + { + return isVisible; + } + + // Check and cache ancestors, short-circuit on first hidden + foreach (var item in modelItem.AncestorsAndSelf) + { + if (!_visibleCache.TryGetValue(item.InstanceGuid, out var visible)) + { + visible = !item.IsHidden; + _visibleCache[item.InstanceGuid] = visible; + } + + if (!visible) // Ancestor is hidden, item must be hidden + { + // Cache the result for this item too + _visibleCache[key] = false; + return false; + } + } + + // All ancestors visible + _visibleCache[key] = true; + return true; + } + + public IEnumerable GetGeometryNodes(NAV.ModelItem modelItem) => ResolveGeometryLeafNodes(modelItem); +} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/InstanceStoreManager.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/InstanceStoreManager.cs deleted file mode 100644 index f0222ed23..000000000 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/InstanceStoreManager.cs +++ /dev/null @@ -1,152 +0,0 @@ -// using Microsoft.Extensions.Logging; -using Speckle.Converter.Navisworks.Constants; -using Speckle.Sdk.Models; -using Speckle.Sdk.Models.Instances; - -namespace Speckle.Converter.Navisworks.Services; - -/// -/// Simple wrapper class that manages two SharedGeometryStores instances for dual instancing pattern. -/// Provides easy access to both mesh definitions store and instance definition proxies store. -/// -public class InstanceStoreManager( -// ILogger logger -) -{ - // private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - /// - /// Store for geometry definitions (geometry data) - untransformed base geometries. - /// - internal SharedGeometryStore GeometryDefinitionsStore { get; } = new(); - - /// - /// Store for InstanceDefinitionProxy objects that reference geometry definitions. - /// - internal SharedGeometryStore InstanceDefinitionProxiesStore { get; } = new(); - - /// - /// Clears both stores for a new conversion session. - /// Should be called at the start of each conversion. - /// - public void ClearAll() - { - GeometryDefinitionsStore.Clear(); - InstanceDefinitionProxiesStore.Clear(); - } - - /// - /// Gets all instance definition proxies from the store, cast to their specific type. - /// Useful for adding to root collection at end of conversion. - /// - public IReadOnlyCollection GetInstanceDefinitionProxies() - { - var proxies = InstanceDefinitionProxiesStore.Geometries.OfType().ToList().AsReadOnly(); - // _logger.LogDebug("GetInstanceDefinitionProxies returning {Count} proxies", proxies.Count); - return proxies; - } - - /// - /// Gets all geometry definitions from the geometry definitions store. - /// - /// - public List GetGeometryDefinitions() => [.. GeometryDefinitionsStore.Geometries.ToList().AsReadOnly()]; - - /// - /// Gets a geometry definition by its application ID from the geometry definitions store. - /// - /// The geometry if found, null otherwise. - public Base? GetGeometryDefinition(string fragmentId) => - GeometryDefinitionsStore.Geometries.FirstOrDefault(g => - g.applicationId == $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}" - ); - - /// - /// Gets an instance definition proxy by its application ID. - /// - /// The instance definition proxy if found, null otherwise. - public InstanceDefinitionProxy? GetInstanceDefinitionProxy(string fragmentId) => - InstanceDefinitionProxiesStore - .Geometries.OfType() - .FirstOrDefault(p => p.applicationId == $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}"); - - /// - /// Adds geometry definitions and corresponding instance definition proxy for shared geometry. - /// This is a convenience method that handles both stores in one call. - /// Supports all geometry primitive types (Mesh, Lines, Points). - /// - /// The fragment-based application ID. - /// The untransformed base geometries (meshes, lines, points). - /// True if geometries were added (new geometry), false if they already existed. - public bool AddSharedGeometry(string fragmentId, List geometries) - { - // _logger.LogDebug("AddSharedGeometry called for FragmentId={FragmentId}, GeometryCount={Count}", fragmentId, geometries.Count); - - if (geometries.Count == 0) - { - return false; - } - - var geometriesAdded = false; - var proxyAdded = false; - - // Create prefixed IDs using base fragment hash - var definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}"; - var geometryApplicationIds = new List(); - - // _logger.LogDebug("Using DefinitionId={DefinitionId}", definitionId); - - // Add each geometry definition with a unique index suffix - for (var i = 0; i < geometries.Count; i++) - { - var geometry = geometries[i]; - var geometryId = - geometries.Count == 1 - ? $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}" - : $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}_{i}"; - - if (!GeometryDefinitionsStore.Contains(geometryId)) - { - geometry.applicationId = geometryId; - var added = GeometryDefinitionsStore.Add(geometry); - geometriesAdded = geometriesAdded || added; - // _logger.LogDebug("Added geometry definition: {GeometryId}, Type={Type}, Success={Success}", geometryId, geometry.GetType().Name, added); - } - else - { - // _logger.LogDebug("Geometry definition already exists: {GeometryId}", geometryId); - } - - geometryApplicationIds.Add(geometryId); - } - - // Add instance definition proxy if not exists - if (!InstanceDefinitionProxiesStore.Contains(definitionId)) - { - var definitionProxy = new InstanceDefinitionProxy - { - applicationId = definitionId, - name = $"Shared Geometry {fragmentId[..8]}...", // Show first 8 chars for readability - objects = geometryApplicationIds, - maxDepth = 0 - }; - proxyAdded = InstanceDefinitionProxiesStore.Add(definitionProxy); - } - else - { - // _logger.LogDebug("Instance definition proxy already exists: {DefinitionId}", definitionId); - } - - var conversionSucceededResult = geometriesAdded || proxyAdded; - return conversionSucceededResult; - } - - /// - /// Checks if shared geometry already exists in the stores. - /// Uses the instance definition proxy as the authoritative check since it references all geometries. - /// - /// The fragment-based application ID. - /// True if the instance definition proxy exists for this fragment. - public bool ContainsSharedGeometry(string fragmentId) => - InstanceDefinitionProxiesStore.Contains($"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}"); -} diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/PropertyConversion.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/PropertyConversion.cs index 36497010d..21a669fbe 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/PropertyConversion.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/PropertyConversion.cs @@ -29,7 +29,7 @@ public class PropertyConverter(IUiUnitsCache uiUnitsCache) : IPropertyConverter { NAV.VariantDataType.IdentifierString, (v, _) => v.ToIdentifierString() }, { NAV.VariantDataType.Int32, (v, _) => v.ToInt32() }, { NAV.VariantDataType.Double, (v, _) => v.ToDouble() }, - // Angle as dictionary with units + // Angle as a dictionary with units { NAV.VariantDataType.DoubleAngle, (v, t) => NumObj(t.name, v.ToDoubleAngle(), "Degrees") }, // Length → dictionary in UI units { diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettings.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettings.cs index 8c07eff69..f270641a6 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettings.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettings.cs @@ -2,28 +2,23 @@ namespace Speckle.Converter.Navisworks.Settings; -/// -/// Represents the settings used for Navisworks conversions. -/// public record NavisworksConversionSettings(Derived Derived, User User); -// Derived from Navisworks Application public record Derived( - NAV.Document Document, // The active Navisworks document to be processed. - SafeBoundingBox ModelBoundingBox, // The bounding box of the model. - SafeVector TransformVector, // Transformation vector applied to the model. - bool IsUpright, // Indicates if the model's orientation is upright relative to canonical up. - string SpeckleUnits // Units used in Speckle for standardised measurements. + NAV.Document Document, + SafeBoundingBox ModelBoundingBox, + SafeVector TransformVector, + bool IsUpright, + string SpeckleUnits ); -// Optional settings for conversion to be offered in UI public record User( - OriginMode OriginMode, // Defines the base point for transformations. - bool IncludeInternalProperties, // Whether to include internal Navisworks properties in the output. - bool ConvertHiddenElements, // Whether to include hidden elements during the conversion process. - RepresentationMode VisualRepresentationMode, // Specifies the visual representation mode. - bool CoalescePropertiesFromFirstObjectAncestor, // Whether to merge properties from the first object ancestor. - bool ExcludeProperties, // Whether to exclude properties from the output. - bool PreserveModelHierarchy, // Whether to maintain the full model hierarchy during conversion. - bool RevitCategoryMapping // Optional mapping to Revit categories (if applicable). + OriginMode OriginMode, + bool IncludeInternalProperties, + bool ConvertHiddenElements, + RepresentationMode VisualRepresentationMode, + bool CoalescePropertiesFromFirstObjectAncestor, + bool ExcludeProperties, + bool PreserveModelHierarchy, + bool RevitCategoryMapping = true ); diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettingsFactory.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettingsFactory.cs index ddc3bbe58..826a5c34e 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettingsFactory.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Settings/NavisworksConversionSettingsFactory.cs @@ -7,39 +7,21 @@ using Speckle.InterfaceGenerator; namespace Speckle.Converter.Navisworks.Settings; [GenerateAutoInterface] -public class NavisworksConversionSettingsFactory : INavisworksConversionSettingsFactory +public class NavisworksConversionSettingsFactory( + IHostToSpeckleUnitConverter unitsConverter, + IConverterSettingsStore settingsStore, + ILogger logger +) : INavisworksConversionSettingsFactory { - private readonly IConverterSettingsStore _settingsStore; - private readonly ILogger _logger; - private readonly IHostToSpeckleUnitConverter _unitsConverter; - private NAV.Document? _document; private SafeBoundingBox _modelBoundingBox; private bool _convertHiddenElements; + private OriginMode _originMode; - public NavisworksConversionSettingsFactory( - IHostToSpeckleUnitConverter unitsConverter, - IConverterSettingsStore settingsStore, - ILogger logger - ) - { - _logger = logger; - _settingsStore = settingsStore; - _unitsConverter = unitsConverter; - } - - public NavisworksConversionSettings Current => _settingsStore.Current; + public NavisworksConversionSettings Current => settingsStore.Current; private static readonly NAV.Vector3D s_canonicalUp = new(0, 0, 1); - private OriginMode _originMode; - - /// - /// Creates a new instance of NavisworksConversionSettings with calculated values. - /// - /// - /// Thrown when no active document is found or document units cannot be converted. - /// public NavisworksConversionSettings Create( OriginMode originMode, RepresentationMode visualRepresentationMode, @@ -60,7 +42,7 @@ public class NavisworksConversionSettingsFactory : INavisworksConversionSettings throw new InvalidOperationException("No active document found."); } - var units = _unitsConverter.ConvertOrThrow(_document.Units); + var units = unitsConverter.ConvertOrThrow(_document.Units); if (string.IsNullOrEmpty(units)) { throw new InvalidOperationException("Document units could not be converted."); @@ -96,7 +78,7 @@ public class NavisworksConversionSettingsFactory : INavisworksConversionSettings private void InitializeDocument() { _document = NavisworksApp.ActiveDocument ?? throw new InvalidOperationException("No active document found."); - _logger.LogInformation("Creating settings for document: {DocumentName}", _document.Title); + logger.LogInformation("Creating settings for document: {DocumentName}", _document.Title); _modelBoundingBox = new SafeBoundingBox(_document.GetBoundingBox(_convertHiddenElements)); } @@ -109,36 +91,15 @@ public class NavisworksConversionSettingsFactory : INavisworksConversionSettings _ => throw new NotSupportedException($"OriginMode {_originMode} is not supported.") }; - /// - /// Calculates the transformation vector based on the project base point. - /// - /// The calculated transformation vector. - /// - /// This uses mocked project base point data and should be replaced with actual logic - /// when finally integrating with UI or external configurations. - /// private SafeVector CalculateProjectBasePointTransform() { - // TODO: Replace with actual logic to fetch project base point and units from UI or settings + // WARNING: Mocked data - replace with actual UI/settings when implementing project base point var projectBasePoint = new SafeVector(10, 20, 0); - // ReSharper disable once ConvertToConstant.Local var projectBasePointUnits = NAV.Units.Meters; - var scale = NAV.UnitConversion.ScaleFactor(projectBasePointUnits, _document!.Units); - - // The transformation vector is the negative of the project base point, scaled to the source units. - // These units are independent of the Speckle units, and because they are from user input. return new SafeVector(-projectBasePoint.X * scale, -projectBasePoint.Y * scale, 0); } - /// - /// Calculates the transformation vector based on the bounding box center offset from the origin. - /// - /// The calculated transformation vector. - /// - /// This uses the document active model bounding box center as the base point for the transformation. - /// Assumes no translation in the Z-axis. - /// private SafeVector CalculateBoundingBoxTransform() => new(-_modelBoundingBox.Center.X, -_modelBoundingBox.Center.Y, 0); } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems index 2523f527d..b87083ddf 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/Speckle.Converters.NavisworksShared.projitems @@ -9,39 +9,42 @@ Speckle.Converters.NavisworksShared - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/NavisworksRootToSpeckleConverter.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/NavisworksRootToSpeckleConverter.cs index fc79cbb8f..8a7b364a6 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/NavisworksRootToSpeckleConverter.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/NavisworksRootToSpeckleConverter.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Logging; using Speckle.Converter.Navisworks.Helpers; -using Speckle.Converter.Navisworks.Settings; using Speckle.Converters.Common; using Speckle.Converters.Common.Objects; using Speckle.Converters.Common.Registration; @@ -8,23 +6,9 @@ using Speckle.Sdk.Models; namespace Speckle.Converter.Navisworks.ToSpeckle; -public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter +public class NavisworksRootToSpeckleConverter(IConverterManager toSpeckle) + : IRootToSpeckleConverter { - private readonly IConverterManager _toSpeckle; - private readonly IConverterSettingsStore _settingsStore; - private readonly ILogger _logger; - - public NavisworksRootToSpeckleConverter( - IConverterSettingsStore settingsStore, - ILogger logger, - IConverterManager toSpeckle - ) - { - _toSpeckle = toSpeckle; - _logger = logger; - _settingsStore = settingsStore; - } - public Base Convert(object target) { if (target == null) @@ -38,7 +22,7 @@ public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter } Type type = target.GetType(); - var objectConverter = _toSpeckle.ResolveConverter(type, true); + var objectConverter = toSpeckle.ResolveConverter(type); Base result = objectConverter.Convert(modelItem); result.applicationId = ElementSelectionHelper.ResolveModelItemToIndexPath(modelItem); diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/BoundingBoxToSpeckleRawConverter.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/BoundingBoxToSpeckleRawConverter.cs index 43838ff4a..4e75f9583 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/BoundingBoxToSpeckleRawConverter.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/BoundingBoxToSpeckleRawConverter.cs @@ -6,28 +6,22 @@ using Speckle.Objects.Primitive; namespace Speckle.Converter.Navisworks.ToSpeckle.Raw; -public class BoundingBoxToSpeckleRawConverter : ITypedConverter +public class BoundingBoxToSpeckleRawConverter(IConverterSettingsStore settingsStore) + : ITypedConverter { - private readonly IConverterSettingsStore _settingsStore; - - public BoundingBoxToSpeckleRawConverter(IConverterSettingsStore settingsStore) - { - _settingsStore = settingsStore; - } - public Box Convert(object target) => Convert((NAV.BoundingBox3D)target); public Box Convert(NAV.BoundingBox3D? target) { if (target == null) { - return default!; // returns null for reference types (Box is a reference type) + return null!; // returns null for reference types (Box is a reference type) } var minPoint = target.Min; var maxPoint = target.Max; - var units = _settingsStore.Current.Derived.SpeckleUnits; + var units = settingsStore.Current.Derived.SpeckleUnits; var basePlane = new Plane { diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/GeometryToSpeckleConverter.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/GeometryToSpeckleConverter.cs index 788015d62..79bd99506 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/GeometryToSpeckleConverter.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/Raw/GeometryToSpeckleConverter.cs @@ -1,47 +1,44 @@ using System.Runtime.InteropServices; -using System.Security.Cryptography; -using System.Text; using Autodesk.Navisworks.Api.Interop.ComApi; -using Microsoft.Extensions.Logging; using Speckle.Converter.Navisworks.Constants; +using Speckle.Converter.Navisworks.Constants.Registers; using Speckle.Converter.Navisworks.Geometry; using Speckle.Converter.Navisworks.Helpers; -using Speckle.Converter.Navisworks.Services; +using Speckle.Converter.Navisworks.Paths; using Speckle.Converter.Navisworks.Settings; using Speckle.DoubleNumerics; using Speckle.Objects.Geometry; +using Speckle.Sdk.Models; using Speckle.Sdk.Models.Instances; +using static Speckle.Converter.Navisworks.Constants.InstanceConstants; using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge; +// ReSharper disable HeuristicUnreachableCode +#pragma warning disable CS0162 // Unreachable code detected namespace Speckle.Converter.Navisworks.ToSpeckle; -/// -/// Memory Safety: All COM objects (InwSelectionPathsColl, InwOaPath, InwOaFragmentList) are explicitly -/// released using Marshal.ReleaseComObject in try-finally blocks to prevent memory leaks. -/// NAV.Color objects are disposed using 'using' statements as they implement IDisposable. -/// -public class GeometryToSpeckleConverter( +/// +/// WARNING: Uses COM interop - cannot use public ModelGeometry API. +/// Process: ModelItem → InwOaPath3 → InwOaFragmentList → InwOaFragment3 → primitives → Speckle geometry +/// +/// COM overhead: ~13.7ms per item (99.5% of the time) - cannot be optimized from C# +/// All COM objects are properly released in try-finally blocks to prevent memory leaks. +/// +public sealed class GeometryToSpeckleConverter( NavisworksConversionSettings settings, - InstanceStoreManager instanceStoreManager, - ILogger logger + IInstanceFragmentRegistry registry ) { private readonly NavisworksConversionSettings _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - + private readonly IInstanceFragmentRegistry _registry = registry ?? throw new ArgumentNullException(nameof(registry)); private readonly bool _isUpright = settings.Derived.IsUpright; private readonly SafeVector _transformVector = settings.Derived.TransformVector; private const double SCALE = 1.0; - private static readonly Matrix4x4 s_identityMatrix = new(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); - private static readonly double[] s_identityTransform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + private const bool ENABLE_INSTANCING = true; + private readonly Dictionary _groupMemberCounts = new(PathKey.Comparer); - private readonly InstanceStoreManager _instanceStoreManager = - instanceStoreManager ?? throw new ArgumentNullException(nameof(instanceStoreManager)); - - private readonly ILogger _logger = - logger ?? throw new ArgumentNullException(nameof(logger)); - - internal List Convert(NAV.ModelItem modelItem) + internal List Convert(NAV.ModelItem modelItem) { if (modelItem == null) { @@ -53,10 +50,10 @@ public class GeometryToSpeckleConverter( return []; } - var comSelection = ComApiBridge.ToInwOpSelection([modelItem]); + NAV.ModelItemCollection collection = new() { modelItem }; + var comSelection = ComApiBridge.ToInwOpSelection(modelItemCollection: collection); try { - var fragmentStack = new Stack(); var paths = comSelection.Paths(); if (paths == null) { @@ -64,32 +61,87 @@ public class GeometryToSpeckleConverter( } try { - if (paths.Count > 0) - { - var firstPath = paths.Cast().First(); - var fragmentsCollection = firstPath.Fragments(); - try - { - if (fragmentsCollection.Count > 1) - { - return ProcessSharedGeometry(paths, fragmentStack); - } - } - finally - { - if (fragmentsCollection != null) - { - Marshal.ReleaseComObject(fragmentsCollection); - } - } - } + var allResults = new List(5); foreach (InwOaPath path in paths) { - CollectFragments(path, fragmentStack); + if (path.ArrayData is not Array pathArr) + { + continue; + } + + var itemPathKey = PathKey.FromComArray(pathArr); + + if (!_registry.TryGetGroup(itemPathKey, out var groupKey)) + { + var members = DiscoverInstancePathsFromFragments(path); + members.Add(itemPathKey); + groupKey = itemPathKey; + _registry.RegisterGroup(groupKey, members); + _groupMemberCounts[groupKey] = members.Count; + } + + var processor = new PrimitiveProcessor(_isUpright); + ProcessPathFragments(path, itemPathKey, groupKey, processor); + + if (!_registry.TryGetInstanceWorld(itemPathKey, out var instanceWorld)) + { + var geometries = ProcessGeometries([processor]); + _registry.MarkConverted(itemPathKey); + allResults.AddRange(geometries); + continue; + } + + if (_groupMemberCounts.TryGetValue(groupKey, out var memberCount) && memberCount == 1) + { + var geometries = ProcessGeometries([processor]); + _registry.MarkConverted(itemPathKey); + allResults.AddRange(geometries); + continue; + } + + if (ENABLE_INSTANCING && !_registry.HasDefinitionGeometry(groupKey)) + { + var geometries = ProcessGeometries([processor]); + + // Transform matrix to Z-up space if model is Y-up, matching vertex transformation + var transformedWorld = _isUpright ? instanceWorld : TransformMatrixYUpToZUp(instanceWorld); + var invDefWorld = GeometryHelpers.InvertRigid(transformedWorld); + var definitionGeometry = UnbakeGeometry(geometries, invDefWorld); + var groupKeyHash = groupKey.ToHashString(); + for (int i = 0; i < definitionGeometry.Count; i++) + { + definitionGeometry[i].applicationId = $"{GEOMETRY_ID_PREFIX}{groupKeyHash}_{i}"; + } + + _registry.StoreDefinitionGeometry(groupKey, definitionGeometry); + } + + if (ENABLE_INSTANCING) + { + // Transform matrix to Z-up space if model is Y-up, matching vertex transformation + var transformedWorld = _isUpright ? instanceWorld : TransformMatrixYUpToZUp(instanceWorld); + var instanceProxy = new InstanceProxy + { + definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{groupKey.ToHashString()}", + transform = ConvertToMatrix4X4(transformedWorld), + units = _settings.Derived.SpeckleUnits, + applicationId = $"{InstanceConstants.INSTANCE_ID_PREFIX}{itemPathKey.ToHashString()}", + maxDepth = 0 + }; + + _registry.MarkConverted(itemPathKey); + allResults.Add(instanceProxy); + } + else + { + var geometries = ProcessGeometries([processor]); + _registry.MarkConverted(itemPathKey); + allResults.AddRange(geometries); + } } - return ProcessFragments(fragmentStack, paths, true); + return allResults; } finally { @@ -103,18 +155,113 @@ public class GeometryToSpeckleConverter( Marshal.ReleaseComObject(comSelection); } } + collection.Dispose(); } - private static void CollectFragments(InwOaPath path, Stack fragmentStack) + private static HashSet DiscoverInstancePathsFromFragments(InwOaPath path) { + var set = new HashSet(PathKey.Comparer); var fragments = path.Fragments(); + try { - foreach (var fragment in fragments.OfType()) + foreach (InwOaFragment3 fragment in fragments.OfType()) { - if (AreFragmentPathsEqual(fragment, path)) + GC.KeepAlive(fragment); + + InwOaPath? fragPath = fragment.path; + if (fragPath?.ArrayData is not Array fragPathArr) { - fragmentStack.Push(fragment); + continue; + } + + var fragmentPathKey = PathKey.FromComArray(fragPathArr); + set.Add(fragmentPathKey); + + Marshal.ReleaseComObject(fragPath); + } + } + finally + { + if (fragments != null) + { + Marshal.ReleaseComObject(fragments); + } + } + + return set; + } + + private void ProcessPathFragments(InwOaPath path, PathKey itemPathKey, PathKey groupKey, PrimitiveProcessor processor) + { + var observed = false; + var fragments = path.Fragments(); + + try + { + foreach (InwOaFragment3 fragment in fragments.OfType()) + { + GC.KeepAlive(fragment); + + InwOaPath? fragPath = null; + InwLTransform3f3? transform = null; + + try + { + fragPath = fragment.path; + if (fragPath?.ArrayData is not Array fragPathArr) + { + continue; + } + + if (!itemPathKey.MatchesComArray(fragPathArr)) + { + continue; + } + + transform = fragment.GetLocalToWorldMatrix() as InwLTransform3f3; + if (transform == null) + { + continue; + } + + if (transform.Matrix is not Array matrixArray) + { + continue; + } + + var instanceWorld = ConvertArrayToDouble(matrixArray); + if (instanceWorld.Length != 16) + { + continue; + } + + processor.LocalToWorldTransformation = instanceWorld; + fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor); + + if (observed) + { + continue; + } + + if (processor.Triangles.Count <= 0 && processor.Lines.Count <= 0) + { + continue; + } + + _registry.RegisterInstanceObservation(groupKey, itemPathKey, instanceWorld, processor); + observed = true; + } + finally + { + if (transform != null) + { + Marshal.ReleaseComObject(transform); + } + if (fragPath != null) + { + Marshal.ReleaseComObject(fragPath); + } } } } @@ -127,95 +274,9 @@ public class GeometryToSpeckleConverter( } } - private List ProcessSharedGeometry(InwSelectionPathsColl paths, Stack fragmentStack) + private List ProcessGeometries(List processors) { - var fragmentId = GenerateFragmentId(paths); - - if (string.IsNullOrEmpty(fragmentId)) - { - foreach (InwOaPath path in paths) - { - CollectFragments(path, fragmentStack); - } - - return ProcessFragments(fragmentStack, paths, true); - } - - if (_instanceStoreManager.ContainsSharedGeometry(fragmentId)) - { - return CreateInstanceReference(fragmentId, paths); - } - - foreach (InwOaPath path in paths) - { - CollectFragments(path, fragmentStack); - } - - var baseGeometries = ExtractUntransformedGeometry(fragmentStack); - - return baseGeometries.Count == 0 || !_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometries) - ? ProcessFragments(fragmentStack, paths) // default false flag for isSingleObject - : CreateInstanceReference(fragmentId, paths); - } - - private List ProcessFragments( - Stack fragmentStack, - InwSelectionPathsColl paths, - bool isSingleObject = false - ) - { - var callbackListeners = new List(); - - foreach (InwOaPath path in paths) - { - var processor = new PrimitiveProcessor(_isUpright); - - foreach (var fragment in fragmentStack) - { - var matrix = fragment.GetLocalToWorldMatrix(); - var transform = matrix as InwLTransform3f3; - if (transform?.Matrix is not Array matrixArray) - { - continue; - } - - var fragmentsForCount = path.Fragments(); - int fragmentCount; - try - { - fragmentCount = fragmentsForCount?.Count ?? 0; - } - finally - { - if (fragmentsForCount != null) - { - Marshal.ReleaseComObject(fragmentsForCount); - } - } - - double[] makeNoChange = s_identityTransform; - double[] transformMatrix = ConvertArrayToDouble(matrixArray); - - processor.LocalToWorldTransformation = - isSingleObject || fragmentCount == 1 ? transformMatrix : (IEnumerable)makeNoChange; - - fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor); - } - - callbackListeners.Add(processor); - } - - return ProcessGeometries(callbackListeners); - } - - private static bool AreFragmentPathsEqual(InwOaFragment3 fragment, InwOaPath path) => - fragment.path?.ArrayData is Array fragmentPathData - && path.ArrayData is Array pathData - && AreFragmentPathsEqual(fragmentPathData, pathData); - - private List ProcessGeometries(List processors) - { - var baseGeometries = new List(); + var baseGeometries = new List(processors.Count * 2); foreach (var processor in processors) { @@ -225,17 +286,13 @@ public class GeometryToSpeckleConverter( baseGeometries.Add(mesh); } - if (processor.Lines.Count > 0) + if (processor.Lines.Count <= 0) { - var lines = CreateLines(processor.Lines); - baseGeometries.AddRange(lines); + continue; } - if (processor.Points.Count > 0) - { - var points = CreatePoints(processor.Points); - baseGeometries.AddRange(points); - } + var lines = CreateLines(processor.Lines); + baseGeometries.AddRange(lines); } return baseGeometries; @@ -243,13 +300,12 @@ public class GeometryToSpeckleConverter( private Mesh CreateMesh(IReadOnlyList triangles) { - var vertices = new List(); - var faces = new List(); + var vertices = new List(triangles.Count * 9); + var faces = new List(triangles.Count * 4); for (var t = 0; t < triangles.Count; t++) { var triangle = triangles[t]; - vertices.AddRange( [ (triangle.Vertex1.X + _transformVector.X) * SCALE, @@ -274,412 +330,35 @@ public class GeometryToSpeckleConverter( }; } - private List CreateLines(IReadOnlyList lines) => - lines - .Select(line => new Line - { - start = new Point( - (line.Start.X + _transformVector.X) * SCALE, - (line.Start.Y + _transformVector.Y) * SCALE, - (line.Start.Z + _transformVector.Z) * SCALE, - _settings.Derived.SpeckleUnits - ), - end = new Point( - (line.End.X + _transformVector.X) * SCALE, - (line.End.Y + _transformVector.Y) * SCALE, - (line.End.Z + _transformVector.Z) * SCALE, - _settings.Derived.SpeckleUnits - ), - units = _settings.Derived.SpeckleUnits - }) - .ToList(); - - private List CreatePoints(IReadOnlyList points) => - points - .Select(point => new Point( - (point.Vertex.X + _transformVector.X) * SCALE, - (point.Vertex.Y + _transformVector.Y) * SCALE, - (point.Vertex.Z + _transformVector.Z) * SCALE, - _settings.Derived.SpeckleUnits - )) - .ToList(); - - public string GenerateFragmentId(InwSelectionPathsColl paths) + private List CreateLines(IReadOnlyList lines) { - try + var result = new List(lines.Count); + + foreach (var line in lines) { - if (paths.Count == 0) - { - return string.Empty; - } - - var fragmentHashes = new List(); - - foreach (var fragments in from InwOaPath path in paths select path.Fragments()) - { - try + result.Add( + new Line { - var fragmentIndex = 0; - foreach (InwOaFragment3 fragment in fragments.OfType()) - { - if (fragment.path?.ArrayData is not Array pathData || pathData.Length == 0) - { - fragmentIndex++; - continue; - } - - try - { - if (pathData.Rank != 1) - { - var fragmentHashFallback = TrySimpleArrayEnumeration(pathData, fragmentIndex); - if (!string.IsNullOrEmpty(fragmentHashFallback)) - { - fragmentHashes.Add(fragmentHashFallback); - } - - fragmentIndex++; - continue; - } - - var lowerBound = pathData.GetLowerBound(0); - var upperBound = pathData.GetUpperBound(0); - - var arrayLength = upperBound - lowerBound + 1; - var pathInts = new int[arrayLength]; - - for (int i = lowerBound; i <= upperBound; i++) - { - try - { - var value = pathData.GetValue(i); - var arrayIndex = i - lowerBound; - pathInts[arrayIndex] = System.Convert.ToInt32(value); - } - catch (Exception ex) when (ex is COMException or InvalidCastException) - { - var errorType = ex is COMException ? "COM array access failed" : "Type conversion failed"; - _logger.LogDebug(ex, "{ErrorType} at index {Index}", errorType, i); - } - } - - var fragmentHash = string.Join("_", pathInts); - fragmentHashes.Add(fragmentHash); - } - catch (Exception ex) when (ex is COMException or IndexOutOfRangeException or RankException) - { - var errorType = ex switch - { - COMException => "COM access failed", - IndexOutOfRangeException => "Array bounds exceeded", - RankException => "Array rank mismatch", - _ => "Error" - }; - _logger.LogDebug( - ex, - "{ErrorType} processing fragment {FragmentIndex}, trying simple enumeration", - errorType, - fragmentIndex - ); - - var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex); - if (!string.IsNullOrEmpty(fragmentHash)) - { - fragmentHashes.Add(fragmentHash); - } - - fragmentIndex++; - continue; - } - - fragmentIndex++; - } + start = new Point( + (line.Start.X + _transformVector.X) * SCALE, + (line.Start.Y + _transformVector.Y) * SCALE, + (line.Start.Z + _transformVector.Z) * SCALE, + _settings.Derived.SpeckleUnits + ), + end = new Point( + (line.End.X + _transformVector.X) * SCALE, + (line.End.Y + _transformVector.Y) * SCALE, + (line.End.Z + _transformVector.Z) * SCALE, + _settings.Derived.SpeckleUnits + ), + units = _settings.Derived.SpeckleUnits } - finally - { - if (fragments != null) - { - Marshal.ReleaseComObject(fragments); - } - } - } - - if (fragmentHashes.Count > 0) - { - fragmentHashes.Sort(); - var rawData = string.Join("__", fragmentHashes); - var fragmentId = HashRawData(rawData); - return fragmentId; - } - - return string.Empty; + ); } - catch (Exception ex) when (ex is COMException or InvalidCastException or IndexOutOfRangeException) - { - var errorType = ex switch - { - COMException => "COM access failed", - InvalidCastException => "Type conversion failed", - IndexOutOfRangeException => "Array bounds exceeded", - _ => "Error" - }; - _logger.LogWarning(ex, "{ErrorType} generating fragment ID", errorType); - return string.Empty; - } - } - - private string TrySimpleArrayEnumeration(Array pathData, int fragmentIndex) - { - try - { - var values = new List(); - var maxAttempts = Math.Min(pathData.Length, 20); - - for (int i = 0; i < maxAttempts; i++) - { - try - { - var value = pathData.GetValue(i); - var convertedValue = System.Convert.ToInt32(value); - values.Add(convertedValue.ToString()); - } - catch (IndexOutOfRangeException) - { - break; - } - catch (InvalidCastException ex) - { - _logger.LogDebug(ex, "Type conversion failed at index {Index}", i); - } - } - - if (values.Count <= 0) - { - return string.Empty; - } - - return string.Join("_", values); - } - catch (COMException ex) - { - _logger.LogDebug(ex, "COM enumeration failed for fragment {FragmentIndex}", fragmentIndex); - return string.Empty; - } - } - - private static string HashRawData(string rawData) - { - using var sha256 = SHA256.Create(); - var inputBytes = Encoding.UTF8.GetBytes(rawData); - var hashBytes = sha256.ComputeHash(inputBytes); - return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); - } - - private List ExtractUntransformedGeometry(Stack fragmentStack) - { - var processor = new PrimitiveProcessor(_isUpright); - - foreach (var fragment in fragmentStack) - { - processor.LocalToWorldTransformation = s_identityTransform; - - fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor); - } - - var geometries = new List(); - - if (processor.Triangles.Count > 0) - { - geometries.Add(CreateMesh(processor.Triangles)); - } - - if (processor.Lines.Count > 0) - { - geometries.AddRange(CreateLines(processor.Lines)); - } - - if (processor.Points.Count > 0) - { - geometries.AddRange(CreatePoints(processor.Points)); - } - - return geometries; - } - - private List CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths) - { - var transform = ExtractInstanceTransform(paths); - - var instanceReference = new InstanceProxy - { - definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}", - transform = transform, - units = _settings.Derived.SpeckleUnits, - maxDepth = 0, - applicationId = Guid.NewGuid().ToString() - }; - - return [instanceReference]; - } - - private Matrix4x4 ExtractInstanceTransform(InwSelectionPathsColl paths) - { - try - { - if (paths.Count == 0) - { - return s_identityMatrix; - } - - var firstPath = paths.Cast().First(); - var fragments = firstPath.Fragments(); - try - { - if (fragments.Count == 0) - { - return s_identityMatrix; - } - - var fragmentStack = new Stack(); - - foreach (var frag in fragments.OfType()) - { - if (frag.path?.ArrayData is not Array pathData1 || firstPath.ArrayData is not Array pathData2) - { - continue; - } - - var pathArray1 = pathData1.Cast().ToArray(); - var pathArray2 = pathData2.Cast().ToArray(); - - if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2)) - { - fragmentStack.Push(frag); - } - } - - var fragment = fragmentStack.First(); - var matrix = fragment.GetLocalToWorldMatrix(); - - if (matrix is InwLTransform3f3 { Matrix: Array matrixArray }) - { - var transformArray = ConvertArrayToDouble(matrixArray); - var transformedMatrix = ApplyCoordinateTransform(transformArray); - - var newMatrix = new Matrix4x4( - transformedMatrix[0], - transformedMatrix[1], - transformedMatrix[2], - transformedMatrix[3], - transformedMatrix[4], - transformedMatrix[5], - transformedMatrix[6], - transformedMatrix[7], - transformedMatrix[8], - transformedMatrix[9], - transformedMatrix[10], - transformedMatrix[11], - transformedMatrix[12], - transformedMatrix[13], - transformedMatrix[14], - transformedMatrix[15] - ); - - return Matrix4x4.Transpose(newMatrix); - } - } - finally - { - if (fragments != null) - { - Marshal.ReleaseComObject(fragments); - } - } - } - catch (Exception ex) when (ex is COMException or InvalidCastException or NullReferenceException) - { - var errorType = ex switch - { - COMException => "COM access failed", - InvalidCastException => "Transform matrix type conversion failed", - NullReferenceException => "Null reference", - _ => "Error" - }; - _logger.LogWarning(ex, "{ErrorType} extracting instance transform", errorType); - } - - return s_identityMatrix; - } - - private double[] ApplyCoordinateTransform(double[] matrixArray) - { - var result = new double[16]; - Array.Copy(matrixArray, result, 16); - - // Apply Y-up to Z-up basis correction for non-upright models. - // When meshes are converted, vertices undergo the transformation (x, y, z) -> (x, -z, y) - // via TransformVectorToOrientation in PrimitiveProcessor. Instance transforms must be - // converted to the same basis using the conjugation: T_zup = C * T_yup * C^(-1) - // where C is the Y-up to Z-up change of basis matrix. - if (!_isUpright) - { - result = ApplyYUpToZUpBasisChange(result); - } - - result[12] = (result[12] + _transformVector.X) * SCALE; - result[13] = (result[13] + _transformVector.Y) * SCALE; - result[14] = (result[14] + _transformVector.Z) * SCALE; return result; } - /// - /// Applies the Y-up to Z-up basis change to a 4x4 transformation matrix. - /// This performs the conjugation T_zup = C * T_yup * C^(-1) where C represents - /// the coordinate transformation (x, y, z) -> (x, -z, y). - /// - /// - /// The change of basis matrix C and its inverse C^(-1) for (x, y, z) -> (x, -z, y): - /// C = | 1 0 0 0 | C^(-1) = | 1 0 0 0 | - /// | 0 0 -1 0 | | 0 0 1 0 | - /// | 0 1 0 0 | | 0 -1 0 0 | - /// | 0 0 0 1 | | 0 0 0 1 | - /// - /// For a matrix T with elements [m0...m15] in row-major order: - /// | m0 m1 m2 m3 | - /// | m4 m5 m6 m7 | - /// | m8 m9 m10 m11 | - /// | m12 m13 m14 m15 | - /// - /// The result of C * T * C^(-1) transforms the basis while preserving - /// the geometric meaning of the transformation in the new coordinate system. - /// - private static double[] ApplyYUpToZUpBasisChange(double[] m) => - // Compute C * T * C^(-1) where: - // C converts Y-up to Z-up: (x, y, z) -> (x, -z, y) - // This ensures instance transforms operate correctly on Z-up geometry. - // - // The multiplication is performed analytically for efficiency. - // Given input matrix m (row-major), the result is: - [ - m[0], - -m[2], - m[1], - m[3], // Row 0: unchanged x, swapped and negated y/z - -m[8], - m[10], - -m[9], - -m[11], // Row 1: from row 2, with sign changes - m[4], - -m[6], - m[5], - m[7], // Row 2: from row 1, with sign changes - m[12], - -m[14], - m[13], - m[15] // Row 3 (translation): swap y/z, negate new y - ]; - private static double[] ConvertArrayToDouble(Array arr) { if (arr.Rank != 1) @@ -696,6 +375,117 @@ public class GeometryToSpeckleConverter( return doubleArray; } - private static bool AreFragmentPathsEqual(Array a1, Array a2) => - a1.Length == a2.Length && a1.Cast().SequenceEqual(a2.Cast()); + /// + /// VALIDATION HELPER: Unbakes geometry from world space to definition space. Creates copies of the geometry and + /// applies inverse transform to move from world coordinates back to definition/local space. + /// Used for visual validation of instance detection. + /// + private static List UnbakeGeometry(List bakedGeometry, double[] invWorld) + { + var result = new List(bakedGeometry.Count); + + foreach (var item in bakedGeometry) + { + switch (item) + { + case Mesh mesh: + { + // Create a copy to avoid mutating the original + var unbaked = new Mesh + { + vertices = [.. mesh.vertices], + faces = mesh.faces, + units = mesh.units + }; + GeometryHelpers.UnbakeMeshVertices(unbaked, invWorld); + result.Add(unbaked); + break; + } + case Line line: + { + var unbaked = new Line + { + start = new Point(line.start.x, line.start.y, line.start.z, line.start.units), + end = new Point(line.end.x, line.end.y, line.end.z, line.end.units), + units = line.units + }; + GeometryHelpers.UnbakeLine(unbaked, invWorld); + result.Add(unbaked); + break; + } + default: + result.Add(item); // Pass through unknown types + break; + } + } + + return result; + } + + /// + /// Transforms a 4x4 matrix from Y-up to Z-up coordinate system. + /// Applies P * M * P^-1 where P is the coordinate transform (x, y, z) -> (x, -z, y). + /// + private static double[] TransformMatrixYUpToZUp(double[] m) + { + // P swaps Y↔Z with sign: (x,y,z) -> (x,-z,y) + // P = | 1 0 0 0 | P^-1 = | 1 0 0 0 | + // | 0 0 -1 0 | | 0 0 1 0 | + // | 0 1 0 0 | | 0 -1 0 0 | + // | 0 0 0 1 | | 0 0 0 1 | + // Result = P * M * P^-1 + + var result = new double[16]; // + + // Column 0 (X basis): unchanged in X, swap Y↔Z + result[0] = m[0]; + result[4] = m[8]; + result[8] = -m[4]; + result[12] = m[12]; + + // Column 1 (Y basis): comes from -Z + result[1] = -m[2]; + result[5] = -m[10]; + result[9] = m[6]; + result[13] = -m[14]; + + // Column 2 (Z basis): comes from Y + result[2] = m[1]; + result[6] = m[9]; + result[10] = -m[5]; + result[14] = m[13]; + + // Column 3 (homogeneous): unchanged + result[3] = m[3]; + result[7] = m[7]; + result[11] = m[11]; + result[15] = m[15]; + + return result; + } + + private static Matrix4x4 ConvertToMatrix4X4(double[] matrix) => + matrix.Length == 16 + ? Matrix4x4.Transpose( + new Matrix4x4 + { + M11 = matrix[0], + M12 = matrix[1], + M13 = matrix[2], + M14 = matrix[3], + M21 = matrix[4], + M22 = matrix[5], + M23 = matrix[6], + M24 = matrix[7], + M31 = matrix[8], + M32 = matrix[9], + M33 = matrix[10], + M34 = matrix[11], + M41 = matrix[12], + M42 = matrix[13], + M43 = matrix[14], + M44 = matrix[15] + } + ) + : throw new ArgumentException("Matrix must have exactly 16 elements", nameof(matrix)); } diff --git a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/TopLevel/ModelItemTopLevelConverterToSpeckle.cs b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/TopLevel/ModelItemTopLevelConverterToSpeckle.cs index 88094b2a3..014f773e2 100644 --- a/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/TopLevel/ModelItemTopLevelConverterToSpeckle.cs +++ b/Converters/Navisworks/Speckle.Converters.NavisworksShared/ToSpeckle/TopLevel/ModelItemTopLevelConverterToSpeckle.cs @@ -8,42 +8,20 @@ using Speckle.Sdk.Models.Collections; namespace Speckle.Converter.Navisworks.ToSpeckle; -/// -/// Converts Navisworks ModelItem objects to Speckle Base objects. -/// [NameAndRankValue(typeof(NAV.ModelItem), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)] -public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter +public class ModelItemToToSpeckleConverter( + IConverterSettingsStore settingsStore, + StandardPropertyHandler standardHandler, + HierarchicalPropertyHandler hierarchicalHandler, + DisplayValueExtractor displayValueExtractor +) : IToSpeckleTopLevelConverter { - private readonly StandardPropertyHandler _standardHandler; - private readonly HierarchicalPropertyHandler _hierarchicalHandler; - private readonly IConverterSettingsStore _settingsStore; - private readonly DisplayValueExtractor _displayValueExtractor; - - public ModelItemToToSpeckleConverter( - IConverterSettingsStore settingsStore, - StandardPropertyHandler standardHandler, - HierarchicalPropertyHandler hierarchicalHandler, - DisplayValueExtractor displayValueExtractor - ) - { - _settingsStore = settingsStore; - _standardHandler = standardHandler; - _hierarchicalHandler = hierarchicalHandler; - _displayValueExtractor = displayValueExtractor; - } - - /// - /// Converts a Navisworks object to a Speckle Base object. - /// - /// The object to convert. - /// The converted Speckle Base object. public Base Convert(object target) => target == null ? throw new ArgumentNullException(nameof(target)) : Convert((NAV.ModelItem)target); - // Converts a Navisworks ModelItem into a Speckle Base object private Base Convert(NAV.ModelItem target) { - IPropertyHandler handler = ShouldMergeProperties(target) ? _hierarchicalHandler : _standardHandler; + IPropertyHandler handler = ShouldMergeProperties(target) ? hierarchicalHandler : standardHandler; var name = GetObjectName(target); return target.HasGeometry @@ -51,28 +29,29 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter : CreateNonGeometryObject(target, name, handler); } - // There are in fact only two types of objects: geometry and non-geometry, the latter being collections of other objects - private NavisworksObject CreateGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) => - new() + private NavisworksObject CreateGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) + { + var displayValue = displayValueExtractor.GetDisplayValue(target); + + var geometryObject = new NavisworksObject { + units = settingsStore.Current.Derived.SpeckleUnits, name = name, - displayValue = _displayValueExtractor.GetDisplayValue(target), - properties = _settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target), - units = _settingsStore.Current.Derived.SpeckleUnits, + properties = settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target), + displayValue = displayValue }; + return geometryObject; + } + private Collection CreateNonGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) => new() { name = name, elements = [], - ["properties"] = _settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target), + ["properties"] = settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target), }; - /// - /// Determines whether properties should be merged from ancestors. - /// Only geometry objects should have their properties merged.... For now. - /// private static bool ShouldMergeProperties(NAV.ModelItem target) => target.HasGeometry; private static string GetObjectName(NAV.ModelItem target) @@ -81,8 +60,6 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter var firstObjectAncestor = target.FindFirstObjectAncestor(); - // while the target node name is null keep cycling through parent objects until displayname is not null or empty OR object is firstObjectAncestor - while (string.IsNullOrEmpty(targetName) && target != firstObjectAncestor) { target = target.Parent;