Compare commits

..

1 Commits

Author SHA1 Message Date
Dimitrie Stefanescu 22c6303167 wip pipe implementation 2026-01-21 14:23:27 +00:00
87 changed files with 1725 additions and 3144 deletions
@@ -6,22 +6,32 @@ using Speckle.Sdk;
namespace Speckle.Connector.Navisworks.Bindings;
public class NavisworksBasicConnectorBinding(
IBrowserBridge parent,
DocumentModelStore store,
ISpeckleApplication speckleApplication
) : IBasicConnectorBinding
public class NavisworksBasicConnectorBinding : IBasicConnectorBinding
{
public string Name => "baseBinding";
public IBrowserBridge Parent { get; } = parent;
public IBrowserBridge Parent { get; }
public BasicConnectorBindingCommands Commands { get; }
public BasicConnectorBindingCommands Commands { get; } = new(parent);
private readonly DocumentModelStore _store;
private readonly ISpeckleApplication _speckleApplication;
public string GetSourceApplicationName() => speckleApplication.Slug;
public NavisworksBasicConnectorBinding(
IBrowserBridge parent,
DocumentModelStore store,
ISpeckleApplication speckleApplication
)
{
Parent = parent;
_store = store;
_speckleApplication = speckleApplication;
Commands = new BasicConnectorBindingCommands(parent);
}
public string GetSourceApplicationVersion() => speckleApplication.HostApplicationVersion;
public string GetSourceApplicationName() => _speckleApplication.Slug;
public string GetConnectorVersion() => speckleApplication.SpeckleVersion;
public string GetSourceApplicationVersion() => _speckleApplication.HostApplicationVersion;
public string GetConnectorVersion() => _speckleApplication.SpeckleVersion;
public DocumentInfo? GetDocumentInfo() =>
NavisworksApp.ActiveDocument is null || NavisworksApp.ActiveDocument.Models.Count == 0
@@ -32,15 +42,15 @@ public class NavisworksBasicConnectorBinding(
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<ModelCard> models) => store.RemoveModels(models);
public void RemoveModels(List<ModelCard> models) => _store.RemoveModels(models);
public Task HighlightModel(string modelCardId) => Task.CompletedTask;
@@ -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;
@@ -12,7 +12,6 @@ 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;
@@ -59,12 +58,12 @@ public class NavisworksSendBinding : ISendBinding
private static void SubscribeToNavisworksEvents() { }
// WARNING: Changes to filter behavior here must match everywhere filters are used, or saved sets won't update correctly
// 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.
public List<ISendFilter> GetSendFilters() =>
[
new NavisworksSelectionFilter() { IsDefault = true },
new NavisworksSavedSetsFilter(new ConnectorElementSelectionService()),
new NavisworksSavedViewsFilter(new ConnectorElementSelectionService())
new NavisworksSavedSetsFilter(new ElementSelectionService()),
new NavisworksSavedViewsFilter(new ElementSelectionService())
];
public List<ICardSetting> GetSendSettings() =>
@@ -106,7 +105,6 @@ 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
@@ -117,78 +115,30 @@ public class NavisworksSendBinding : ISendBinding
{
throw new SpeckleSendFilterException(message);
}
onOperationProgressed.Report(new CardProgress("Getting selection...", null));
await Task.CompletedTask;
int estimatedCapacity = selectedPaths.Count * 10;
var modelItems = new List<NAV.ModelItem>(estimatedCapacity);
var modelItems = new List<NAV.ModelItem>();
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);
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);
}
}
modelItems.AddRange(_selectionService.GetGeometryNodes(modelItem).Where(_selectionService.IsVisible));
count++;
}
return modelItems.Count == 0 ? throw new SpeckleSendFilterException(message) : modelItems;
}
public void CancelSend(string modelCardId) => _cancellationManager.CancelOperation(modelCardId);
/// <summary>
/// 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.
/// </summary>
public void CancelAllSendOperations()
{
foreach (var modelCardId in _store.GetSenders().Select(m => m.ModelCardId))
@@ -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.Constants.Registers;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using Speckle.Sdk.Models.GraphTraversal;
@@ -53,6 +53,9 @@ public static class NavisworksConnectorServiceRegistration
serviceCollection.AddScoped<NavisworksMaterialUnpacker>();
serviceCollection.AddScoped<NavisworksColorUnpacker>();
// Register dual shared geometry stores for instancing pattern
serviceCollection.AddScoped<InstanceStoreManager>();
serviceCollection.AddSingleton<IAppIdleManager, NavisworksIdleManager>();
// Sending operations
@@ -61,9 +64,6 @@ public static class NavisworksConnectorServiceRegistration
serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc());
serviceCollection.AddSingleton<IOperationProgressManager, OperationProgressManager>();
// Registers and caches
serviceCollection.AddScoped<IInstanceFragmentRegistry, InstanceFragmentRegistry>();
// Register Intercom/interop
serviceCollection.AddSingleton<NavisworksDocumentModelStore>();
serviceCollection.AddSingleton<DocumentModelStore>(sp => sp.GetRequiredService<NavisworksDocumentModelStore>());
@@ -73,9 +73,6 @@ public static class NavisworksConnectorServiceRegistration
serviceCollection.AddScoped<ISendFilter, NavisworksSelectionFilter>();
serviceCollection.AddScoped<ISendFilter, NavisworksSavedSetsFilter>();
serviceCollection.AddScoped<ISendFilter, NavisworksSavedViewsFilter>();
serviceCollection.AddScoped<
Converter.Navisworks.Services.IElementSelectionService,
ConnectorElementSelectionService
>();
serviceCollection.AddScoped<IElementSelectionService, ElementSelectionService>();
}
}
@@ -1,12 +1,31 @@
namespace Speckle.Connector.Navisworks.Services;
using Speckle.InterfaceGenerator;
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
/// <summary>
/// Connector-specific element selection service that extends the converter's base implementation.
/// Inherits the cached visibility checking and path resolution from the converter layer.
/// </summary>
public class ConnectorElementSelectionService : Converter.Navisworks.Services.ElementSelectionService
namespace Speckle.Connector.Navisworks.Services;
[GenerateAutoInterface]
public class ElementSelectionService : IElementSelectionService
{
// 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
private readonly Dictionary<Guid, bool> _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<NAV.ModelItem> GetGeometryNodes(NAV.ModelItem modelItem) => ResolveGeometryLeafNodes(modelItem);
}
@@ -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,20 +130,16 @@ public class NavisworksColorUnpacker(
var comSelection = ComBridge.ToInwOpSelection([modelItem]);
try
{
var paths = comSelection.Paths();
var pathsCollection = comSelection.Paths();
try
{
foreach (ComApi.InwOaPath path in paths)
foreach (ComApi.InwOaPath path in pathsCollection)
{
GC.KeepAlive(path);
var fragments = path.Fragments();
var fragmentsCollection = path.Fragments();
try
{
foreach (ComApi.InwOaFragment3 fragment in fragments)
foreach (ComApi.InwOaFragment3 fragment in fragmentsCollection.OfType<ComApi.InwOaFragment3>())
{
GC.KeepAlive(fragment);
fragment.GenerateSimplePrimitives(ComApi.nwEVertexProperty.eNORMAL, primitiveChecker);
if (primitiveChecker.HasTriangles)
@@ -154,9 +150,9 @@ public class NavisworksColorUnpacker(
}
finally
{
if (fragments != null)
if (fragmentsCollection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragments);
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragmentsCollection);
}
}
}
@@ -165,9 +161,9 @@ public class NavisworksColorUnpacker(
}
finally
{
if (paths != null)
if (pathsCollection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(paths);
System.Runtime.InteropServices.Marshal.ReleaseComObject(pathsCollection);
}
}
}
@@ -120,6 +120,17 @@ 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;
@@ -1,18 +1,22 @@
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<NavisworksMaterialUnpacker> logger,
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
IElementSelectionService selectionService
IElementSelectionService selectionService,
GeometryToSpeckleConverter converter
)
{
private static T SelectByRepresentationMode<T>(
@@ -70,6 +74,66 @@ 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<InwOaPath>().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;
@@ -98,7 +162,7 @@ public class NavisworksMaterialUnpacker(
);
var materialName =
$"{DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
$"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
var itemCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Item");
if (itemCategory != null)
@@ -124,14 +188,14 @@ public class NavisworksMaterialUnpacker(
if (renderMaterialProxies.TryGetValue(renderMaterialId.ToString(), out RenderMaterialProxy? value))
{
value.objects.Add(finalId);
value.objects.Add(!string.IsNullOrEmpty(hashId) ? hashId : finalId);
}
else
{
renderMaterialProxies[renderMaterialId.ToString()] = new RenderMaterialProxy()
{
value = CreateRenderMaterial(materialName, renderTransparency, renderColor, renderMaterialId),
objects = [finalId]
objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId]
};
}
}
@@ -155,7 +219,9 @@ public class NavisworksMaterialUnpacker(
var speckleRenderMaterial = new RenderMaterial()
{
name = !string.IsNullOrEmpty(name) ? name : $"{DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}",
name = !string.IsNullOrEmpty(name)
? name
: $"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}",
opacity = 1 - transparency,
metalness = 0,
roughness = 1,
@@ -1,7 +1,7 @@
using Speckle.Connectors.DUI.Exceptions;
using Speckle.Connector.Navisworks.Services;
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;
@@ -1,7 +1,7 @@
using Speckle.Connectors.DUI.Exceptions;
using Speckle.Connector.Navisworks.Services;
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,6 +48,8 @@ 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.
@@ -80,12 +82,12 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect
{
var objectIds = new List<string>();
// THIS IS COMMENTED OUT AS IT IS LEGACY DEFENSIVE BEHAVIOR - DISCUSSION REQUIRED
// THIS IS COMMENTED OUT AS IT IS LEGACY DEFENSIVE BEHAVIOUR - 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 behavior.
// // This will need to be the documented behaviour.
// throw new SpeckleSendFilterException(
// "Saved view does not contain visibility overrides. This would effectively publish everything in the model."
// );
@@ -152,7 +154,7 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect
switch (item)
{
// case NAV.SavedViewpoint { ContainsVisibilityOverrides: false }:
// Legacy defensive behavior: skip viewpoints without visibility overrides.
// Legacy defensive behaviour: skip viewpoints without visibility overrides.
// Essentially, send everything, or whatever the current view state for hidden elements is.
// break;
case NAV.SavedViewpointAnimationCut:
@@ -1,4 +1,4 @@
using static Speckle.Converter.Navisworks.Constants.PathConstants;
using Speckle.Converter.Navisworks.Constants;
namespace Speckle.Connector.Navisworks.Operations.Send.Filters;
@@ -15,6 +15,6 @@ public static class SavedItemHelpers
current = current.Parent;
}
return string.Join(SET_SEPARATOR, pathParts);
return string.Join(PathConstants.SET_SEPARATOR, pathParts);
}
}
@@ -1,5 +1,5 @@
using Speckle.Connector.Navisworks.Services;
using static Speckle.Converter.Navisworks.Constants.PathConstants;
using Speckle.Converter.Navisworks.Constants;
namespace Speckle.Connector.Navisworks.Operations.Send;
@@ -10,29 +10,29 @@ public static class GeometryNodeMerger
{
/// <summary>
/// Groups sibling geometry nodes based on material properties for merging.
/// This only merges nodes that share the same parent and have identical material properties.
/// Only merges nodes that share the same parent and have identical material properties.
/// </summary>
/// <param name="nodes">The collection of ModelItems to process</param>
/// <returns>Dictionary mapping parent paths (with material signature suffix) to their mergeable child nodes</returns>
public static Dictionary<string, List<NAV.ModelItem>> GroupSiblingGeometryNodes(IReadOnlyList<NAV.ModelItem> nodes)
{
var selectionService = new ConnectorElementSelectionService();
var selectionService = new ElementSelectionService();
// 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 the parent path
// Get parent path
var path = selectionService.GetModelItemPath(node);
var lastSeparatorIndex = path.LastIndexOf(SEPARATOR);
var lastSeparatorIndex = path.LastIndexOf(PathConstants.SEPARATOR);
var parentPath = lastSeparatorIndex == -1 ? path : path[..lastSeparatorIndex];
// Generate material signature
string signature = GenerateSignature(node);
// Combine parent path with signature
return $"{parentPath}{MATERIAL_SEPARATOR}{signature}";
return $"{parentPath}{PathConstants.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 a consistent order
// Sort keys to ensure consistent order
var sortedKeys = properties.Keys.OrderBy(k => k).ToList();
foreach (var key in sortedKeys)
@@ -139,7 +139,7 @@ public static class GeometryNodeMerger
/// </summary>
private static string GetMaterialName(NAV.ModelItem node)
{
// Check the Item category for material name
// Check Item category for material name
var itemCategory = node.PropertyCategories.FindCategoryByDisplayName("Item");
if (itemCategory != null)
{
@@ -1,8 +1,8 @@
using Speckle.Converter.Navisworks.Services;
using Speckle.Connector.Navisworks.Services;
using Speckle.Converter.Navisworks.Constants;
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 == SEPARATOR);
var depthB = b.Count(c => c == SEPARATOR);
var depthA = a.Count(c => c == PathConstants.SEPARATOR);
var depthB = b.Count(c => c == PathConstants.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(SEPARATOR);
var idx = path.LastIndexOf(PathConstants.SEPARATOR);
return idx == -1 ? string.Empty : path[..idx];
}
@@ -1,5 +1,6 @@
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;
@@ -13,10 +14,7 @@ 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;
@@ -28,15 +26,14 @@ public class NavisworksRootObjectBuilder(
ISdkActivityFactory activityFactory,
NavisworksMaterialUnpacker materialUnpacker,
NavisworksColorUnpacker colorUnpacker,
Speckle.Converter.Navisworks.Constants.Registers.IInstanceFragmentRegistry instanceRegistry,
IElementSelectionService elementSelectionService,
IUiUnitsCache uiUnitsCache
IUiUnitsCache uiUnitsCache,
InstanceStoreManager instanceStoreManager
) : IRootObjectBuilder<NAV.ModelItem>
{
#pragma warning disable CA1823
#pragma warning restore CA1823
private bool SkipNodeMerging { get; set; }
private bool DisableGroupingForInstanceTesting { get; set; }
internal NavisworksConversionSettings GetCurrentSettings() => converterSettings.Current;
public async Task<RootObjectBuilderResult> Build(
IReadOnlyList<NAV.ModelItem> navisworksModelItems,
@@ -46,14 +43,14 @@ public class NavisworksRootObjectBuilder(
)
{
#if DEBUG
SkipNodeMerging = false;
DisableGroupingForInstanceTesting = false;
SkipNodeMerging = true;
#endif
using var activity = activityFactory.Start("Build");
ValidateInputs(navisworksModelItems, projectId, onOperationProgressed);
var rootCollection = InitializeRootCollection();
(Dictionary<string, Base?> convertedElements, List<SendConversionResult> conversionResults) =
await ConvertModelItemsAsync(navisworksModelItems, projectId, onOperationProgressed, cancellationToken);
@@ -61,17 +58,30 @@ public class NavisworksRootObjectBuilder(
var groupedNodes = SkipNodeMerging ? [] : GroupSiblingGeometryNodes(navisworksModelItems);
var finalElements = BuildFinalElements(convertedElements, groupedNodes);
List<Base> geometryDefinitions = instanceStoreManager.GetGeometryDefinitions();
await AddProxiesToCollection(rootCollection, navisworksModelItems, groupedNodes);
AddInstanceDefinitionsToCollection(rootCollection, ref finalElements);
int finalInstanceProxyCount = CountInstanceProxiesRecursive(finalElements);
logger.LogInformation(
"Final output contains {count} InstanceProxy objects in displayValues",
finalInstanceProxyCount
);
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);
}
rootCollection.elements = finalElements;
return new RootObjectBuilderResult(rootCollection, conversionResults);
}
@@ -117,32 +127,16 @@ public class NavisworksRootObjectBuilder(
var convertedBases = new Dictionary<string, Base?>();
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<Base> 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));
}
@@ -161,24 +155,10 @@ public class NavisworksRootObjectBuilder(
{
var finalElements = new List<Base>();
var processedPaths = new HashSet<string>();
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");
}
AddGroupedElements(finalElements, convertedBases, groupedNodes, processedPaths);
if (converterSettings.Current.User.PreserveModelHierarchy)
{
logger.LogInformation("Building hierarchy (PreserveModelHierarchy=true)");
var hierarchyBuilder = new NavisworksHierarchyBuilder(
convertedBases,
rootToSpeckleConverter,
@@ -188,10 +168,7 @@ 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;
}
@@ -204,7 +181,7 @@ public class NavisworksRootObjectBuilder(
{
foreach (var group in groupedNodes)
{
var siblingBases = new List<Base>(group.Value.Count);
var siblingBases = new List<Base>();
foreach (var itemPath in group.Value.Select(elementSelectionService.GetModelItemPath))
{
processedPaths.Add(itemPath);
@@ -259,29 +236,10 @@ public class NavisworksRootObjectBuilder(
string cleanParentPath = ElementSelectionHelper.GetCleanPath(groupKey);
(string name, string path) = GetElementNameAndPath(cleanParentPath);
int estimatedCapacity = siblingBases.Sum(b => (b["displayValue"] as List<Base>)?.Count ?? 0);
var displayValues = new List<Base>(estimatedCapacity);
displayValues.AddRange(
siblingBases
.Where(sibling => sibling["displayValue"] is List<Base>)
.SelectMany(sibling => (List<Base>)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 = displayValues,
displayValue = siblingBases.SelectMany(b => b["displayValue"] as List<Base> ?? []).ToList(),
properties = siblingBases.First()["properties"] as Dictionary<string, object?> ?? [],
units = converterSettings.Current.Derived.SpeckleUnits,
applicationId = groupKey,
@@ -322,100 +280,25 @@ public class NavisworksRootObjectBuilder(
var renderMaterials = materialUnpacker.UnpackRenderMaterial(navisworksModelItems, groupedNodes);
if (renderMaterials.Count > 0)
{
rootCollection[RENDER_MATERIAL] = renderMaterials;
rootCollection[ProxyKeys.RENDER_MATERIAL] = renderMaterials;
}
var colors = colorUnpacker.UnpackColor(navisworksModelItems, groupedNodes);
if (colors.Count > 0)
{
rootCollection[COLOR] = colors;
rootCollection[ProxyKeys.COLOR] = colors;
}
var instanceDefinitionProxies = instanceStoreManager.GetInstanceDefinitionProxies();
if (instanceDefinitionProxies.Count > 0)
{
rootCollection[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies.ToList();
}
return Task.CompletedTask;
}
private void AddInstanceDefinitionsToCollection(Collection rootCollection, ref List<Base> 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<InstanceDefinitionProxy>(allDefinitions.Count);
int estimatedGeometryCount = allDefinitions.Sum(kvp => kvp.Value.Count);
var allDefinitionGeometries = new List<Base>(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<Base> elements)
{
int count = 0;
foreach (var element in elements)
{
if (element["displayValue"] is List<Base> 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<string, Base?> convertedBases,
@@ -1,4 +1,5 @@
using Speckle.Connectors.Common.Caching;
using System.Diagnostics.CodeAnalysis;
using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.DUI.Models.Card;
using Speckle.Converter.Navisworks.Settings;
using Speckle.InterfaceGenerator;
@@ -7,106 +8,158 @@ using Speckle.Sdk.Common;
namespace Speckle.Connector.Navisworks.Operations.Send.Settings;
[GenerateAutoInterface]
public class ToSpeckleSettingsManagerNavisworks(ISendConversionCache sendConversionCache)
: IToSpeckleSettingsManagerNavisworks
public class ToSpeckleSettingsManagerNavisworks : IToSpeckleSettingsManagerNavisworks
{
// cache invalidation process run with ModelCardId since the settings are model-specific
private readonly ISendConversionCache _sendConversionCache;
// cache invalidation process run with ModelCardId since the settings are model specific
private readonly Dictionary<string, RepresentationMode> _visualRepresentationCache = [];
private readonly Dictionary<string, OriginMode> _originModeCache = [];
private readonly Dictionary<string, bool> _convertHiddenElementsCache = [];
private readonly Dictionary<string, bool> _includeInternalPropertiesCache = [];
private readonly Dictionary<string, bool> _preserveModelHierarchyCache = [];
private readonly Dictionary<string, bool> _revitCategoryMappingCache = [];
private readonly Dictionary<string, bool?> _convertHiddenElementsCache = [];
private readonly Dictionary<string, bool?> _includeInternalPropertiesCache = [];
private readonly Dictionary<string, bool?> _preserveModelHierarchyCache = [];
private readonly Dictionary<string, bool?> _revitCategoryMappingCache = [];
/// <summary>
/// Generic helper to get a setting value with caching and cache invalidation.
/// </summary>
private T GetCachedSetting<T>(
SenderModelCard modelCard,
string settingId,
Dictionary<string, T> cache,
Func<object?, T> valueExtractor,
T defaultValue
)
public ToSpeckleSettingsManagerNavisworks(ISendConversionCache sendConversionCache)
{
_sendConversionCache = sendConversionCache;
}
public RepresentationMode GetVisualRepresentationMode(SenderModelCard modelCard)
{
if (modelCard == null)
{
throw new ArgumentNullException(nameof(modelCard));
}
var settingValue = modelCard.Settings?.FirstOrDefault(s => s.Id == settingId)?.Value;
var returnValue = settingValue != null ? valueExtractor(settingValue) : defaultValue;
var representationString = modelCard.Settings?.First(s => s.Id == "visualRepresentation").Value as string;
if (
cache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue)
&& !EqualityComparer<T>.Default.Equals(previousValue, returnValue)
representationString is not null
&& VisualRepresentationSetting.VisualRepresentationMap.TryGetValue(
representationString,
out RepresentationMode representation
)
)
{
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);
}
cache[modelCard.ModelCardId.NotNull()] = returnValue;
_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;
return returnValue;
}
public RepresentationMode GetVisualRepresentationMode(SenderModelCard modelCard) =>
GetCachedSetting(
modelCard,
"visualRepresentation",
_visualRepresentationCache,
value =>
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)
{
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
);
EvictCacheForModelCard(modelCard);
}
}
public OriginMode GetOriginMode(SenderModelCard modelCard) =>
GetCachedSetting(
modelCard,
"originMode",
_originModeCache,
value =>
_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)
{
var originString = value as string;
if (OriginModeSetting.OriginModeMap.TryGetValue(originString ?? string.Empty, out var origin))
{
return origin;
}
return OriginMode.ModelOrigin;
},
OriginMode.ModelOrigin
);
EvictCacheForModelCard(modelCard);
}
}
public bool GetMappingToRevitCategories(SenderModelCard modelCard) =>
GetCachedSetting(modelCard, "mappingToRevitCategories", _revitCategoryMappingCache, value => value is true, false);
_includeInternalPropertiesCache[modelCard.ModelCardId] = returnValue;
return returnValue;
}
public bool GetConvertHiddenElements(SenderModelCard modelCard) =>
GetCachedSetting(modelCard, "convertHiddenElements", _convertHiddenElementsCache, 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 GetIncludeInternalProperties(SenderModelCard modelCard) =>
GetCachedSetting(
modelCard,
"includeInternalProperties",
_includeInternalPropertiesCache,
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 GetPreserveModelHierarchy(SenderModelCard modelCard) =>
GetCachedSetting(modelCard, "preserveModelHierarchy", _preserveModelHierarchyCache, value => value is true, false);
_preserveModelHierarchyCache[modelCard.ModelCardId] = returnValue;
return returnValue;
}
private void EvictCacheForModelCard(SenderModelCard modelCard)
{
var objectIds = modelCard.SendFilter != null ? modelCard.SendFilter.NotNull().SelectedObjectIds : [];
sendConversionCache.EvictObjects(objectIds);
_sendConversionCache.EvictObjects(objectIds);
}
}
@@ -14,7 +14,7 @@ public static class SpeckleV3Tool
public const string RIBBON_STRINGS = "NavisworksRibbon.name";
public const string PLUGIN_SUFFIX = ".Speckle";
public static Sdk.Application App =>
public static Speckle.Sdk.Application App =>
#if NAVIS
HostApplications.Navisworks;
#else
@@ -3,15 +3,17 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:dui="clr-namespace:Speckle.Connectors.DUI;assembly=Speckle.Connectors.DUI"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<!-- WebView2 is created lazily -->
<Border Name="BrowserContainer" Background="White">
<TextBlock Name="LoadingText"
Text="Loading Speckle..."
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="14"
Foreground="Gray" />
</Border>
<UserControl.Resources>
<wv2:CoreWebView2CreationProperties x:Key="EvergreenWebView2CreationProperties" UserDataFolder="C:\temp" />
</UserControl.Resources>
<DockPanel>
<wv2:WebView2
CreationProperties="{StaticResource EvergreenWebView2CreationProperties}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Name="Browser" Grid.Row="0" Source="{x:Static dui:Url.Netlify}" />
</DockPanel>
</UserControl>
@@ -2,8 +2,6 @@ using System.Windows.Controls;
using System.Windows.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using Speckle.Connectors.DUI;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.Revit.Plugin;
@@ -14,10 +12,6 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
{
private readonly IServiceProvider _serviceProvider;
private readonly IRevitTask _revitTask;
#pragma warning disable CA2213
private WebView2? _browser;
#pragma warning restore CA2213
private bool _isInitializing;
public RevitControlWebView(IServiceProvider serviceProvider, IRevitTask revitTask)
{
@@ -25,61 +19,35 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
_revitTask = revitTask;
InitializeComponent();
// Delay WebView2 creation until the panel is actually visible
// This avoids conflicts with other plugins (like pyRevit) during startup
IsVisibleChanged += OnIsVisibleChanged;
}
private void OnIsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is true && _browser == null && !_isInitializing)
{
_isInitializing = true;
Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, CreateWebView2);
}
}
private void CreateWebView2()
{
_browser = new WebView2
{
CreationProperties = new CoreWebView2CreationProperties { UserDataFolder = "C:\\temp" },
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
VerticalAlignment = System.Windows.VerticalAlignment.Stretch,
Source = Url.Netlify
};
_browser.CoreWebView2InitializationCompleted += (sender, args) =>
Browser.CoreWebView2InitializationCompleted += (sender, args) =>
_serviceProvider
.GetRequiredService<ITopLevelExceptionHandler>()
.CatchUnhandled(() => OnInitialized(sender, args));
BrowserContainer.Child = _browser;
}
public bool IsBrowserInitialized => _browser?.IsInitialized ?? false;
public bool IsBrowserInitialized => Browser.IsInitialized;
public object BrowserElement => _browser!;
public object BrowserElement => Browser;
public void ExecuteScript(string script)
{
if (_browser == null || !_browser.IsInitialized)
if (!Browser.IsInitialized)
{
throw new InvalidOperationException("Failed to execute script, Webview2 is not initialized yet.");
}
_revitTask.Run(() => _browser.ExecuteScriptAsync(script));
_revitTask.Run(() => Browser.ExecuteScriptAsync(script));
}
public void SendProgress(string script)
{
if (_browser == null || !_browser.IsInitialized)
if (!Browser.IsInitialized)
{
throw new InvalidOperationException("Failed to execute script, Webview2 is not initialized yet.");
}
//always invoke even on the main thread because it's better somehow
_browser.Dispatcher.Invoke(
Browser.Dispatcher.Invoke(
//fire and forget
() => _browser.ExecuteScriptAsync(script),
() => Browser.ExecuteScriptAsync(script),
DispatcherPriority.Background
);
}
@@ -106,18 +74,11 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
private void SetupBinding(IBinding binding)
{
binding.Parent.AssociateWithBinding(binding);
_browser!.CoreWebView2.AddHostObjectToScript(binding.Name, binding.Parent);
Browser.CoreWebView2.AddHostObjectToScript(binding.Name, binding.Parent);
}
public void ShowDevTools() => _browser?.CoreWebView2?.OpenDevToolsWindow();
public void ShowDevTools() => Browser.CoreWebView2.OpenDevToolsWindow();
//https://github.com/MicrosoftEdge/WebView2Feedback/issues/2161
public void Dispose()
{
if (_browser != null)
{
_browser.Dispatcher.Invoke(() => _browser.Dispose(), DispatcherPriority.Send);
_browser = null;
}
}
public void Dispose() => Browser.Dispatcher.Invoke(() => Browser.Dispose(), DispatcherPriority.Send);
}
@@ -24,7 +24,7 @@ namespace Speckle.Connectors.Revit.Bindings;
internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
{
private readonly RevitIdleManager _revitIdleManager;
private readonly IAppIdleManager _idleManager;
private readonly RevitContext _revitContext;
private readonly DocumentModelStore _store;
private readonly ICancellationManager _cancellationManager;
@@ -38,8 +38,6 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private readonly LinkedModelHandler _linkedModelHandler;
private readonly IThreadContext _threadContext;
private readonly ISendOperationManagerFactory _sendOperationManagerFactory;
private bool _isDocChangedSubscribed;
private EventHandler<Autodesk.Revit.DB.Events.DocumentChangedEventArgs>? _documentChangedHandler;
/// <summary>
/// Used internally to aggregate the changed objects' id. Note we're using a concurrent dictionary here as the expiry check method is not thread safe, and this was causing problems. See:
@@ -50,7 +48,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private ConcurrentHashSet<ElementId> ChangedObjectIds { get; set; } = new();
public RevitSendBinding(
RevitIdleManager revitIdleManager,
IAppIdleManager idleManager,
RevitContext revitContext,
DocumentModelStore store,
ICancellationManager cancellationManager,
@@ -68,7 +66,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
)
: base("sendBinding", bridge)
{
_revitIdleManager = revitIdleManager;
_idleManager = idleManager;
_revitContext = revitContext;
_store = store;
_cancellationManager = cancellationManager;
@@ -88,54 +86,12 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
revitTask.Run(() =>
{
// revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
// _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
_documentChangedHandler = (_, e) => _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
_store.ModelCardsChanged += (_, e) => OnModelCardsChanged(e);
revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
_topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
_store.DocumentChanged += (_, _) => topLevelExceptionHandler.FireAndForget(async () => await OnDocumentChanged());
});
}
private void OnModelCardsChanged(ModelCardsChangedEventArgs e)
{
if (e.ModelCards.Count > 0 && e.ModelCards.Any(m => m.TypeDiscriminator == nameof(SenderModelCard)))
{
SubscribeDocChanged();
}
else
{
UnsubscribeDocChanged();
}
}
private void SubscribeDocChanged()
{
if (_documentChangedHandler == null || _isDocChangedSubscribed)
{
return;
}
_threadContext.RunOnMain(() =>
{
_revitContext.UIApplication.NotNull().Application.DocumentChanged += _documentChangedHandler;
});
_isDocChangedSubscribed = true;
}
private void UnsubscribeDocChanged()
{
if (_documentChangedHandler == null || !_isDocChangedSubscribed)
{
return;
}
_threadContext.RunOnMain(() =>
{
_revitContext.UIApplication.NotNull().Application.DocumentChanged -= _documentChangedHandler;
});
_isDocChangedSubscribed = false;
}
public List<ISendFilter> GetSendFilters() =>
[
new RevitSelectionFilter { IsDefault = true },
@@ -320,7 +276,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
if (addedElementIds.Count > 0)
{
_revitIdleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
_idleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
}
if (HaveUnitsChanged(doc))
@@ -340,8 +296,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
_sendConversionCache.EvictObjects(unpackedObjectIds);
}
_revitIdleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
_revitIdleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
_idleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
_idleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
}
// Keeps track of doc and current units
@@ -17,7 +17,7 @@ internal sealed class SelectionBinding : RevitBaseBinding, ISelectionBinding, ID
public SelectionBinding(
RevitContext revitContext,
IBrowserBridge parent,
RevitIdleManager idleManager,
IAppIdleManager idleManager,
ITopLevelExceptionHandler topLevelExceptionHandler,
IRevitTask revitTask
)
@@ -48,12 +48,11 @@ public static class ServiceRegistration
serviceCollection.AddSingleton<IBinding, SelectionBinding>();
serviceCollection.AddSingleton<IBinding, RevitSendBinding>();
serviceCollection.AddSingleton<IBinding, RevitReceiveBinding>();
serviceCollection.AddSingleton<RevitIdleManager>();
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IBasicConnectorBinding>());
serviceCollection.AddSingleton<IBasicConnectorBinding, BasicConnectorBindingRevit>();
// serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
// send operation and dependencies
serviceCollection.AddScoped<SendOperation<DocumentToConvert>>();
@@ -17,16 +17,13 @@ namespace Speckle.Connectors.Revit.HostApp;
internal sealed class RevitDocumentStore : DocumentModelStore
{
private readonly ILogger<RevitDocumentStore> _logger;
//private readonly IAppIdleManager _idleManager;
private readonly RevitIdleManager _idleManager;
private readonly IAppIdleManager _idleManager;
private readonly RevitContext _revitContext;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
public RevitDocumentStore(
//IAppIdleManager idleManager,
RevitIdleManager idleManager,
IAppIdleManager idleManager,
RevitContext revitContext,
IJsonSerializer jsonSerializer,
ITopLevelExceptionHandler topLevelExceptionHandler,
@@ -37,7 +34,6 @@ internal sealed class RevitDocumentStore : DocumentModelStore
: base(logger, jsonSerializer)
{
_jsonCacheManager = jsonCacheManagerFactory.CreateForUser("ConnectorsFileData");
//_idleManager = idleManager;
_idleManager = idleManager;
_revitContext = revitContext;
_topLevelExceptionHandler = topLevelExceptionHandler;
@@ -50,8 +50,7 @@ public static class SupportedCategoriesUtils
return
#if REVIT2023_OR_GREATER
category.BuiltInCategory != BuiltInCategory.OST_AreaSchemes
&& category.BuiltInCategory != BuiltInCategory.OST_AreaSchemeLines
&& category.BuiltInCategory != BuiltInCategory.INVALID;
&& category.BuiltInCategory != BuiltInCategory.OST_AreaSchemeLines;
#else
category.Name != "OST_AreaSchemeLines" && category.Name != "OST_AreaSchemes";
#endif
@@ -21,7 +21,6 @@ using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
using Transform = Speckle.Objects.Other.Transform;
namespace Speckle.Connectors.Revit.Operations.Receive;
@@ -39,15 +38,12 @@ public sealed class RevitHostObjectBuilder(
IThreadContext threadContext,
RevitToHostCacheSingleton revitToHostCacheSingleton,
ITypedConverter<
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject),
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix),
DirectShape
> localToGlobalDirectShapeConverter,
IReceiveConversionHandler conversionHandler
) : IHostObjectBuilder, IDisposable
{
// Maps atomic object applicationId -> parent DataObject
private readonly Dictionary<string, DataObject> _atomicObjectToParentDataObject = new();
public Task<HostObjectBuilderResult> Build(
Base rootObject,
string projectName,
@@ -106,9 +102,6 @@ public sealed class RevitHostObjectBuilder(
unpackedRoot.ObjectsToConvert.ToList()
);
// Register DataObjects with InstanceProxy displayValues
RegisterDataObjectsWithInstanceProxies(unpackedRoot);
// NOTE: below is 💩... https://github.com/specklesystems/speckle-sharp-connectors/pull/813 broke sketchup to revit workflow
// ids were modified to fix receiving instances [CNX-1707](https://linear.app/speckle/issue/CNX-1707/revit-curves-and-meshes-in-blocks-come-as-duplicated)
// but we then broke sketchup to revit because applicationIds in proxies didn't match modified application ids which cam from #813 hack
@@ -183,9 +176,6 @@ public sealed class RevitHostObjectBuilder(
}
}
// Update DataObject lookup IDs
UpdateAtomicObjectLookupWithModifiedIds(originalToModifiedIds);
// 2 - Bake materials (now with the updated IDs)
if (unpackedRoot.RenderMaterialProxies != null)
{
@@ -244,87 +234,6 @@ public sealed class RevitHostObjectBuilder(
return conversionResults.builderResult;
}
/// <summary>
/// Registers DataObjects that have InstanceProxy displayValues and builds the lookup.
/// </summary>
private void RegisterDataObjectsWithInstanceProxies(RootObjectUnpackerResult unpackedRoot)
{
var definitionToDataObject = new Dictionary<string, DataObject>();
foreach (var tc in unpackedRoot.ObjectsToConvert)
{
if (tc.Current is DataObject dataObject)
{
var instanceProxies = dataObject.displayValue.OfType<InstanceProxy>().ToList();
if (instanceProxies.Count > 0)
{
foreach (var ip in instanceProxies)
{
definitionToDataObject[ip.definitionId] = dataObject;
}
}
}
}
// Build lookup: definition object applicationId -> parent DataObject
_atomicObjectToParentDataObject.Clear();
if (unpackedRoot.DefinitionProxies is not null)
{
foreach (var defProxy in unpackedRoot.DefinitionProxies)
{
if (
defProxy.applicationId is not null
&& definitionToDataObject.TryGetValue(defProxy.applicationId, out var parentDataObject)
)
{
foreach (var objectId in defProxy.objects)
{
_atomicObjectToParentDataObject[objectId] = parentDataObject;
}
}
else
{
logger.LogError(
"Could not find parent DataObject for DefinitionProxy {ApplicationId}",
defProxy.applicationId
);
}
}
}
}
/// <summary>
/// Updates the atomic object lookup with modified IDs
/// </summary>
private void UpdateAtomicObjectLookupWithModifiedIds(Dictionary<string, List<string>> originalToModifiedIds)
{
// Build updated entries first to avoid modifying collection during iteration
var entriesToAdd = new List<KeyValuePair<string, DataObject>>();
var keysToRemove = new List<string>();
foreach (var kvp in _atomicObjectToParentDataObject)
{
if (originalToModifiedIds.TryGetValue(kvp.Key, out var modifiedIds))
{
keysToRemove.Add(kvp.Key);
foreach (var modifiedId in modifiedIds)
{
entriesToAdd.Add(new(modifiedId, kvp.Value));
}
}
}
foreach (var key in keysToRemove)
{
_atomicObjectToParentDataObject.Remove(key);
}
foreach (var entry in entriesToAdd)
{
_atomicObjectToParentDataObject[entry.Key] = entry.Value;
}
}
private Autodesk.Revit.DB.Transform? CalculateNewTransform(
Autodesk.Revit.DB.Transform? receiveTransform,
Autodesk.Revit.DB.Transform? rootTransform
@@ -369,17 +278,9 @@ public sealed class RevitHostObjectBuilder(
onOperationProgressed.Report(new("Converting", (double)++count / localToGlobalMaps.Count));
if (result is DirectShapeDefinitionWrapper)
{
// Look up parent DataObject for this atomic object (handles InstanceProxy displayValue)
var atomicId = localToGlobalMap.AtomicObject.applicationId;
DataObject? parentDataObject = null;
if (atomicId is not null)
{
_atomicObjectToParentDataObject.TryGetValue(atomicId, out parentDataObject);
}
// direct shape creation happens here
DirectShape directShapes = localToGlobalDirectShapeConverter.Convert(
(localToGlobalMap.AtomicObject, localToGlobalMap.Matrix, parentDataObject)
(localToGlobalMap.AtomicObject, localToGlobalMap.Matrix)
);
bakedObjectIds.Add(directShapes.UniqueId);
@@ -450,7 +351,6 @@ public sealed class RevitHostObjectBuilder(
DirectShapeLibrary.GetDirectShapeLibrary(converterSettings.Current.Document).Reset(); // Note: this needs to be cleared, as it is being used in the converter
revitToHostCacheSingleton.MaterialsByObjectId.Clear(); // Massive hack!
_atomicObjectToParentDataObject.Clear();
groupManager.PurgeGroups(baseGroupName);
materialBaker.PurgeMaterials(baseGroupName);
}
@@ -42,7 +42,11 @@ public class RevitRootObjectBuilder(
() => Task.FromResult(BuildSync(documentElementContexts, projectId, onOperationProgressed, ct))
);
#pragma warning disable CA1506
#pragma warning disable CA1502
private RootObjectBuilderResult BuildSync(
#pragma warning restore CA1506
#pragma warning restore CA1502
IReadOnlyList<DocumentToConvert> documentElementContexts,
string projectId,
IProgress<CardProgress> onOperationProgressed,
@@ -56,6 +60,9 @@ public class RevitRootObjectBuilder(
throw new SpeckleException("Family Environment documents are not supported.");
}
// create a new send pipeline
using var sendPipeline = new Speckle.Sdk.Pipeline.Send();
// init the root
Collection rootObject =
new() { name = converterSettings.Current.Document.PathName.Split('\\').Last().Split('.').First() };
@@ -184,10 +191,12 @@ public class RevitRootObjectBuilder(
// non-transformed elements can safely rely on cache
// TODO: Potential here to transform cached objects and NOT reconvert,
// TODO: we wont do !hasTransform here, and re-set application id before this
bool wasCached = false;
if (!hasTransform && sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
{
// TODO: cahce hit
converted = value;
wasCached = true;
cacheHitCount++;
}
// not in cache means we convert
@@ -206,6 +215,12 @@ public class RevitRootObjectBuilder(
converted.applicationId = applicationId;
}
var reference = sendPipeline.Process(converted).Result; // .Wait(cancellationToken);//.ConfigureAwait(false);
if (!wasCached)
{
sendConversionCache.AppendSendResult(projectId, applicationId, reference);
}
var collection = sendCollectionManager.GetAndCreateObjectHostCollection(
revitElement,
rootObject,
@@ -213,7 +228,7 @@ public class RevitRootObjectBuilder(
modelDisplayName
);
collection.elements.Add(converted);
collection.elements.Add(reference);
results.Add(new(Status.SUCCESS, applicationId, sourceType, converted));
}
catch (Exception ex) when (!ex.IsFatal())
@@ -254,13 +269,20 @@ public class RevitRootObjectBuilder(
rootObject[ProxyKeys.INSTANCE_DEFINITION] = revitToSpeckleCacheSingleton.GetInstanceDefinitionProxiesForObjects(
idsAndSubElementIds
);
rootObject.elements.Add(
new Collection()
{
elements = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds),
name = "revitInstancedObjects"
}
);
// NOTE: i might be overdoing things in here, but tldr:
// - all instance objects (meshes) are processed individually
// - process their collection individually, and then attach it to the root collection
// we could, theoretically, just process the collection as a whole (but it can be big?)
// note/ask: do these need to go in the conversion cache? or not?
var instanceObjects = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds);
var instanceReferences = new Collection("revitInstancedObjects");
foreach (var instanceObject in instanceObjects)
{
var referenceInstanceObject = sendPipeline.Process(instanceObject).Result;
instanceReferences.elements.Add(referenceInstanceObject);
}
var instanceReferenceCollection = sendPipeline.Process(instanceReferences).Result;
rootObject.elements.Add(instanceReferenceCollection);
// STEP 6: Unpack all other objects to attach to root collection
List<Objects.Other.Camera> views = viewUnpacker.Unpack(converterSettings.Current.Document);
@@ -279,6 +301,10 @@ public class RevitRootObjectBuilder(
rootObject[RootKeys.REFERENCE_POINT_TRANSFORM] = transformMatrix;
}
return new RootObjectBuilderResult(rootObject, results);
// NOTE: could be
sendPipeline.Process(rootObject).Wait(cancellationToken);
sendPipeline.WaitForUpload().Wait(cancellationToken);
return new RootObjectBuilderResult(new Collection() { name = "ignore" }, results);
}
}
@@ -1,75 +1,43 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using Autodesk.Revit.UI;
using Autodesk.Revit.UI.Events;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Sdk.Common;
namespace Speckle.Connectors.Revit.Plugin;
/// <summary>
/// OK.
/// Please do not try to generalize this class with other IdleManagers for whatever reason.
/// This class is simple, targeted to host app and singleton.
/// </summary>
public class RevitIdleManager(RevitContext revitContext)
public sealed class RevitIdleManager : AppIdleManager
{
private readonly UIApplication _uiApplication = revitContext.UIApplication.NotNull();
private readonly UIApplication _uiApplication;
private readonly IIdleCallManager _idleCallManager;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly ConcurrentDictionary<string, Func<Task>> _calls = new();
private volatile bool _hasSubscribed;
private event EventHandler<IdlingEventArgs>? OnIdle;
/// <summary>
/// Subscribe deferred action to Idling event to run it whenever Revit becomes idle.
/// </summary>
/// <param name="action"> Action to call whenever Revit becomes Idle.</param>
/// some events in host app are trigerred many times, we might get 10x per object
/// Making this more like a deferred action, so we don't update the UI many times
public void SubscribeToIdle(string name, Action action)
public RevitIdleManager(
RevitContext revitContext,
IIdleCallManager idleCallManager,
ITopLevelExceptionHandler topLevelExceptionHandler,
IRevitTask revitTask
)
: base(idleCallManager)
{
// I want to be called back ONCE when the host app has become idle once more
_calls[name] = () =>
{
action();
return Task.CompletedTask;
};
if (_hasSubscribed)
{
return;
}
_hasSubscribed = true;
_uiApplication.Idling += RevitAppOnIdle;
_topLevelExceptionHandler = topLevelExceptionHandler;
_uiApplication = revitContext.UIApplication.NotNull();
_idleCallManager = idleCallManager;
revitTask.Run(
() => _uiApplication.Idling += (s, e) => OnIdle?.Invoke(s, e) // will be called on the main thread always and fixing the Revit exceptions on subscribing/unsubscribing Idle events
);
}
/// <summary>
/// Run once on the next Revit idle tick (deduped by name).
/// </summary>
public void SubscribeToIdle(string name, Func<Task> action)
protected override void AddEvent()
{
_calls[name] = action;
if (_hasSubscribed)
_topLevelExceptionHandler.CatchUnhandled(() =>
{
return;
}
_hasSubscribed = true;
_uiApplication.Idling += RevitAppOnIdle;
OnIdle += RevitAppOnIdle;
});
}
private void RevitAppOnIdle(object? sender, IdlingEventArgs e)
{
foreach (KeyValuePair<string, Func<Task>> kvp in _calls)
{
Debug.WriteLine($"{kvp.Key}");
kvp.Value();
}
_calls.Clear();
_uiApplication.Idling -= RevitAppOnIdle;
// setting last will delay entering re-subscritption
_hasSubscribed = false;
}
private void RevitAppOnIdle(object? sender, IdlingEventArgs e) =>
_idleCallManager.AppOnIdle(() => OnIdle -= RevitAppOnIdle);
}
@@ -54,9 +54,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\DetailLevelSetting.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\IRevitPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitTask.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitExternalApplication.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitThreadContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
@@ -1,311 +0,0 @@
using System.Runtime.InteropServices;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;
using Speckle.Connectors.GrasshopperShared.HostApp;
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Connectors.GrasshopperShared.Properties;
using Speckle.Sdk.Models.Collections;
namespace Speckle.Connectors.GrasshopperShared.Components.Collections;
/// <summary>
/// Creates collections by matching name tree structure to elements tree structure.
/// Each branch in the names tree corresponds to the same-path branch in the elements tree.
/// </summary>
[Guid("7E8F9A1B-2C3D-4E5F-6A7B-8C9D0E1F2A3B")]
public class CollectionsByName : GH_Component
{
public override Guid ComponentGuid => GetType().GUID;
protected override Bitmap Icon => Resources.speckle_collections_create; // TODO: Update to specific icon if available
public override GH_Exposure Exposure => GH_Exposure.primary;
public CollectionsByName()
: base(
"Collections by Name",
"CbN",
"Creates collections by matching name tree structure to objects tree structure. Each branch in the names tree corresponds to the same-path branch in the objects tree.",
ComponentCategories.PRIMARY_RIBBON,
ComponentCategories.COLLECTIONS
) { }
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddTextParameter(
"Names",
"N",
"Collection names (tree structure must match Objects tree structure)",
GH_ParamAccess.tree
);
pManager.AddGenericParameter(
"Objects",
"O",
"Objects to group into collections (tree structure must match Names tree structure)",
GH_ParamAccess.tree
);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
pManager.AddParameter(
new SpeckleCollectionParam(),
"Collection",
"C",
"Root collection containing named sub-collections",
GH_ParamAccess.item
);
protected override void SolveInstance(IGH_DataAccess da)
{
// access the tree data directly from parameters
var namesParam = Params.Input[0];
var elementsParam = Params.Input[1];
var namesTree = namesParam.VolatileData;
var elementsTree = elementsParam.VolatileData;
// validate that both inputs have data
if (namesTree.IsEmpty)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Names tree is empty");
return;
}
if (elementsTree.IsEmpty)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Objects tree is empty");
return;
}
// validate tree structures match exactly
if (!TreeStructuresMatch(namesTree, elementsTree))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Tree structures and topologies must match exactly");
return;
}
// create root collection
var rootCollection = CollectionHelpers.CreateRootCollection(InstanceGuid.ToString());
// process each path
foreach (var path in namesTree.Paths)
{
var nameBranch = namesTree.get_Branch(path);
var elementsBranch = elementsTree.get_Branch(path);
// validate name branch - throw if empty
if (nameBranch.Count == 0)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Name branch at path {path} is empty");
return;
}
// validate name branch - just warn if multiple, use first
if (nameBranch.Count > 1)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Name branch at path {path} has {nameBranch.Count} names - using first name only"
);
}
// get the collection name
string collectionName = GetCollectionName(nameBranch[0]);
if (string.IsNullOrWhiteSpace(collectionName))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Invalid collection name at path {path}");
return;
}
// skip empty element branches with warning
if (elementsBranch.Count == 0)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Skipping empty elements branch at path {path}");
continue;
}
// parse nested collection path (e.g. parent::child)
var collectionNames = collectionName.Split(
new[] { Constants.LAYER_PATH_DELIMITER },
StringSplitOptions.RemoveEmptyEntries
);
// create or get nested collection structure
var targetCollection = GetOrCreateNestedCollection(rootCollection, collectionNames, elementsBranch, path);
// add elements to deepest collection
AddElementsToCollection(targetCollection, elementsBranch, path);
}
// validate collection has content
if (rootCollection.Elements.Count == 0)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Error,
"Collection contains no valid geometry. All branches were empty or contained unsupported types."
);
return;
}
// validate for duplicate application IDs (following CreateCollection pattern)
if (CollectionHelpers.HasDuplicateApplicationIds(rootCollection))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
return;
}
da.SetData(0, new SpeckleCollectionWrapperGoo(rootCollection));
}
/// <summary>
/// Validates that two tree structures have exactly matching paths
/// </summary>
private bool TreeStructuresMatch(
Grasshopper.Kernel.Data.IGH_Structure namesTree,
Grasshopper.Kernel.Data.IGH_Structure elementsTree
)
{
if (namesTree.PathCount != elementsTree.PathCount)
{
return false;
}
// check that all paths match exactly
var namePaths = namesTree.Paths.ToList();
var elementPaths = elementsTree.Paths.ToList();
for (int i = 0; i < namePaths.Count; i++)
{
if (namePaths[i].CompareTo(elementPaths[i]) != 0)
{
return false;
}
}
return true;
}
/// <summary>
/// Extracts collection name, handling GH_String and other text types
/// </summary>
private string GetCollectionName(object nameObj) =>
nameObj switch
{
GH_String ghString => ghString.Value,
IGH_Goo goo => goo.ToString(),
_ => nameObj.ToString()
};
/// <summary>
/// Gets or creates a nested collection structure based on the collection names.
/// </summary>
/// <remarks>
/// Handles paths like "parent::child::grandchild" by creating intermediate collections.
/// </remarks>
private SpeckleCollectionWrapper GetOrCreateNestedCollection(
SpeckleCollectionWrapper rootCollection,
string[] collectionNames,
System.Collections.IList elementsBranch,
Grasshopper.Kernel.Data.GH_Path path
)
{
SpeckleCollectionWrapper currentCollection = rootCollection;
var currentPath = new List<string>(rootCollection.Path);
foreach (var collectionName in collectionNames)
{
// build path for this level
currentPath.Add(collectionName);
// check if child collection already exists
var existingChild = currentCollection
.Elements.OfType<SpeckleCollectionWrapper>()
.FirstOrDefault(c => c.Name == collectionName);
if (existingChild != null)
{
// use existing collection
currentCollection = existingChild;
}
else
{
// create new child collection
var newChild = new SpeckleCollectionWrapper
{
Base = new Collection(),
Name = collectionName,
Path = currentPath.ToList(),
Color = null,
Material = null,
Topology = null, // only set topology on leaf collections
ApplicationId = Guid.NewGuid().ToString()
};
currentCollection.Elements.Add(newChild);
currentCollection = newChild;
}
}
// set topology on the final (leaf) collection
currentCollection.Topology = GetBranchTopology(path, elementsBranch.Count);
return currentCollection;
}
/// <summary>
/// Adds elements from a branch to the target collection
/// </summary>
private void AddElementsToCollection(
SpeckleCollectionWrapper targetCollection,
System.Collections.IList elementsBranch,
Grasshopper.Kernel.Data.GH_Path path
)
{
foreach (var item in elementsBranch)
{
if (item == null)
{
// preserve nulls for topology (CNX-2855 pattern)
targetCollection.Elements.Add(null);
continue;
}
// convert to SpeckleWrapper if possible - cast to IGH_Goo first
if (item is not IGH_Goo goo)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Unsupported object type in branch {path}: {item.GetType().Name}"
);
continue;
}
var wrapper = goo.ToSpeckleObjectWrapper();
if (wrapper == null)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Unsupported object type in branch {path}: {item.GetType().Name}"
);
continue;
}
if (wrapper is ISpeckleCollectionObject collectionObject)
{
targetCollection.Elements.Add(collectionObject);
}
else
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Object type {wrapper.GetType().Name} is not a valid collection element"
);
}
}
}
/// <summary>
/// Creates topology string for a single branch (following GrasshopperHelpers.GetParamTopology pattern)
/// </summary>
private string GetBranchTopology(Grasshopper.Kernel.Data.GH_Path path, int count) =>
$"{path.ToString(false)}-{count}";
}
@@ -47,7 +47,7 @@ public class CreateCollection : VariableParameterComponentBase
protected override void SolveInstance(IGH_DataAccess dataAccess)
{
var rootCollection = CollectionHelpers.CreateRootCollection(InstanceGuid.ToString());
var rootCollection = CreateRootCollection();
bool hasAnyInput = false;
foreach (var inputParam in Params.Input)
@@ -73,14 +73,14 @@ public class CreateCollection : VariableParameterComponentBase
}
// validate for duplicate application IDs across the entire collection hierarchy
if (CollectionHelpers.HasDuplicateApplicationIds(rootCollection))
if (HasDuplicateApplicationIds(rootCollection))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
return;
}
// validate collection isn't empty (CNX-2855)
if (rootCollection.Elements.Count == 0 || !rootCollection.Elements.Any(CollectionHelpers.HasAnyValidContent))
if (rootCollection.Elements.Count == 0 || !rootCollection.Elements.Any(HasAnyValidContent))
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Error,
@@ -233,6 +233,56 @@ public class CreateCollection : VariableParameterComponentBase
}
}
/// <summary>
/// Validates that all application IDs are unique across the entire collection hierarchy.
/// Shows an error if duplicates are found, indicating objects appear in multiple collections.
/// </summary>
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
private bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
{
// args to CheckForDuplicateApplicationIds passed in since the method can recursively check
var seenIds = new HashSet<string>();
var duplicateIds = new HashSet<string>();
// iterate, create hash set and check all application IDs
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
return duplicateIds.Count > 0;
}
/// <summary>
/// Recursively collects application IDs from all in the collection hierarchy.
/// </summary>
/// <remarks>
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
/// </remarks>
private void ProcessAndCheckForDuplicateApplicationIds(
SpeckleCollectionWrapper collection,
HashSet<string> seenIds,
HashSet<string> duplicateIds
)
{
foreach (var element in collection.Elements)
{
switch (element)
{
case null:
break; // skip nulls (CNX-2855)
case SpeckleCollectionWrapper childCollection:
// recurse into child collections
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
break;
case SpeckleWrapper wrapper:
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
{
duplicateIds.Add(wrapper.ApplicationId);
}
break;
}
}
}
// IGH_VariableParameterComponent implementation
public override bool CanInsertParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input;
@@ -1,6 +1,5 @@
using System.Runtime.InteropServices;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Parameters;
using Grasshopper.Kernel.Types;
using Speckle.Connectors.GrasshopperShared.Components.BaseComponents;
using Speckle.Connectors.GrasshopperShared.HostApp;
@@ -13,7 +12,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
/// Given a list of objects, this component will filter the list for objects that match the queries.
/// </summary>
[Guid("26AEA046-4DD4-4F61-8251-E92A6D2AC880")]
public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
public class FilterSpeckleObjects : GH_Component
{
public override Guid ComponentGuid => GetType().GUID;
protected override Bitmap Icon => Resources.speckle_objects_filter;
@@ -50,6 +49,17 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
GH_ParamAccess.item
);
Params.Input[3].Optional = true;
pManager.AddTextParameter(
"Application Id",
"aID",
"Find objects with a matching applicationId",
GH_ParamAccess.item
);
Params.Input[4].Optional = true;
pManager.AddTextParameter("Speckle Id", "sID", "Find objects with a matching Speckle id", GH_ParamAccess.item);
Params.Input[5].Optional = true;
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
@@ -93,39 +103,101 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
dataAccess.GetData(2, ref property);
string material = "";
dataAccess.GetData(3, ref material);
// optional parameters - only read if they've been added via ⊕
string appId = "";
dataAccess.GetData(4, ref appId);
string speckleId = "";
int? appIdIndex = FindInputIndexByName("Application Id");
int? speckleIdIndex = FindInputIndexByName("Speckle Id");
if (appIdIndex.HasValue)
{
dataAccess.GetData(appIdIndex.Value, ref appId);
}
if (speckleIdIndex.HasValue)
{
dataAccess.GetData(speckleIdIndex.Value, ref speckleId);
}
bool filterByAppId = appIdIndex.HasValue;
bool filterBySpeckleId = speckleIdIndex.HasValue;
dataAccess.GetData(5, ref speckleId);
List<SpeckleWrapper> matchedObjects = new();
List<SpeckleWrapper> removedObjects = new();
foreach (SpeckleWrapper wrapper in objects.Cast<SpeckleWrapper>())
for (int i = 0; i < objects.Count; i++)
{
if (MatchesAllFilters(wrapper, name, property, material, appId, filterByAppId, speckleId, filterBySpeckleId))
SpeckleWrapper wrapper = objects[i]!;
// filter by name
if (!MatchesSearchPattern(name, wrapper.Name))
{
matchedObjects.Add(wrapper);
removedObjects.Add(wrapper);
continue;
}
// filter by property
bool foundProperty = false;
if (string.IsNullOrEmpty(property))
{
foundProperty = true;
}
else
{
removedObjects.Add(wrapper);
SpecklePropertyGroupGoo? properties = wrapper is SpeckleDataObjectWrapper dataObjPropWrapper
? dataObjPropWrapper.Properties
: wrapper is SpeckleGeometryWrapper geoPropWrapper
? geoPropWrapper.Properties
: null;
if (properties is not null)
{
// use flattened properties to search ALL nested property keys
// fix for [CNX-2512](https://linear.app/speckle/issue/CNX-2512/filter-objects-material-and-property-key-inputs-dont-work-as-expected)
Dictionary<string, SpecklePropertyGoo> flattenedProps = properties.Flatten();
foreach (string key in flattenedProps.Keys)
{
if (MatchesSearchPattern(property, key))
{
foundProperty = true;
break;
}
}
}
}
if (!foundProperty)
{
removedObjects.Add(wrapper);
continue;
}
// filter by material name
bool materialMatches = true;
if (!string.IsNullOrEmpty(material))
{
materialMatches = false;
if (wrapper is SpeckleGeometryWrapper geoWrapper)
{
materialMatches = MatchesSearchPattern(material, geoWrapper.Material?.Name ?? "");
}
else if (wrapper is SpeckleDataObjectWrapper dataObjWrapper)
{
// check if ANY geometry in the data object has a matching material (not sure about this...)
// fix for [CNX-2512](https://linear.app/speckle/issue/CNX-2512/filter-objects-material-and-property-key-inputs-dont-work-as-expected)
materialMatches = dataObjWrapper.Geometries.Any(geo =>
MatchesSearchPattern(material, geo.Material?.Name ?? "")
);
}
}
if (!materialMatches)
{
removedObjects.Add(wrapper);
continue;
}
// filter by application id
if (!MatchesSearchPattern(appId, wrapper.Base.applicationId ?? ""))
{
removedObjects.Add(wrapper);
continue;
}
// filter by speckle id
if (!MatchesSearchPattern(speckleId, wrapper.Base.id ?? ""))
{
removedObjects.Add(wrapper);
continue;
}
matchedObjects.Add(wrapper);
}
// Set output objects
@@ -142,190 +214,4 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
return Operator.IsSymbolNameLike(target, searchPattern);
}
/// <summary>
/// Determines if a wrapper matches all active filter criteria.
/// </summary>
private bool MatchesAllFilters(
SpeckleWrapper wrapper,
string name,
string property,
string material,
string appId,
bool filterByAppId,
string speckleId,
bool filterBySpeckleId
)
{
// filter by name
if (!MatchesSearchPattern(name, wrapper.Name))
{
return false;
}
// filter by property
if (!MatchesPropertyFilter(wrapper, property))
{
return false;
}
// filter by material name
if (!MatchesMaterialFilter(wrapper, material))
{
return false;
}
// filter by application id (only if parameter was added)
if (filterByAppId && !MatchesSearchPattern(appId, wrapper.Base.applicationId ?? ""))
{
return false;
}
// filter by speckle id (only if parameter was added)
if (filterBySpeckleId && !MatchesSearchPattern(speckleId, wrapper.Base.id ?? ""))
{
return false;
}
return true;
}
private bool MatchesPropertyFilter(SpeckleWrapper wrapper, string property)
{
if (string.IsNullOrEmpty(property))
{
return true;
}
SpecklePropertyGroupGoo? properties = wrapper is SpeckleDataObjectWrapper dataObjPropWrapper
? dataObjPropWrapper.Properties
: wrapper is SpeckleGeometryWrapper geoPropWrapper
? geoPropWrapper.Properties
: null;
if (properties is null)
{
return false;
}
// use flattened properties to search ALL nested property keys
return properties.Flatten().Keys.Any(key => MatchesSearchPattern(property, key));
}
private bool MatchesMaterialFilter(SpeckleWrapper wrapper, string material)
{
if (string.IsNullOrEmpty(material))
{
return true;
}
if (wrapper is SpeckleGeometryWrapper geoWrapper)
{
return MatchesSearchPattern(material, geoWrapper.Material?.Name ?? "");
}
if (wrapper is SpeckleDataObjectWrapper dataObjWrapper)
{
// check if ANY geometry in the data object has a matching material
return dataObjWrapper.Geometries.Any(geo => MatchesSearchPattern(material, geo.Material?.Name ?? ""));
}
return false;
}
/// <summary>
/// Finds the index of an input parameter by its Name.
/// Returns null if the parameter doesn't exist.
/// </summary>
private int? FindInputIndexByName(string paramName)
{
for (int i = 0; i < Params.Input.Count; i++)
{
if (Params.Input[i].Name == paramName)
{
return i;
}
}
return null;
}
#region IGH_VariableParameterComponent
public bool CanInsertParameter(GH_ParameterSide side, int index)
{
if (side != GH_ParameterSide.Input)
{
return false;
}
// only allow inserting after the fixed parameters (index 4+)
if (index < 4)
{
return false;
}
// check how many optional params are already added (total inputs - 4 fixed)
int addedOptionalCount = Params.Input.Count - 4;
// we have 2 optional parameters available
return addedOptionalCount < 2;
}
public bool CanRemoveParameter(GH_ParameterSide side, int index) =>
// only allow removing optional input parameters (index 4+)
side == GH_ParameterSide.Input
&& index >= 4;
/// <remarks>
/// The ternary operator for NickName is needed due to a Grasshopper quirk where
/// dynamically created parameters don't respect the "Draw Full Names" setting automatically.
/// We check CanvasFullNames at creation time to set the appropriate NickName.
/// This does not handle the case where the user toggles "Draw Full Names" while the
/// component is already on the canvas. Handling that would require subscribing to
/// Grasshopper.CentralSettings.CanvasFullNamesChanged event, which is overkill for now.
/// </remarks>
public IGH_Param CreateParameter(GH_ParameterSide side, int index)
{
bool hasAppId = FindInputIndexByName("Application Id").HasValue;
bool hasSpeckleId = FindInputIndexByName("Speckle Id").HasValue;
if (!hasAppId)
{
return new Param_String
{
Name = "Application Id",
NickName = Grasshopper.CentralSettings.CanvasFullNames ? "Application Id" : "aID", // see remarks
Description = "Find objects with a matching applicationId",
Access = GH_ParamAccess.item,
Optional = true
};
}
if (!hasSpeckleId)
{
return new Param_String
{
Name = "Speckle Id",
NickName = Grasshopper.CentralSettings.CanvasFullNames ? "Speckle Id" : "sID", // see remarks
Description = "Find objects with a matching Speckle id",
Access = GH_ParamAccess.item,
Optional = true
};
}
return new Param_String();
}
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input && index >= 4;
public void VariableParameterMaintenance()
{
// ensure all optional parameters stay marked as optional
for (int i = 4; i < Params.Input.Count; i++)
{
Params.Input[i].Optional = true;
}
}
#endregion
}
@@ -9,7 +9,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("8D2E3F4A-1B5C-4E7F-9A8B-3C6D9E2F1A4B")]
public class SpeckleBlockDefinitionPassthrough()
: SpecklePassthroughComponentBase(
: SpeckleSolveInstance(
"Speckle Block Definition",
"SBD",
"Create or modify a Speckle Block Definition",
@@ -21,9 +21,6 @@ public class SpeckleBlockDefinitionPassthrough()
protected override Bitmap Icon => Resources.speckle_objects_block_def;
public override GH_Exposure Exposure => GH_Exposure.tertiary;
protected override int FixedInputCount => 3;
protected override int FixedOutputCount => 3;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddParameter(
@@ -125,16 +122,12 @@ public class SpeckleBlockDefinitionPassthrough()
result.Value.Name = inputName;
}
// process application id (only if user provided one, otherwise preserve existing)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.Value.ApplicationId = inputAppId;
}
// no need to process application Id.
// New definitions should have a new appID generated in the new() constructor, and we want to preserve old appID otherwise for changetracking.
// set outputs
da.SetData(0, result);
da.SetDataList(1, result.Value.Objects.Select(o => o.CreateGoo()));
da.SetData(2, result.Value.Name);
SetApplicationIdOutput(da, result.Value.ApplicationId);
}
}
@@ -9,7 +9,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("2F8A9B1C-3D4E-5F6A-7B8C-9D0E1F2A3B4C")]
public class SpeckleBlockInstancePassthrough()
: SpecklePassthroughComponentBase(
: SpeckleSolveInstance(
"Speckle Block Instance",
"SBI",
"Create or modify a Speckle Block Instance",
@@ -21,9 +21,6 @@ public class SpeckleBlockInstancePassthrough()
protected override Bitmap Icon => Resources.speckle_objects_block_inst;
public override GH_Exposure Exposure => GH_Exposure.tertiary;
protected override int FixedInputCount => 7;
protected override int FixedOutputCount => 7;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
int instanceIndex = pManager.AddParameter(
@@ -208,11 +205,8 @@ public class SpeckleBlockInstancePassthrough()
result.Value.Material = inputMaterial.Value;
}
// process application id (only if user provided one, otherwise preserve existing)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.Value.ApplicationId = inputAppId;
}
// no need to process application id.
// new appids are generated if this is a new object, otherwise the input object appID should be preserved for change tracking.
// Set outputs
da.SetData(0, result);
@@ -222,7 +216,6 @@ public class SpeckleBlockInstancePassthrough()
da.SetData(4, result.Value.Properties);
da.SetData(5, result.Value.Color);
da.SetData(6, result.Value.Material);
SetApplicationIdOutput(da, result.Value.ApplicationId);
}
private Transform? ExtractTransform(IGH_Goo input) =>
@@ -8,7 +8,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("5CE8AA40-7706-4893-853D-4C77604548FA")]
public class SpeckleDataObjectPassthrough()
: SpecklePassthroughComponentBase(
: SpeckleSolveInstance(
"Speckle Data Object",
"SDO",
"Create or modify a Speckle Data Object",
@@ -20,9 +20,6 @@ public class SpeckleDataObjectPassthrough()
protected override Bitmap Icon => Resources.speckle_objects_dataobject;
public override GH_Exposure Exposure => GH_Exposure.secondary;
protected override int FixedInputCount => 4;
protected override int FixedOutputCount => 5;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
int objIndex = pManager.AddParameter(
@@ -161,17 +158,9 @@ public class SpeckleDataObjectPassthrough()
result.Properties = inputProperties;
}
// process application id (only if user provided one)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.ApplicationId = inputAppId;
}
else
{
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
result.ApplicationId ??= Guid.NewGuid().ToString();
}
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
result.ApplicationId ??= Guid.NewGuid().ToString();
// get the path
string? path =
@@ -183,6 +172,5 @@ public class SpeckleDataObjectPassthrough()
da.SetData(2, result.Name);
da.SetData(3, result.Properties);
da.SetData(4, path);
SetApplicationIdOutput(da, result.ApplicationId);
}
}
@@ -10,7 +10,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("F9418610-ACAE-4417-B010-19EBEA6A121F")]
public class SpeckleGeometryPassthrough()
: SpecklePassthroughComponentBase(
: SpeckleSolveInstance(
"Speckle Geometry",
"SG",
"Create or modify a Speckle Geometry",
@@ -22,9 +22,6 @@ public class SpeckleGeometryPassthrough()
protected override Bitmap Icon => Resources.speckle_objects_geometry;
public override GH_Exposure Exposure => GH_Exposure.secondary;
protected override int FixedInputCount => 6;
protected override int FixedOutputCount => 7;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
int objIndex = pManager.AddGenericParameter(
@@ -223,11 +220,8 @@ public class SpeckleGeometryPassthrough()
result.Material = inputMaterial.Value;
}
// process application id (only if user provided one, otherwise preserve existing)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.ApplicationId = inputAppId;
}
// no need to process application Id.
// New definitions should have a new appID generated in the new() constructor, and we want to preserve old appID otherwise for changetracking.
// get the path
string? path =
@@ -241,7 +235,6 @@ public class SpeckleGeometryPassthrough()
da.SetData(4, result.Color);
da.SetData(5, result.Material);
da.SetData(6, path);
SetApplicationIdOutput(da, result.ApplicationId);
}
// keeps the geometry and wrapped base the same while assigning all other props from the inut wrapper
@@ -87,7 +87,6 @@ public class SendAsyncComponent : GH_AsyncComponent<SendAsyncComponent>
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddParameter(new SpeckleUrlModelResourceParam());
pManager.AddTextParameter("Version ID", "V", "ID of the created version", GH_ParamAccess.item);
}
public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
@@ -322,7 +321,6 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
private Stopwatch? _stopwatch;
public SpeckleUrlModelResource? OutputParam { get; set; }
public string? OutputVersionId { get; set; }
private List<(GH_RuntimeMessageLevel, string)> RuntimeMessages { get; } = new();
public override WorkerInstance<SendAsyncComponent> Duplicate(string id, CancellationToken cancellationToken)
@@ -334,7 +332,6 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
{
_stopwatch = new Stopwatch();
_stopwatch.Start();
OutputVersionId = null;
}
public override void SetData(IGH_DataAccess da)
@@ -345,7 +342,6 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
{
Parent.JustPastedIn = false;
da.SetData(0, Parent.OutputParam);
da.SetData(1, OutputVersionId);
return;
}
@@ -361,7 +357,6 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
}
da.SetData(0, OutputParam);
da.SetData(1, OutputVersionId);
Parent.CurrentComponentState = ComponentState.UpToDate;
Parent.OutputParam = OutputParam; // ref the outputs in the parent too, so we can serialise them on write/read
@@ -378,7 +373,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
*/
Parent.AddRuntimeMessage(
GH_RuntimeMessageLevel.Remark,
"Successfully published to Speckle. Right-click on the component to view online."
$"Successfully published to Speckle. Right-click on the component to view online."
);
Parent.AddRuntimeMessage(
GH_RuntimeMessageLevel.Remark,
@@ -476,7 +471,6 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
result.VersionId
);
OutputParam = createdVersion;
OutputVersionId = result.VersionId;
Parent.Url = $"{createdVersion.Account.Server}/projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}";
}
}
@@ -36,10 +36,9 @@ public class SendComponentInput
}
}
public class SendComponentOutput(SpeckleUrlModelResource? resource, string? versionId = null)
public class SendComponentOutput(SpeckleUrlModelResource? resource)
{
public SpeckleUrlModelResource? Resource { get; } = resource;
public string? VersionId { get; } = versionId;
}
public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, SendComponentOutput>
@@ -87,11 +86,8 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
pManager.AddBooleanParameter("Run", "r", "Run the publish operation", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
pManager.AddParameter(new SpeckleUrlModelResourceParam());
pManager.AddTextParameter("Version ID", "V", "ID of the created version", GH_ParamAccess.item);
}
protected override SendComponentInput GetInput(IGH_DataAccess da)
{
@@ -138,7 +134,6 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
else
{
da.SetData(0, result.Resource);
da.SetData(1, result.VersionId);
Message = "Done";
}
}
@@ -221,7 +216,7 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
using var client = clientFactory.Create(account);
var sendInfo = await input.Resource.GetSendInfo(client, cancellationToken).ConfigureAwait(false);
var result = await sendOperation
await sendOperation
.Execute(
new List<SpeckleCollectionWrapperGoo> { collectionToSend },
sendInfo,
@@ -249,6 +244,6 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
sendInfo.ModelId
);
Url = $"{sendInfo.Account.serverInfo.url}/projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}";
return new SendComponentOutput(createdVersionResource, result.VersionId);
return new SendComponentOutput(createdVersionResource);
}
}
@@ -1,231 +0,0 @@
using GH_IO.Serialization;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Parameters;
namespace Speckle.Connectors.GrasshopperShared.Components;
/// <summary>
/// Base class for passthrough components with "hidden" Application ID parameter.
/// </summary>
/// <remarks>
/// Users can click ⊕ to add an optional Application ID input and output.
/// </remarks>
public abstract class SpecklePassthroughComponentBase : SpeckleSolveInstance, IGH_VariableParameterComponent
{
private const string APP_ID_NAME = "Application Id";
private const string APP_ID_NICKNAME = "aID";
private const string APP_ID_DESCRIPTION = "The application id of the Speckle objects";
protected abstract int FixedInputCount { get; }
protected abstract int FixedOutputCount { get; }
private bool HasApplicationIdParam => Params.Input.Count > FixedInputCount;
protected SpecklePassthroughComponentBase(
string name,
string nickname,
string description,
string category,
string subCategory
)
: base(name, nickname, description, category, subCategory) { }
/// <summary>
/// Reads the optional Application Id input. Returns true if user provided a valid value.
/// </summary>
protected bool TryGetApplicationIdInput(IGH_DataAccess da, out string? applicationId)
{
applicationId = null;
if (!HasApplicationIdParam)
{
return false;
}
string appId = string.Empty;
if (da.GetData(FixedInputCount, ref appId))
{
if (string.IsNullOrWhiteSpace(appId))
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
"Empty Application Id ignored - existing or auto-generated id will be used"
);
return false;
}
applicationId = appId;
return true;
}
return false;
}
/// <summary>
/// Sets the Application Id output (if the parameter exists).
/// </summary>
protected void SetApplicationIdOutput(IGH_DataAccess da, string? applicationId)
{
if (!HasApplicationIdParam)
{
return;
}
da.SetData(FixedOutputCount, applicationId);
}
public bool CanInsertParameter(GH_ParameterSide side, int index)
{
// only allow inserting if not yet added
if (HasApplicationIdParam)
{
return false;
}
// only allow at the end position
return side switch
{
GH_ParameterSide.Input => index == FixedInputCount,
GH_ParameterSide.Output => index == FixedOutputCount,
_ => false
};
}
public bool CanRemoveParameter(GH_ParameterSide side, int index)
{
if (!HasApplicationIdParam)
{
return false;
}
return side switch
{
GH_ParameterSide.Input => index == FixedInputCount,
GH_ParameterSide.Output => index == FixedOutputCount,
_ => false
};
}
/// <remarks>
/// The ternary for NickName handles a Grasshopper quirk where dynamically created parameters
/// don't respect the "Draw Full Names" setting automatically.
/// </remarks>
public IGH_Param CreateParameter(GH_ParameterSide side, int index)
{
// when adding on either side, add both input and output together
if (side == GH_ParameterSide.Input && Params.Output.Count == FixedOutputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => AddApplicationIdOutput());
}
else if (side == GH_ParameterSide.Output && Params.Input.Count == FixedInputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => AddApplicationIdInput());
}
return CreateApplicationIdParam();
}
public bool DestroyParameter(GH_ParameterSide side, int index)
{
// when removing from either side, remove both input and output together
if (side == GH_ParameterSide.Input && index == FixedInputCount && Params.Output.Count > FixedOutputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => RemoveApplicationIdOutput());
}
else if (side == GH_ParameterSide.Output && index == FixedOutputCount && Params.Input.Count > FixedInputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => RemoveApplicationIdInput());
}
return side switch
{
GH_ParameterSide.Input => index == FixedInputCount,
GH_ParameterSide.Output => index == FixedOutputCount,
_ => false
};
}
public void VariableParameterMaintenance()
{
// ensure the Application Id input stays optional
if (HasApplicationIdParam && Params.Input.Count > FixedInputCount)
{
Params.Input[FixedInputCount].Optional = true;
}
}
private static IGH_Param CreateApplicationIdParam() =>
new Param_String
{
Name = APP_ID_NAME,
NickName = Grasshopper.CentralSettings.CanvasFullNames ? APP_ID_NAME : APP_ID_NICKNAME,
Description = APP_ID_DESCRIPTION,
Access = GH_ParamAccess.item,
Optional = true
};
private void AddApplicationIdInput()
{
if (Params.Input.Count > FixedInputCount)
{
return;
}
Params.RegisterInputParam(CreateApplicationIdParam());
Params.OnParametersChanged();
VariableParameterMaintenance();
ExpireSolution(true);
}
private void AddApplicationIdOutput()
{
if (Params.Output.Count > FixedOutputCount)
{
return;
}
Params.RegisterOutputParam(CreateApplicationIdParam());
Params.OnParametersChanged();
ExpireSolution(true);
}
private void RemoveApplicationIdInput()
{
if (Params.Input.Count <= FixedInputCount)
{
return;
}
Params.UnregisterInputParameter(Params.Input[FixedInputCount]);
Params.OnParametersChanged();
ExpireSolution(true);
}
private void RemoveApplicationIdOutput()
{
if (Params.Output.Count <= FixedOutputCount)
{
return;
}
Params.UnregisterOutputParameter(Params.Output[FixedOutputCount]);
Params.OnParametersChanged();
ExpireSolution(true);
}
public override bool Write(GH_IWriter writer)
{
var result = base.Write(writer);
writer.SetBoolean("HasApplicationIdParam", HasApplicationIdParam);
return result;
}
public override bool Read(GH_IReader reader)
{
var result = base.Read(reader);
// parameters are restored by GH serialization, this flag is for reference
bool hasAppIdParam = false;
reader.TryGetBoolean("HasApplicationIdParam", ref hasAppIdParam);
return result;
}
}
@@ -1,83 +0,0 @@
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Sdk.Models.Collections;
namespace Speckle.Connectors.GrasshopperShared.Components.Collections;
/// <summary>
/// Shared helper methods for collection components to avoid code duplication
/// </summary>
public static class CollectionHelpers
{
/// <summary>
/// Creates a root collection wrapper with default values
/// </summary>
public static SpeckleCollectionWrapper CreateRootCollection(string instanceGuid) =>
new SpeckleCollectionWrapper
{
Base = new Collection(),
Name = "Unnamed",
Path = new List<string> { "Unnamed" },
Color = null,
Material = null,
ApplicationId = instanceGuid
};
/// <summary>
/// Validates that all application IDs are unique across the entire collection hierarchy.
/// </summary>
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
public static bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
{
var seenIds = new HashSet<string>();
var duplicateIds = new HashSet<string>();
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
return duplicateIds.Count > 0;
}
/// <summary>
/// Recursively collects application IDs from all wrappers in the collection hierarchy.
/// </summary>
/// <remarks>
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
/// </remarks>
private static void ProcessAndCheckForDuplicateApplicationIds(
SpeckleCollectionWrapper collection,
HashSet<string> seenIds,
HashSet<string> duplicateIds
)
{
foreach (var element in collection.Elements)
{
switch (element)
{
case null:
break; // skip nulls (CNX-2855)
case SpeckleCollectionWrapper childCollection:
// recurse into child collections
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
break;
case SpeckleWrapper wrapper:
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
{
duplicateIds.Add(wrapper.ApplicationId);
}
break;
}
}
}
/// <summary>
/// Recursively checks if collection or any descendants contain valid geometry/data objects
/// </summary>
public static bool HasAnyValidContent(ISpeckleCollectionObject? element) =>
element switch
{
SpeckleGeometryWrapper => true,
SpeckleDataObjectWrapper => true,
SpeckleCollectionWrapper collection => collection.Elements.Any(HasAnyValidContent),
_ => false
};
}
@@ -225,14 +225,6 @@ internal sealed class LocalToGlobalMapHandler
var entry = _dataObjectInstanceRegistry.GetEntries()[dataObjectId];
var resolvedGeometries = ResolveInstanceProxiesToGeometries(entry.InstanceProxies);
var primitiveConverted = dataObject
.displayValue.Where(item => item is not InstanceProxy)
.SelectMany(item => SpeckleConversionContext.Current.ConvertToHost(item))
.ToList();
resolvedGeometries.AddRange(ConvertToGeometryWrappers(primitiveConverted));
var dataObjectWrapper = CreateDataObjectWrapper(dataObject, resolvedGeometries, path, objectCollection);
CollectionRebuilder.AppendSpeckleGrasshopperObject(dataObjectWrapper, path, _colorUnpacker, _materialUnpacker);
@@ -14,7 +14,6 @@
<Compile Include="$(MSBuildThisFileDirectory)Components\BaseComponents\ValueSet.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\BaseComponents\VariableParameterComponentBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CollectionPathsSelector.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CollectionsByName.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CreateCollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\ExpandCollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Objects\ExpandSpeckleProperties.cs" />
@@ -42,9 +41,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\VersionMenuHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\WorkspaceMenuHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\SpeckleOperationWizard.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\SpecklePassthroughComponentBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\SpeckleSolveInstance.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\CollectionHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extras\StateTag.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\KeyWatcher.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\GrasshopperBlockUnpacker.cs" />
@@ -122,9 +122,7 @@ public class RhinoLayerBaker : TraversalContextUnpacker
continue;
}
var cleanNewLayerName = string.IsNullOrWhiteSpace(collection.name)
? "unnamed"
: RhinoUtils.CleanLayerName(collection.name);
var cleanNewLayerName = RhinoUtils.CleanLayerName(collection.name);
if (!ModelComponent.IsValidComponentName(cleanNewLayerName))
{
@@ -14,7 +14,6 @@ public static class RhinoUtils
public static string CleanLayerName(string str)
{
var sb = new StringBuilder(str.Length);
bool lastWasSpace = true;
foreach (char c in str)
{
@@ -31,29 +30,10 @@ public static class RhinoUtils
if (s_replaceWithHyphen.Contains(c))
{
sb.Append('-');
lastWasSpace = false;
continue;
}
// Collapse double spaces into one and skip leading spaces.
// e.g. " Items Name " -> "Items Name"
if (c == ' ')
{
if (!lastWasSpace)
{
sb.Append(c);
lastWasSpace = true;
}
continue;
}
sb.Append(c);
lastWasSpace = false;
}
if (sb.Length > 0 && sb[^1] == ' ')
{
sb.Length--;
}
return sb.ToString();
@@ -75,6 +75,8 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
)
{
using var activity = _activityFactory.Start("Build");
using var sendPipeline = new Speckle.Sdk.Pipeline.Send();
// 0 - Init the root
Collection rootObjectCollection = new() { name = _converterSettings.Current.Document.Name ?? "Unnamed document" };
rootObjectCollection["units"] = _converterSettings.Current.SpeckleUnits;
@@ -97,6 +99,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
// 3 - Convert atomic objects
List<SendConversionResult> results = new(atomicObjects.Count);
int count = 0;
using (var _ = _activityFactory.Start("Convert all"))
{
foreach (RhinoObject rhinoObject in atomicObjects)
@@ -108,9 +111,8 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
Layer layer = _converterSettings.Current.Document.Layers[rhinoObject.Attributes.LayerIndex];
Collection collectionHost = _layerUnpacker.GetHostObjectCollection(layer, rootObjectCollection);
var result = ConvertRhinoObject(rhinoObject, collectionHost, instanceProxies, projectId);
var result = await ConvertRhinoObject(rhinoObject, collectionHost, instanceProxies, projectId, sendPipeline);
results.Add(result);
++count;
onOperationProgressed.Report(new("Converting", (double)count / atomicObjects.Count));
await Task.Yield();
@@ -149,18 +151,23 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
}
}
return new RootObjectBuilderResult(rootObjectCollection, results);
await sendPipeline.Process(rootObjectCollection);
await sendPipeline.WaitForUpload();
return new RootObjectBuilderResult(new Collection() { name = "ignore" }, results);
}
private SendConversionResult ConvertRhinoObject(
private async Task<SendConversionResult> ConvertRhinoObject(
RhinoObject rhinoObject,
Collection collectionHost,
IReadOnlyDictionary<string, InstanceProxy> instanceProxies,
string projectId
string projectId,
Sdk.Pipeline.Send sendPipeline
)
{
string applicationId = rhinoObject.Id.ToString();
string sourceType = rhinoObject.ObjectType.ToString();
bool wasCached = false;
try
{
// get from cache or convert:
@@ -174,6 +181,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
else if (_sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
{
converted = value;
wasCached = true;
}
else
{
@@ -194,10 +202,17 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
converted["properties"] = properties;
}
// add to host
collectionHost.elements.Add(converted);
// process in pipeline
var reference = await sendPipeline.Process(converted).ConfigureAwait(false);
if (!wasCached)
{
_sendConversionCache.AppendSendResult(projectId, applicationId, reference);
}
return new(Status.SUCCESS, applicationId, sourceType, converted);
// add to host
collectionHost.elements.Add(reference);
return new(Status.SUCCESS, applicationId, sourceType, reference);
}
catch (Exception ex) when (!ex.IsFatal())
{
@@ -2,12 +2,11 @@ using Microsoft.Extensions.DependencyInjection;
using Rhino.PlugIns;
using Speckle.Connectors.Common;
using Speckle.Connectors.DUI;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.Rhino.DependencyInjection;
using Speckle.Converters.Rhino;
using Speckle.Sdk;
using Speckle.Sdk.Models.Extensions;
namespace Speckle.Connectors.Rhino.Plugin;
///<summary>
@@ -51,10 +50,6 @@ public class SpeckleConnectorsRhinoPlugin : PlugIn
// but the Rhino connector has `.rhp` as it is extension.
Container = services.BuildServiceProvider();
// FORCE INITIALIZATION RhinoDocumentStore to register event handlers for BeginOpenDocument and EndOpenDocument
// this is needed when the user opens a Rhino file by double clicking the file,
// instead of opening a file in an already running Rhino instance
Container.GetRequiredService<DocumentModelStore>();
Container.UseDUI();
return LoadReturnCode.Success;
@@ -1,8 +0,0 @@
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_";
}
@@ -1,6 +0,0 @@
namespace Speckle.Converter.Navisworks.Constants;
public static class MaterialConstants
{
public const string DEFAULT_MATERIAL_NAME_PREFIX = "NavisworksMaterial_";
}
@@ -1,8 +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 = ">";
}
@@ -4,8 +4,15 @@ namespace Speckle.Converter.Navisworks.ToSpeckle;
public class ClassPropertiesExtractor
{
public Dictionary<string, object?> GetClassProperties(NAV.ModelItem modelItem) =>
modelItem == null ? throw new ArgumentNullException(nameof(modelItem)) : ExtractClassProperties(modelItem);
public Dictionary<string, object?>? GetClassProperties(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
throw new ArgumentNullException(nameof(modelItem));
}
return ExtractClassProperties(modelItem);
}
/// <summary>
/// Extracts property sets from a NAV.ModelItem and adds them to a dictionary,
@@ -1,23 +1,19 @@
using Speckle.Converter.Navisworks.Services;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models;
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public class DisplayValueExtractor(
GeometryToSpeckleConverter geometryConverter,
IElementSelectionService elementSelectionService
)
public class DisplayValueExtractor(GeometryToSpeckleConverter geometryConverter)
{
internal List<Base> GetDisplayValue(NAV.ModelItem modelItem) =>
modelItem == null
? throw new ArgumentNullException(nameof(modelItem))
: !modelItem.HasGeometry || !elementSelectionService.IsVisible(modelItem)
? []
: GeometryConverter.Convert(modelItem);
/// <summary>
/// Gets the underlying geometry converter for accessing cache statistics.
/// </summary>
internal GeometryToSpeckleConverter GeometryConverter { get; } =
geometryConverter ?? throw new ArgumentNullException(nameof(geometryConverter));
: !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);
}
@@ -3,11 +3,18 @@ using Speckle.Converters.Common;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public class ModelPropertiesExtractor(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
public class ModelPropertiesExtractor
{
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
public ModelPropertiesExtractor(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
{
_settingsStore = settingsStore;
}
internal Dictionary<string, object?>? GetModelProperties(NAV.Model model)
{
if (settingsStore.Current.User.ExcludeProperties)
if (_settingsStore.Current.User.ExcludeProperties)
{
return null;
}
@@ -35,7 +35,7 @@ public class PropertySetsExtractor(
/// <summary>
/// Extracts property sets from a NAV.ModelItem and adds them to a dictionary,
/// PropertySets are the specific set per host application source appended to Navisworks and therefore
/// PropertySets are specific to the host application source appended to Navisworks and therefore
/// arbitrary in nature.
/// </summary>
/// <param name="modelItem">The NAV.ModelItem from which property sets are extracted.</param>
@@ -28,12 +28,6 @@ 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();
@@ -78,8 +78,15 @@ public abstract class BasePropertyHandler(
}
}
private static Dictionary<string, object?> CreatePropertyDictionary(Dictionary<string, object?> properties) =>
properties.Where(prop => IsValidPropertyValue(prop.Value)).ToDictionary(prop => prop.Key, prop => prop.Value);
private static Dictionary<string, object?> CreatePropertyDictionary(Dictionary<string, object?> properties)
{
var propertyDict = new Dictionary<string, object?>();
foreach (var prop in properties.Where(prop => IsValidPropertyValue(prop.Value)))
{
propertyDict[prop.Key] = prop.Value;
}
return propertyDict;
}
protected static bool IsValidPropertyValue(object? value) => value != null && !string.IsNullOrEmpty(value.ToString());
}
@@ -20,7 +20,7 @@ public class HierarchicalPropertyHandler(
public override Dictionary<string, object?> 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))
@@ -1,6 +1,5 @@
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;
@@ -43,17 +42,19 @@ public static class NavisworksConverterServiceRegistration
serviceCollection.AddScoped<PrimitiveProcessor>();
serviceCollection.AddScoped<PropertySetsExtractor>();
// Register element selection service
serviceCollection.AddScoped<ElementSelectionService>();
// Register geometry conversion
serviceCollection.AddScoped<DisplayValueExtractor>();
serviceCollection.AddScoped<GeometryToSpeckleConverter>(sp =>
{
var settingsStore = sp.GetRequiredService<IConverterSettingsStore<NavisworksConversionSettings>>();
var registry = sp.GetRequiredService<IInstanceFragmentRegistry>();
return new GeometryToSpeckleConverter(settingsStore.Current, registry);
});
serviceCollection.AddScoped<GeometryToSpeckleConverter>();
// 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<InstanceStoreManager>();
// Register ISharedGeometryStore interface using the geometry definitions store for backward compatibility
serviceCollection.AddScoped<ISharedGeometryStore>(provider =>
provider.GetRequiredService<InstanceStoreManager>().GeometryDefinitionsStore
);
// Register settings resolved from factory
serviceCollection.AddScoped<NavisworksConversionSettings>(sp =>
sp.GetRequiredService<INavisworksConversionSettingsFactory>().Current
@@ -1,8 +1,5 @@
using System.Diagnostics.CodeAnalysis;
namespace Speckle.Converter.Navisworks.Geometry;
namespace Speckle.Converter.Navisworks.Geometry;
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public readonly struct SafeBoundingBox
{
public SafeVertex Center { get; }
@@ -106,7 +103,6 @@ public readonly struct SafeVertex
}
}
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public readonly struct SafePoint
{
public SafeVertex Vertex { get; }
@@ -1,4 +1,4 @@
using static Speckle.Converter.Navisworks.Constants.PathConstants;
using Speckle.Converter.Navisworks.Constants;
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}{SEPARATOR}{modelItemPathId.PathId}"; // Nested model item
: $"{modelItemPathId.ModelIndex}{PathConstants.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(MATERIAL_SEPARATOR, StringComparison.Ordinal);
int separatorIndex = indexPath.IndexOf(PathConstants.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(SEPARATOR);
var indexPathParts = pathToResolve.Split(PathConstants.SEPARATOR);
var modelIndex = int.Parse(indexPathParts[0]);
var pathId = string.Join(SEPARATOR.ToString(), indexPathParts.Skip(1));
var pathId = string.Join(PathConstants.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,6 +72,23 @@ public static class ElementSelectionHelper
return modelItem;
}
/// <summary>
/// Determines whether a Navisworks <see cref="NAV.ModelItem"/> and all its ancestors are visible.
/// </summary>
/// <param name="modelItem">The model item to check for visibility.</param>
/// <returns>True if the item and all ancestors are visible; otherwise, false.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="modelItem"/> is null.</exception>
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<NAV.ModelItem> ResolveGeometryLeafNodes(NAV.ModelItem modelItem) =>
modelItem.DescendantsAndSelf.Where(x => x.HasGeometry);
}
@@ -1,24 +1,4 @@
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)
);
}
namespace Speckle.Converter.Navisworks.Helpers;
public static class GeometryHelpers
{
@@ -27,292 +7,10 @@ public static class GeometryHelpers
/// </summary>
/// <param name="vectorA">The first comparison vector.</param>
/// <param name="vectorB">The second comparison vector.</param>
/// <param name="tolerance">The tolerance value for the comparison. The default is 1e-9.</param>
/// <param name="tolerance">The tolerance value for the comparison. Default is 1e-9.</param>
/// <returns>True if the vectors match within the tolerance; otherwise, false.</returns>
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;
}
/// <summary>
/// Multiplies two 4x4 matrices in row-major order.
/// Used to compute instance transforms: inverse(definitionWorld) × instanceWorld
/// </summary>
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;
}
@@ -5,11 +5,6 @@ using Speckle.DoubleNumerics;
namespace Speckle.Converter.Navisworks.Helpers;
/// <summary>
/// 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.
/// </summary>
public class PrimitiveProcessor : InwSimplePrimitivesCB
{
private readonly List<double> _coords = [];
@@ -57,13 +52,13 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB
var safeLine = new SafeLine(vD1, vD2);
AddLine(safeLine);
}
catch (ArgumentException)
catch (ArgumentException ex)
{
// Invalid line geometry - skip
Console.WriteLine($"ArgumentException caught: {ex.Message}");
}
catch (InvalidOperationException)
catch (InvalidOperationException ex)
{
// Invalid line geometry - skip
Console.WriteLine($"InvalidOperationException caught: {ex.Message}");
}
}
@@ -83,6 +78,7 @@ 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)
@@ -105,6 +101,7 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB
IsUpright
);
// Capture values immediately in our safe struct
var safeTriangle = new SafeTriangle(vD1, vD2, vD3);
var indexPointer = Faces.Count;
@@ -168,9 +165,6 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB
return new NAV.Vector3D(vectorDoubleX, vectorDoubleY, vectorDoubleZ);
}
/// <summary>
/// WARNING: Called for every vertex - COM marshaling overhead from Array cast and 3 GetValue() calls.
/// </summary>
private static Vector3 VectorFromVertex(InwSimpleVertex v)
{
var arrayV = (Array)v.coord;
@@ -0,0 +1,19 @@
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_";
}
@@ -1,131 +0,0 @@
namespace Speckle.Converter.Navisworks.Paths;
public readonly record struct PathKey
{
internal readonly int[] Data;
internal readonly int Hash;
public static readonly IEqualityComparer<PathKey> 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);
}
/// <summary>
/// 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.
/// </summary>
public string ToHashString() => unchecked((uint)Hash).ToString();
}
internal sealed class PathKeyComparer : IEqualityComparer<PathKey>
{
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;
}
@@ -1,151 +0,0 @@
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<PathKey> instancePaths);
void MarkConverted(PathKey instancePath);
IEnumerable<PathKey> GetConvertedPaths();
Dictionary<PathKey, List<PathKey>> 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<Base> geometry);
bool TryGetDefinitionGeometry(PathKey groupKey, out List<Base> geometry);
Dictionary<PathKey, List<Base>> GetAllDefinitionGeometries();
List<PathKey> GetAllGroupKeys();
void RegisterInstanceObservation(
PathKey groupKey,
PathKey instancePath,
double[] instanceWorld,
PrimitiveProcessor processor
);
}
public sealed class InstanceFragmentRegistry : IInstanceFragmentRegistry
{
private readonly Dictionary<PathKey, PathKey> _pathToGroup = new(PathKey.Comparer);
private readonly HashSet<PathKey> _converted = new(PathKey.Comparer);
private readonly Dictionary<PathKey, double[]> _groupToDefinitionWorld = new(PathKey.Comparer);
private readonly Dictionary<PathKey, double[]> _pathToInstanceWorld = new(PathKey.Comparer);
private readonly Dictionary<PathKey, Aabb> _groupSignature = new(PathKey.Comparer);
private readonly Dictionary<PathKey, List<Base>> _groupDefinitions = new(PathKey.Comparer);
public bool TryGetGroup(PathKey instancePath, out PathKey groupKey) =>
_pathToGroup.TryGetValue(instancePath, out groupKey);
public void RegisterGroup(PathKey groupKey, HashSet<PathKey> instancePaths)
{
foreach (var p in instancePaths)
{
_pathToGroup[p] = groupKey;
}
}
public void MarkConverted(PathKey instancePath) => _converted.Add(instancePath);
public IEnumerable<PathKey> GetConvertedPaths() => _converted;
public Dictionary<PathKey, List<PathKey>> BuildGroupToConvertedPaths()
{
var map = new Dictionary<PathKey, List<PathKey>>(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<Base> geometry) => _groupDefinitions[groupKey] = geometry;
public bool TryGetDefinitionGeometry(PathKey groupKey, out List<Base> geometry) =>
_groupDefinitions.TryGetValue(groupKey, out geometry);
public Dictionary<PathKey, List<Base>> GetAllDefinitionGeometries() => new(_groupDefinitions, PathKey.Comparer);
public List<PathKey> 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;
}
}
}
@@ -1,191 +0,0 @@
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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>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.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
@@ -1,46 +0,0 @@
using Speckle.InterfaceGenerator;
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
namespace Speckle.Converter.Navisworks.Services;
[GenerateAutoInterface]
public class ElementSelectionService : IElementSelectionService
{
private readonly Dictionary<Guid, bool> _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<NAV.ModelItem> GetGeometryNodes(NAV.ModelItem modelItem) => ResolveGeometryLeafNodes(modelItem);
}
@@ -0,0 +1,152 @@
// using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Constants;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converter.Navisworks.Services;
/// <summary>
/// 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.
/// </summary>
public class InstanceStoreManager(
// ILogger<InstanceStoreManager> logger
)
{
// private readonly ILogger<InstanceStoreManager> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
/// <summary>
/// Store for geometry definitions (geometry data) - untransformed base geometries.
/// </summary>
internal SharedGeometryStore GeometryDefinitionsStore { get; } = new();
/// <summary>
/// Store for InstanceDefinitionProxy objects that reference geometry definitions.
/// </summary>
internal SharedGeometryStore InstanceDefinitionProxiesStore { get; } = new();
/// <summary>
/// Clears both stores for a new conversion session.
/// Should be called at the start of each conversion.
/// </summary>
public void ClearAll()
{
GeometryDefinitionsStore.Clear();
InstanceDefinitionProxiesStore.Clear();
}
/// <summary>
/// Gets all instance definition proxies from the store, cast to their specific type.
/// Useful for adding to root collection at end of conversion.
/// </summary>
public IReadOnlyCollection<InstanceDefinitionProxy> GetInstanceDefinitionProxies()
{
var proxies = InstanceDefinitionProxiesStore.Geometries.OfType<InstanceDefinitionProxy>().ToList().AsReadOnly();
// _logger.LogDebug("GetInstanceDefinitionProxies returning {Count} proxies", proxies.Count);
return proxies;
}
/// <summary>
/// Gets all geometry definitions from the geometry definitions store.
/// </summary>
/// <returns></returns>
public List<Base> GetGeometryDefinitions() => [.. GeometryDefinitionsStore.Geometries.ToList().AsReadOnly()];
/// <summary>
/// Gets a geometry definition by its application ID from the geometry definitions store.
/// </summary>
/// <returns>The geometry if found, null otherwise.</returns>
public Base? GetGeometryDefinition(string fragmentId) =>
GeometryDefinitionsStore.Geometries.FirstOrDefault(g =>
g.applicationId == $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}"
);
/// <summary>
/// Gets an instance definition proxy by its application ID.
/// </summary>
/// <returns>The instance definition proxy if found, null otherwise.</returns>
public InstanceDefinitionProxy? GetInstanceDefinitionProxy(string fragmentId) =>
InstanceDefinitionProxiesStore
.Geometries.OfType<InstanceDefinitionProxy>()
.FirstOrDefault(p => p.applicationId == $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}");
/// <summary>
/// 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).
/// </summary>
/// <param name="fragmentId">The fragment-based application ID.</param>
/// <param name="geometries">The untransformed base geometries (meshes, lines, points).</param>
/// <returns>True if geometries were added (new geometry), false if they already existed.</returns>
public bool AddSharedGeometry(string fragmentId, List<Base> 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<string>();
// _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;
}
/// <summary>
/// Checks if shared geometry already exists in the stores.
/// Uses the instance definition proxy as the authoritative check since it references all geometries.
/// </summary>
/// <param name="fragmentId">The fragment-based application ID.</param>
/// <returns>True if the instance definition proxy exists for this fragment.</returns>
public bool ContainsSharedGeometry(string fragmentId) =>
InstanceDefinitionProxiesStore.Contains($"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}");
}
@@ -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 a dictionary with units
// Angle as dictionary with units
{ NAV.VariantDataType.DoubleAngle, (v, t) => NumObj(t.name, v.ToDoubleAngle(), "Degrees") },
// Length → dictionary in UI units
{
@@ -2,23 +2,28 @@
namespace Speckle.Converter.Navisworks.Settings;
/// <summary>
/// Represents the settings used for Navisworks conversions.
/// </summary>
public record NavisworksConversionSettings(Derived Derived, User User);
// Derived from Navisworks Application
public record Derived(
NAV.Document Document,
SafeBoundingBox ModelBoundingBox,
SafeVector TransformVector,
bool IsUpright,
string SpeckleUnits
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.
);
// Optional settings for conversion to be offered in UI
public record User(
OriginMode OriginMode,
bool IncludeInternalProperties,
bool ConvertHiddenElements,
RepresentationMode VisualRepresentationMode,
bool CoalescePropertiesFromFirstObjectAncestor,
bool ExcludeProperties,
bool PreserveModelHierarchy,
bool RevitCategoryMapping = true
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).
);
@@ -7,21 +7,39 @@ using Speckle.InterfaceGenerator;
namespace Speckle.Converter.Navisworks.Settings;
[GenerateAutoInterface]
public class NavisworksConversionSettingsFactory(
IHostToSpeckleUnitConverter<NAV.Units> unitsConverter,
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
ILogger<NavisworksConversionSettingsFactory> logger
) : INavisworksConversionSettingsFactory
public class NavisworksConversionSettingsFactory : INavisworksConversionSettingsFactory
{
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
private readonly ILogger<NavisworksConversionSettingsFactory> _logger;
private readonly IHostToSpeckleUnitConverter<NAV.Units> _unitsConverter;
private NAV.Document? _document;
private SafeBoundingBox _modelBoundingBox;
private bool _convertHiddenElements;
private OriginMode _originMode;
public NavisworksConversionSettings Current => settingsStore.Current;
public NavisworksConversionSettingsFactory(
IHostToSpeckleUnitConverter<NAV.Units> unitsConverter,
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
ILogger<NavisworksConversionSettingsFactory> logger
)
{
_logger = logger;
_settingsStore = settingsStore;
_unitsConverter = unitsConverter;
}
public NavisworksConversionSettings Current => _settingsStore.Current;
private static readonly NAV.Vector3D s_canonicalUp = new(0, 0, 1);
private OriginMode _originMode;
/// <summary>
/// Creates a new instance of NavisworksConversionSettings with calculated values.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when no active document is found or document units cannot be converted.
/// </exception>
public NavisworksConversionSettings Create(
OriginMode originMode,
RepresentationMode visualRepresentationMode,
@@ -42,7 +60,7 @@ public class NavisworksConversionSettingsFactory(
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.");
@@ -78,7 +96,7 @@ public class NavisworksConversionSettingsFactory(
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));
}
@@ -91,15 +109,36 @@ public class NavisworksConversionSettingsFactory(
_ => throw new NotSupportedException($"OriginMode {_originMode} is not supported.")
};
/// <summary>
/// Calculates the transformation vector based on the project base point.
/// </summary>
/// <returns>The calculated transformation vector.</returns>
/// <remarks>
/// This uses mocked project base point data and should be replaced with actual logic
/// when finally integrating with UI or external configurations.
/// </remarks>
private SafeVector CalculateProjectBasePointTransform()
{
// WARNING: Mocked data - replace with actual UI/settings when implementing project base point
// TODO: Replace with actual logic to fetch project base point and units from UI or settings
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);
}
/// <summary>
/// Calculates the transformation vector based on the bounding box center offset from the origin.
/// </summary>
/// <returns>The calculated transformation vector.</returns>
/// <remarks>
/// This uses the document active model bounding box center as the base point for the transformation.
/// Assumes no translation in the Z-axis.
/// </remarks>
private SafeVector CalculateBoundingBoxTransform() =>
new(-_modelBoundingBox.Center.X, -_modelBoundingBox.Center.Y, 0);
}
@@ -9,42 +9,39 @@
<Import_RootNamespace>Speckle.Converters.NavisworksShared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Constants\InstanceConstants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Constants\MaterialConstants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Constants\PathConstants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Paths\PathKey.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Registers\InstanceFragmentRegistry.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\ElementSelectionService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)PathConstants.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\PropertyConversion.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\UIUnits.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\SharedGeometryStores.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettingsFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\NavisworksRootToSpeckleConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoundingBoxToSpeckleRawConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\ModelItemTopLevelConverterToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\UIUnits.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\SharedGeometryStores.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\InstanceStoreManager.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs"/>
<Folder Include="$(MSBuildThisFileDirectory)Models\"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettingsFactory.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\NavisworksRootToSpeckleConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoundingBoxToSpeckleRawConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\ModelItemTopLevelConverterToSpeckle.cs"/>
</ItemGroup>
</Project>
@@ -1,4 +1,6 @@
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;
@@ -6,9 +8,23 @@ using Speckle.Sdk.Models;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public class NavisworksRootToSpeckleConverter(IConverterManager<IToSpeckleTopLevelConverter> toSpeckle)
: IRootToSpeckleConverter
public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter
{
private readonly IConverterManager<IToSpeckleTopLevelConverter> _toSpeckle;
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
private readonly ILogger<NavisworksRootToSpeckleConverter> _logger;
public NavisworksRootToSpeckleConverter(
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
ILogger<NavisworksRootToSpeckleConverter> logger,
IConverterManager<IToSpeckleTopLevelConverter> toSpeckle
)
{
_toSpeckle = toSpeckle;
_logger = logger;
_settingsStore = settingsStore;
}
public Base Convert(object target)
{
if (target == null)
@@ -22,7 +38,7 @@ public class NavisworksRootToSpeckleConverter(IConverterManager<IToSpeckleTopLev
}
Type type = target.GetType();
var objectConverter = toSpeckle.ResolveConverter(type);
var objectConverter = _toSpeckle.ResolveConverter(type, true);
Base result = objectConverter.Convert(modelItem);
result.applicationId = ElementSelectionHelper.ResolveModelItemToIndexPath(modelItem);
@@ -6,22 +6,28 @@ using Speckle.Objects.Primitive;
namespace Speckle.Converter.Navisworks.ToSpeckle.Raw;
public class BoundingBoxToSpeckleRawConverter(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
: ITypedConverter<NAV.BoundingBox3D, Box>
public class BoundingBoxToSpeckleRawConverter : ITypedConverter<NAV.BoundingBox3D, Box>
{
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
public BoundingBoxToSpeckleRawConverter(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
{
_settingsStore = settingsStore;
}
public Box Convert(object target) => Convert((NAV.BoundingBox3D)target);
public Box Convert(NAV.BoundingBox3D? target)
{
if (target == null)
{
return null!; // returns null for reference types (Box is a reference type)
return default!; // 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
{
@@ -1,44 +1,47 @@
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.Paths;
using Speckle.Converter.Navisworks.Services;
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;
/// <summary>
/// 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.
/// </summary>
public sealed class GeometryToSpeckleConverter(
/// <remarks>
/// 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.
/// </remarks>
public class GeometryToSpeckleConverter(
NavisworksConversionSettings settings,
IInstanceFragmentRegistry registry
InstanceStoreManager instanceStoreManager,
ILogger<GeometryToSpeckleConverter> logger
)
{
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 const bool ENABLE_INSTANCING = true;
private readonly Dictionary<PathKey, int> _groupMemberCounts = new(PathKey.Comparer);
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];
internal List<Base> Convert(NAV.ModelItem modelItem)
private readonly InstanceStoreManager _instanceStoreManager =
instanceStoreManager ?? throw new ArgumentNullException(nameof(instanceStoreManager));
private readonly ILogger<GeometryToSpeckleConverter> _logger =
logger ?? throw new ArgumentNullException(nameof(logger));
internal List<SSM.Base> Convert(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
@@ -50,10 +53,10 @@ public sealed class GeometryToSpeckleConverter(
return [];
}
NAV.ModelItemCollection collection = new() { modelItem };
var comSelection = ComApiBridge.ToInwOpSelection(modelItemCollection: collection);
var comSelection = ComApiBridge.ToInwOpSelection([modelItem]);
try
{
var fragmentStack = new Stack<InwOaFragment3>();
var paths = comSelection.Paths();
if (paths == null)
{
@@ -61,87 +64,32 @@ public sealed class GeometryToSpeckleConverter(
}
try
{
var allResults = new List<Base>(5);
foreach (InwOaPath path in paths)
if (paths.Count > 0)
{
if (path.ArrayData is not Array pathArr)
var firstPath = paths.Cast<InwOaPath>().First();
var fragmentsCollection = firstPath.Fragments();
try
{
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++)
if (fragmentsCollection.Count > 1)
{
definitionGeometry[i].applicationId = $"{GEOMETRY_ID_PREFIX}{groupKeyHash}_{i}";
return ProcessSharedGeometry(paths, fragmentStack);
}
_registry.StoreDefinitionGeometry(groupKey, definitionGeometry);
}
if (ENABLE_INSTANCING)
finally
{
// Transform matrix to Z-up space if model is Y-up, matching vertex transformation
var transformedWorld = _isUpright ? instanceWorld : TransformMatrixYUpToZUp(instanceWorld);
var instanceProxy = new InstanceProxy
if (fragmentsCollection != null)
{
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);
Marshal.ReleaseComObject(fragmentsCollection);
}
}
}
return allResults;
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths, true);
}
finally
{
@@ -155,128 +103,119 @@ public sealed class GeometryToSpeckleConverter(
Marshal.ReleaseComObject(comSelection);
}
}
collection.Dispose();
}
private static HashSet<PathKey> DiscoverInstancePathsFromFragments(InwOaPath path)
private static void CollectFragments(InwOaPath path, Stack<InwOaFragment3> fragmentStack)
{
var set = new HashSet<PathKey>(PathKey.Comparer);
var fragments = path.Fragments();
try
{
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
foreach (var fragment in fragments.OfType<InwOaFragment3>())
{
GC.KeepAlive(fragment);
if (AreFragmentPathsEqual(fragment, path))
{
fragmentStack.Push(fragment);
}
}
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
}
}
InwOaPath? fragPath = fragment.path;
if (fragPath?.ArrayData is not Array fragPathArr)
private List<SSM.Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
{
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<SSM.Base> ProcessFragments(
Stack<InwOaFragment3> fragmentStack,
InwSelectionPathsColl paths,
bool isSingleObject = false
)
{
var callbackListeners = new List<PrimitiveProcessor>();
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 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<InwOaFragment3>())
{
GC.KeepAlive(fragment);
InwOaPath? fragPath = null;
InwLTransform3f3? transform = null;
var fragmentsForCount = path.Fragments();
int fragmentCount;
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;
fragmentCount = fragmentsForCount?.Count ?? 0;
}
finally
{
if (transform != null)
if (fragmentsForCount != null)
{
Marshal.ReleaseComObject(transform);
}
if (fragPath != null)
{
Marshal.ReleaseComObject(fragPath);
Marshal.ReleaseComObject(fragmentsForCount);
}
}
double[] makeNoChange = s_identityTransform;
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
processor.LocalToWorldTransformation =
isSingleObject || fragmentCount == 1 ? transformMatrix : (IEnumerable<double>)makeNoChange;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
callbackListeners.Add(processor);
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
}
return ProcessGeometries(callbackListeners);
}
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
private static bool AreFragmentPathsEqual(InwOaFragment3 fragment, InwOaPath path) =>
fragment.path?.ArrayData is Array fragmentPathData
&& path.ArrayData is Array pathData
&& AreFragmentPathsEqual(fragmentPathData, pathData);
private List<SSM.Base> ProcessGeometries(List<PrimitiveProcessor> processors)
{
var baseGeometries = new List<Base>(processors.Count * 2);
var baseGeometries = new List<SSM.Base>();
foreach (var processor in processors)
{
@@ -286,13 +225,17 @@ public sealed class GeometryToSpeckleConverter(
baseGeometries.Add(mesh);
}
if (processor.Lines.Count <= 0)
if (processor.Lines.Count > 0)
{
continue;
var lines = CreateLines(processor.Lines);
baseGeometries.AddRange(lines);
}
var lines = CreateLines(processor.Lines);
baseGeometries.AddRange(lines);
if (processor.Points.Count > 0)
{
var points = CreatePoints(processor.Points);
baseGeometries.AddRange(points);
}
}
return baseGeometries;
@@ -300,12 +243,13 @@ public sealed class GeometryToSpeckleConverter(
private Mesh CreateMesh(IReadOnlyList<SafeTriangle> triangles)
{
var vertices = new List<double>(triangles.Count * 9);
var faces = new List<int>(triangles.Count * 4);
var vertices = new List<double>();
var faces = new List<int>();
for (var t = 0; t < triangles.Count; t++)
{
var triangle = triangles[t];
vertices.AddRange(
[
(triangle.Vertex1.X + _transformVector.X) * SCALE,
@@ -330,35 +274,412 @@ public sealed class GeometryToSpeckleConverter(
};
}
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines)
{
var result = new List<Line>(lines.Count);
private List<Line> CreateLines(IReadOnlyList<SafeLine> 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();
foreach (var line in lines)
private List<Point> CreatePoints(IReadOnlyList<SafePoint> 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)
{
try
{
result.Add(
new Line
if (paths.Count == 0)
{
return string.Empty;
}
var fragmentHashes = new List<string>();
foreach (var fragments in from InwOaPath path in paths select path.Fragments())
{
try
{
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
var fragmentIndex = 0;
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
{
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++;
}
}
);
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<string>();
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<SSM.Base> ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
{
var processor = new PrimitiveProcessor(_isUpright);
foreach (var fragment in fragmentStack)
{
processor.LocalToWorldTransformation = s_identityTransform;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
var geometries = new List<SSM.Base>();
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<SSM.Base> 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<InwOaPath>().First();
var fragments = firstPath.Fragments();
try
{
if (fragments.Count == 0)
{
return s_identityMatrix;
}
var fragmentStack = new Stack<InwOaFragment3>();
foreach (var frag in fragments.OfType<InwOaFragment3>())
{
if (frag.path?.ArrayData is not Array pathData1 || firstPath.ArrayData is not Array pathData2)
{
continue;
}
var pathArray1 = pathData1.Cast<int>().ToArray();
var pathArray2 = pathData2.Cast<int>().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;
}
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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)
@@ -375,117 +696,6 @@ public sealed class GeometryToSpeckleConverter(
return doubleArray;
}
/// <summary>
/// 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.
/// </summary>
private static List<Base> UnbakeGeometry(List<Base> bakedGeometry, double[] invWorld)
{
var result = new List<Base>(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;
}
/// <summary>
/// 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).
/// </summary>
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));
private static bool AreFragmentPathsEqual(Array a1, Array a2) =>
a1.Length == a2.Length && a1.Cast<int>().SequenceEqual(a2.Cast<int>());
}
@@ -8,20 +8,42 @@ using Speckle.Sdk.Models.Collections;
namespace Speckle.Converter.Navisworks.ToSpeckle;
/// <summary>
/// Converts Navisworks ModelItem objects to Speckle Base objects.
/// </summary>
[NameAndRankValue(typeof(NAV.ModelItem), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
public class ModelItemToToSpeckleConverter(
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
StandardPropertyHandler standardHandler,
HierarchicalPropertyHandler hierarchicalHandler,
DisplayValueExtractor displayValueExtractor
) : IToSpeckleTopLevelConverter
public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
{
private readonly StandardPropertyHandler _standardHandler;
private readonly HierarchicalPropertyHandler _hierarchicalHandler;
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
private readonly DisplayValueExtractor _displayValueExtractor;
public ModelItemToToSpeckleConverter(
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
StandardPropertyHandler standardHandler,
HierarchicalPropertyHandler hierarchicalHandler,
DisplayValueExtractor displayValueExtractor
)
{
_settingsStore = settingsStore;
_standardHandler = standardHandler;
_hierarchicalHandler = hierarchicalHandler;
_displayValueExtractor = displayValueExtractor;
}
/// <summary>
/// Converts a Navisworks object to a Speckle Base object.
/// </summary>
/// <param name="target">The object to convert.</param>
/// <returns>The converted Speckle Base object.</returns>
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
@@ -29,29 +51,28 @@ public class ModelItemToToSpeckleConverter(
: CreateNonGeometryObject(target, name, handler);
}
private NavisworksObject CreateGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler)
{
var displayValue = displayValueExtractor.GetDisplayValue(target);
var geometryObject = new NavisworksObject
// 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()
{
units = settingsStore.Current.Derived.SpeckleUnits,
name = name,
properties = settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target),
displayValue = displayValue
displayValue = _displayValueExtractor.GetDisplayValue(target),
properties = _settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target),
units = _settingsStore.Current.Derived.SpeckleUnits,
};
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),
};
/// <summary>
/// Determines whether properties should be merged from ancestors.
/// Only geometry objects should have their properties merged.... For now.
/// </summary>
private static bool ShouldMergeProperties(NAV.ModelItem target) => target.HasGeometry;
private static string GetObjectName(NAV.ModelItem target)
@@ -60,6 +81,8 @@ public class ModelItemToToSpeckleConverter(
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;
@@ -75,8 +75,7 @@ public sealed class DisplayValueExtractor
}
return areaDisplay;
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView
// Reference point transform is handled by point converters during conversion
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView + apply reference point transform
case DB.Structure.Rebar rebar:
return _converterSettings.Current.SendRebarsAsVolumetric
? GetRebarVolumetricDisplayValue(rebar)
@@ -109,16 +108,15 @@ public sealed class DisplayValueExtractor
using DB.Transform? documentToLocal = localToDocument?.Inverse;
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
DB.Transform? localToWorld = (localToDocument, documentToWorld) switch
{
(not null, not null) => documentToWorld.Multiply(localToDocument),
(not null, null) => localToDocument,
(null, not null) => documentToWorld,
(null, null) => null
};
using DB.Transform? compoundTransform =
localToDocument is not null && documentToWorld is not null
? documentToWorld.Multiply(localToDocument)
: localToDocument;
DB.Transform? localToWorld = compoundTransform ?? documentToWorld;
var collections = GetSortedGeometryFromElement(element, options, documentToLocal);
return ProcessGeometryCollections(element, collections, localToWorld, localToDocument);
return ProcessGeometryCollections(element, collections, localToWorld);
}
/// <summary>
@@ -170,15 +168,13 @@ public sealed class DisplayValueExtractor
/// Converts sorted geometry into DisplayValueResults <see cref="ElementTopLevelConverterToSpeckle"/>.
/// </summary>
/// <remarks>
/// Meshes get localToWorld attached as transform metadata (for instancing).
/// Curves, polylines, and points get curveTransform applied (instance transform only) -
/// reference point transform is handled by the point converters.
/// Applies localToWorld only to curves, points, polylines.
/// Meshes remain in symbol space to generate correct instance proxies and avoid duplicates.
/// </remarks>
private List<DisplayValueResult> ProcessGeometryCollections(
DB.Element element,
GeometryCollections collections,
DB.Transform? localToWorld,
DB.Transform? curveTransform
DB.Transform? localToWorld
)
{
var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids);
@@ -200,24 +196,16 @@ public sealed class DisplayValueExtractor
foreach (var curve in collections.Curves)
{
if (curveTransform is not null)
{
using var transformedCurve = curve.CreateTransformed(curveTransform);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
var transformedCurve = localToWorld != null ? curve.CreateTransformed(localToWorld) : curve;
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
foreach (var polyline in collections.Polylines)
{
if (curveTransform is not null)
if (localToWorld != null)
{
var coords = polyline.GetCoordinates();
var transformedCoords = coords.Select(coord => curveTransform.OfPoint(coord)).ToList();
using var transformedPolyline = DB.PolyLine.Create(transformedCoords);
var coords = polyline.GetCoordinates().Select(p => localToWorld.OfPoint(p)).ToList();
using var transformedPolyline = DB.PolyLine.Create(coords);
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(transformedPolyline)));
}
else
@@ -228,9 +216,9 @@ public sealed class DisplayValueExtractor
foreach (var point in collections.Points)
{
if (curveTransform is not null)
if (localToWorld != null)
{
using var transformedPoint = DB.Point.Create(curveTransform.OfPoint(point.Coord));
using var transformedPoint = DB.Point.Create(localToWorld.OfPoint(point.Coord));
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(transformedPoint)));
}
else
@@ -570,7 +558,7 @@ public sealed class DisplayValueExtractor
{
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
SortGeometry(rebar, collections, geometryElements, null);
return ProcessGeometryCollections(rebar, collections, documentToWorld, null);
return ProcessGeometryCollections(rebar, collections, documentToWorld);
}
// Return empty list if no geometry is found - imo not critical
@@ -611,11 +599,20 @@ public sealed class DisplayValueExtractor
)
);
}
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
List<DisplayValueResult> displayValue = new();
foreach (var curve in curves)
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
if (documentToWorld is not null)
{
using var transformedCurve = curve.CreateTransformed(documentToWorld);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
}
return displayValue;
@@ -633,7 +630,7 @@ public sealed class DisplayValueExtractor
{
var collections = GetSortedGeometryFromElement(element, null, null);
// pass null for transform - curves are already in correct document coordinates
return ProcessGeometryCollections(element, collections, null, null);
return ProcessGeometryCollections(element, collections, null);
}
/// <summary>
@@ -642,10 +639,9 @@ public sealed class DisplayValueExtractor
/// and reduce the risk of parameter ordering errors.
/// </summary>
/// <remarks>
/// <see cref="Solids"/> and <see cref="Meshes"/> are transformed to symbol space in SortGeometry.
/// <see cref="Solids"/> and <see cref="Meshes"/> are transformed to local coordinate space in SortGeometry.
/// <see cref="Curves"/>, <see cref="Polylines"/>, and <see cref="Points"/> remain in their original coordinate space
/// and receive only the instance transform (if any) in ProcessGeometryCollections - reference point
/// transform is handled by the point converters during conversion.
/// and are transformed to world space during processing in ProcessGeometryCollections.
/// </remarks>
private sealed record GeometryCollections
{
@@ -61,7 +61,6 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveArrArrayToSpecklePolycurveConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveArrayConversionToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveConversionToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveOriginToPlaneConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\EllipseToSpeckleConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\HermiteSplineToSpeckleConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\LineConversionToSpeckle.cs" />
@@ -2,7 +2,6 @@ using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects.Data;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Extensions;
@@ -11,14 +10,11 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
/// <summary>
/// Converts local to global maps to direct shapes.
/// When atomicObject comes from an InstanceProxy displayValue, parentDataObject
/// provides the original DataObject's metadata (category, name) for semantic preservation.
/// Spirit of the LocalToGlobalMap, we can't pass that object directly here bc it lives in Connectors.Common which I (ogu) don't want to bother with it.
/// All this is poc that should be burned, once we enable proper block support to revit.
/// </summary>
public class LocalToGlobalToDirectShapeConverter
: ITypedConverter<
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject),
DB.DirectShape
>
: ITypedConverter<(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix), DB.DirectShape>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<(Matrix4x4 matrix, string units), DB.Transform> _transformConverter;
@@ -32,13 +28,22 @@ public class LocalToGlobalToDirectShapeConverter
_transformConverter = transformConverter;
}
public DB.DirectShape Convert(
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject) target
)
public DB.DirectShape Convert((Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix) target)
{
// 1- set ds category
var category = ExtractBuiltInCategory(target.parentDataObject, target.atomicObject);
var name = target.parentDataObject?.name ?? target.atomicObject.TryGetName();
// NOTE: previously, builtInCategory was on the atomicObject level. this was subsequently moved to properties
string? category = null;
// NOTE: no longer limited to DataObject since the introduction of mapper
// The change from `if (target.atomicObject is DataObject dataObject)` is very hacky, but nothing else to do for now
// TODO: better define prop interfaces for different applications
if (
target.atomicObject["properties"] is Dictionary<string, object?> properties
&& properties.TryGetValue("builtInCategory", out var builtInCategory)
)
{
category = builtInCategory?.ToString();
}
var dsCategory = DB.BuiltInCategory.OST_GenericModel;
if (category is not null)
@@ -57,6 +62,10 @@ public class LocalToGlobalToDirectShapeConverter
// 2 - init DirectShape
var result = DB.DirectShape.CreateElement(_converterSettings.Current.Document, new DB.ElementId(dsCategory));
// NOTE: this should technically be in a property extraction class / helper method
// This change is localised to [CNX-1825](https://linear.app/speckle/issue/CNX-1825/set-directshape-name)
// TODO: Property extraction is a greater conversation which needs to be had: [CNX-1830](https://linear.app/speckle/issue/CNX-1830/data-exchange-investigations)
var name = target.atomicObject.TryGetName();
if (name is not null)
{
result.SetName(name);
@@ -112,24 +121,4 @@ public class LocalToGlobalToDirectShapeConverter
result.SetShape(transformedGeometries);
return result;
}
private static string? ExtractBuiltInCategory(DataObject? parentDataObject, Base atomicObject)
{
// Try parent DataObject first (for InstanceProxy displayValue case)
if (parentDataObject?.properties.TryGetValue("builtInCategory", out var cat) == true)
{
return cat?.ToString();
}
// Fallback to atomicObject properties
if (
atomicObject["properties"] is Dictionary<string, object?> props
&& props.TryGetValue("builtInCategory", out var fallbackCat)
)
{
return fallbackCat?.ToString();
}
return null;
}
}
@@ -1,4 +1,3 @@
using Autodesk.Revit.DB;
using Speckle.Converters.Common;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Settings;
@@ -81,45 +80,24 @@ public class ClassPropertiesExtractor
// get room id if applicable (only for FamilyInstance elements)
if (familyInstance.Room is not null)
{
elementProperties.Add("roomApplicationId", familyInstance.Room.UniqueId.ToString());
elementProperties.Add("roomId", familyInstance.Room.Id.ToString());
}
// get space id if applicable (only for FamilyInstance elements)
if (familyInstance.Space is not null)
{
elementProperties.Add("spaceApplicationId", familyInstance.Space.UniqueId.ToString());
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
}
// get toRoom and fromRoom for FamilyInstance elements with those properties (e.g. Doors)
if (familyInstance.ToRoom is not null)
{
elementProperties.Add("toRoomApplicationId", familyInstance.ToRoom.UniqueId.ToString());
elementProperties.Add("toRoomId", familyInstance.ToRoom.Id.ToString());
}
if (familyInstance.FromRoom is not null)
{
elementProperties.Add("fromRoomApplicationId", familyInstance.FromRoom.UniqueId.ToString());
}
Element? parent = null;
#if REVIT2023_OR_GREATER
BuiltInCategory bic = familyInstance.Category.BuiltInCategory;
#else
// Cast for 2022 and older
BuiltInCategory bic = (BuiltInCategory)familyInstance.Category.Id.IntegerValue;
#endif
if (bic == BuiltInCategory.OST_CurtainWallMullions || bic == BuiltInCategory.OST_CurtainWallPanels)
{
parent = familyInstance.Host;
}
parent ??= familyInstance.SuperComponent;
if (parent != null)
{
elementProperties.Add("parentApplicationId", parent.UniqueId);
elementProperties.Add("fromRoomId", familyInstance.FromRoom.Id.ToString());
}
}
catch (Exception e) when (!e.IsFatal())
@@ -1,5 +1,6 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Objects.Primitive;
@@ -9,34 +10,34 @@ public class ArcToSpeckleConverter : ITypedConverter<DB.Arc, SOG.Arc>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
private readonly ScalingServiceToSpeckle _scalingService;
public ArcToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
ScalingServiceToSpeckle scalingService
)
{
_converterSettings = converterSettings;
_xyzToPointConverter = xyzToPointConverter;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
_planeConverter = planeConverter;
_scalingService = scalingService;
}
public SOG.Arc Convert(DB.Arc target)
{
// Revit arcs are always counterclockwise in the arc normal direction. This aligns with Speckle arc plane convention.
var arcPlane = DB.Plane.CreateByOriginAndBasis(target.Center, target.XDirection, target.YDirection);
DB.XYZ start = target.Evaluate(0, true);
DB.XYZ end = target.Evaluate(1, true);
DB.XYZ mid = target.Evaluate(0.5, true);
return new SOG.Arc()
{
plane = _curveOriginToPlaneConverter.Convert(
(target.Center, target.XDirection, target.YDirection, target.Normal)
),
plane = _planeConverter.Convert(arcPlane),
units = _converterSettings.Current.SpeckleUnits,
endPoint = _xyzToPointConverter.Convert(end),
startPoint = _xyzToPointConverter.Convert(start),
@@ -9,20 +9,17 @@ public class BoundingBoxXYZToSpeckleConverter : ITypedConverter<DB.BoundingBoxXY
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
public BoundingBoxXYZToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter
ITypedConverter<DB.Plane, SOG.Plane> planeConverter
)
{
_converterSettings = converterSettings;
_xyzToPointConverter = xyzToPointConverter;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
_planeConverter = planeConverter;
}
public SOG.Box Convert(DB.BoundingBoxXYZ target)
@@ -33,19 +30,21 @@ public class BoundingBoxXYZToSpeckleConverter : ITypedConverter<DB.BoundingBoxXY
// get the base plane of the bounding box from the transform
var transform = target.Transform;
var plane = DB.Plane.CreateByOriginAndBasis(
transform.Origin,
transform.BasisX.Normalize(),
transform.BasisY.Normalize()
);
// assemble components for getting origin plane
var xDir = transform.BasisX.Normalize();
var yDir = transform.BasisY.Normalize();
var normal = xDir.CrossProduct(yDir).Normalize();
return new SOG.Box()
var box = new SOG.Box()
{
xSize = new Interval { start = min.x, end = max.x },
ySize = new Interval { start = min.y, end = max.y },
zSize = new Interval { start = min.z, end = max.z },
plane = _curveOriginToPlaneConverter.Convert((transform.Origin, xDir, yDir, normal)),
plane = _planeConverter.Convert(plane),
units = _converterSettings.Current.SpeckleUnits
};
return box;
}
}
@@ -8,32 +8,33 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
public class CircleToSpeckleConverter : ITypedConverter<DB.Arc, SOG.Circle>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
private readonly ScalingServiceToSpeckle _scalingService;
public CircleToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
ScalingServiceToSpeckle scalingService
)
{
_converterSettings = converterSettings;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
_planeConverter = planeConverter;
_scalingService = scalingService;
}
public SOG.Circle Convert(DB.Arc target) =>
public SOG.Circle Convert(DB.Arc target)
{
// POC: should we check for arc of 360 and throw? Original CircleToSpeckle did not do this.
// see https://forums.autodesk.com/t5/revit-api-forum/how-to-retrieve-startangle-and-endangle-of-arc-object/td-p/7637128
new()
var arcPlane = DB.Plane.CreateByNormalAndOrigin(target.Normal, target.Center);
var c = new SOG.Circle()
{
plane = _curveOriginToPlaneConverter.Convert(
(target.Center, target.XDirection, target.YDirection, target.Normal)
),
plane = _planeConverter.Convert(arcPlane),
radius = _scalingService.ScaleLength(target.Radius),
units = _converterSettings.Current.SpeckleUnits
};
return c;
}
}
@@ -1,61 +0,0 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Settings;
namespace Speckle.Converters.RevitShared.ToSpeckle;
/// <summary>
/// Converts curve origin plane components directly to Speckle plane.
/// </summary>
/// <remarks>
/// <para>
/// Revit's <see cref="DB.Plane.CreateByOriginAndBasis"/> fails when the origin is ~10+ km from the internal origin,
/// even though the documented limit is 16 km. This is a known API limitation:
/// https://forums.autodesk.com/t5/revit-api-forum/the-input-point-lies-outside-of-revit-design-limits/td-p/12689066
///</para>
/// <para>
/// This converter bypasses Revit plane creation entirely and builds Speckle planes directly from
/// the curve's origin and basis vectors.
/// </para>
/// </remarks>
public class CurveOriginToPlaneConverter
: ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<DB.XYZ, SOG.Vector> _xyzToVectorConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
public CurveOriginToPlaneConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<DB.XYZ, SOG.Vector> xyzToVectorConverter,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter
)
{
_converterSettings = converterSettings;
_xyzToPointConverter = xyzToPointConverter;
_xyzToVectorConverter = xyzToVectorConverter;
_planeConverter = planeConverter;
}
public SOG.Plane Convert((DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal) target)
{
// within limits? then use standard Revit plane creation
if (DB.XYZ.IsWithinLengthLimits(target.origin))
{
using var revitPlane = DB.Plane.CreateByOriginAndBasis(target.origin, target.xDir, target.yDir);
return _planeConverter.Convert(revitPlane);
}
// beyond limits? then build Speckle plane directly
return new SOG.Plane
{
origin = _xyzToPointConverter.Convert(target.origin),
xdir = _xyzToVectorConverter.Convert(target.xDir),
ydir = _xyzToVectorConverter.Convert(target.yDir),
normal = _xyzToVectorConverter.Convert(target.normal),
units = _converterSettings.Current.SpeckleUnits
};
}
}
@@ -9,40 +9,40 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
public class EllipseToSpeckleConverter : ITypedConverter<DB.Ellipse, SOG.Ellipse>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
private readonly ScalingServiceToSpeckle _scalingService;
public EllipseToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
ScalingServiceToSpeckle scalingService
)
{
_converterSettings = converterSettings;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
_planeConverter = planeConverter;
_scalingService = scalingService;
}
public SOG.Ellipse Convert(DB.Ellipse target)
{
var trim = target.IsBound
? new Interval { start = target.GetEndParameter(0), end = target.GetEndParameter(1) }
: null;
return new SOG.Ellipse()
using (DB.Plane basePlane = DB.Plane.CreateByOriginAndBasis(target.Center, target.XDirection, target.YDirection))
{
plane = _curveOriginToPlaneConverter.Convert(
(target.Center, target.XDirection, target.YDirection, target.Normal)
),
firstRadius = _scalingService.ScaleLength(target.RadiusX),
secondRadius = _scalingService.ScaleLength(target.RadiusY),
domain = Interval.UnitInterval,
trimDomain = trim,
length = _scalingService.ScaleLength(target.Length),
units = _converterSettings.Current.SpeckleUnits
};
var trim = target.IsBound
? new Interval { start = target.GetEndParameter(0), end = target.GetEndParameter(1) }
: null;
return new SOG.Ellipse()
{
plane = _planeConverter.Convert(basePlane),
// POC: scale length correct? seems right?
firstRadius = _scalingService.ScaleLength(target.RadiusX),
secondRadius = _scalingService.ScaleLength(target.RadiusY),
// POC: original EllipseToSpeckle() method was setting this twice
domain = Interval.UnitInterval,
trimDomain = trim,
length = _scalingService.ScaleLength(target.Length),
units = _converterSettings.Current.SpeckleUnits
};
}
}
}
@@ -14,7 +14,6 @@ namespace Speckle.Connectors.DUI.Models;
public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJsonSerializer serializer)
: IDocumentModelStore
{
public event EventHandler<ModelCardsChangedEventArgs>? ModelCardsChanged;
private readonly List<ModelCard> _models = new();
/// <summary>
@@ -153,7 +152,6 @@ public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJs
{
var state = Serialize();
HostAppSaveState(state);
ModelCardsChanged?.Invoke(this, new ModelCardsChangedEventArgs(_models.ToList().AsReadOnly()));
}
}
@@ -1,8 +0,0 @@
using Speckle.Connectors.DUI.Models.Card;
namespace Speckle.Connectors.DUI.Models;
public sealed class ModelCardsChangedEventArgs(IReadOnlyList<ModelCard> modelCards) : EventArgs
{
public IReadOnlyList<ModelCard> ModelCards { get; } = modelCards;
}
@@ -14,6 +14,7 @@ namespace Speckle.Connectors.Common.Caching;
public interface ISendConversionCache
{
void StoreSendResult(string projectId, IReadOnlyDictionary<Id, ObjectReference> convertedReferences);
void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference);
/// <summary>
/// <para>Call this method whenever you need to invalidate a set of objects that have changed in the host app.</para>
@@ -11,6 +11,8 @@ public class NullSendConversionCache : ISendConversionCache
{
public void StoreSendResult(string projectId, IReadOnlyDictionary<Id, ObjectReference> convertedReferences) { }
public void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference) { }
public void EvictObjects(IEnumerable<string> objectIds) { }
public void ClearCache() { }
@@ -17,6 +17,11 @@ public class SendConversionCache : ISendConversionCache
}
}
public void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference)
{
Cache[(applicationId, projectId)] = convertedReference;
}
/// <inheritdoc/>
public void EvictObjects(IEnumerable<string> objectIds) =>
Cache = Cache