Compare commits

...

12 Commits

Author SHA1 Message Date
Jonathon Broughton 79a6c42cc7 Improves geometry conversion performance
Introduces caching mechanisms for fragment IDs and geometry to avoid redundant processing, significantly enhancing geometry conversion speed.

Adds methods to clear caches and retrieve statistics for performance monitoring and memory usage estimation.

Handles COM exceptions and invalid operations more gracefully, preventing crashes and improving stability.
2025-11-15 18:03:36 +00:00
Jonathon Broughton e2318df87a Adds COM object lifetime management utility
Introduces a `ComScope` struct to automatically manage the
lifecycle of Navisworks COM objects using RAII principles.

This prevents memory leaks caused by unreleased COM objects,
addressing a common issue with Navisworks interop.
2025-11-15 18:03:17 +00:00
Jonathon Broughton 78d7814351 Fixes RenderMaterials 2025-11-14 19:11:16 +03:00
Jonathon Broughton 17a320ee53 LFG!!!
Matrix4x4 Transposed
Adds an application ID to instance references.
2025-11-14 18:33:32 +03:00
Jonathon Broughton 396ef981ee Fixes geometry processing for instance handling
Refactors geometry processing logic to handle instances more efficiently.
It now correctly applies transformations for single objects, ensuring accurate placement in the scene.
Simplifies processing of transforms where necessary for single objects.
Removes redundant logging.
2025-11-14 16:41:58 +03:00
Jonathon Broughton 763c413871 Adds instance geometry support to Navisworks connector
Introduces instance geometry handling for more efficient and accurate Navisworks data streams.

This includes:
- Scoping the `InstanceStoreManager` to each conversion session.
- Creating a "Geometry Definitions" collection to store shared geometry.
- Adding instance definition proxies to the root collection.

Addresses issues with duplicate geometry and improves stream performance.
2025-11-14 14:48:31 +03:00
Jonathon Broughton 57a5b41ec1 Refactors display value extraction
Streamlines display value extraction by injecting the geometry converter.

This change simplifies the DisplayValueExtractor by removing its dependencies on settings and logging.
It now directly uses a GeometryToSpeckleConverter instance, which is passed in, for converting model item geometry.
2025-11-14 14:47:47 +03:00
Jonathon Broughton 8385532b96 Introduces instance store management
Adds a service to manage shared geometry and instance definition proxies.

This system uses two stores for geometry definitions and their instance proxies,
facilitating efficient handling of shared geometry. It provides methods for
adding, retrieving, and clearing geometries, ensuring deduplication and
optimized memory usage.
2025-11-14 14:47:14 +03:00
Jonathon Broughton e109515852 Implements geometry instancing for Navisworks
Adds geometry instancing to improve performance and reduce data size when converting Navisworks models with shared geometry.

It identifies shared geometries based on fragment paths, extracts the base geometry once, and creates instance references with transforms. This reduces data duplication and improves loading times in Speckle.

Improves handling of COM array data for fragment ID generation and includes comprehensive logging for debugging instancing issues.
2025-11-14 14:46:54 +03:00
Jonathon Broughton fdf2425ec6 Adds shared project file for Navisworks converter
Introduces a shared project file for the Navisworks converter, promoting code reuse and consistency across different Navisworks converter implementations.

This file includes common data extractors, data handlers, helpers, services, and settings, reducing duplication and improving maintainability.
2025-11-14 14:46:28 +03:00
Jonathon Broughton c067cf6f91 Adds instance store manager for geometry
Introduces an instance store manager to handle shared geometry, which includes separate stores for geometry definitions and instance proxies.

This change prepares for the implementation of an instancing pattern compatible with .NET Framework.
It also registers settings from a factory to resolve conversion settings.
2025-11-14 14:45:49 +03:00
Jonathon Broughton 60e26d85c6 Introduces shared geometry store service
Creates a singleton service for deduplicating geometry during conversion.

This service stores mesh objects, preventing redundant processing of identical geometries by leveraging application IDs.

It uses a dictionary and a hash set to optimize lookup and storage, and includes thread safety mechanisms.
2025-11-13 14:17:50 +03:00
12 changed files with 1028 additions and 151 deletions
@@ -16,6 +16,7 @@ using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.DUI.Models.Card.SendFilter;
using Speckle.Connectors.DUI.WebView;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converters.Common;
using Speckle.Sdk.Models.GraphTraversal;
@@ -52,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
@@ -1,7 +1,10 @@
using Autodesk.Navisworks.Api.ComApi;
using Autodesk.Navisworks.Api.Interop.ComApi;
using Microsoft.Extensions.Logging;
using Speckle.Connector.Navisworks.Services;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.ToSpeckle;
using Speckle.Converters.Common;
using Speckle.Objects.Other;
using Speckle.Sdk;
@@ -11,7 +14,8 @@ namespace Speckle.Connector.Navisworks.HostApp;
public class NavisworksMaterialUnpacker(
ILogger<NavisworksMaterialUnpacker> logger,
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
IElementSelectionService selectionService
IElementSelectionService selectionService,
GeometryToSpeckleConverter converter
)
{
// Helper function to select a property based on the representation mode
@@ -64,6 +68,19 @@ public class NavisworksMaterialUnpacker(
var navisworksObjectId = selectionService.GetModelItemPath(navisworksObject);
var finalId = mergedIds.TryGetValue(navisworksObjectId, out var mergedId) ? mergedId : navisworksObjectId;
var item = selectionService.GetModelItemFromPath(finalId);
string hashId = "";
var comSelection = ComApiBridge.ToInwOpSelection([item]);
var paths = comSelection.Paths();
var path = paths.OfType<InwOaPath>().First();
var fragments = path.Fragments();
if (fragments.Count > 1)
{
var fragmentId = converter.GenerateFragmentId(paths);
hashId = $"geom_{fragmentId}";
}
var geometry = navisworksObject.Geometry;
var mode = converterSettings.Current.User.VisualRepresentationMode;
@@ -120,7 +137,7 @@ public class NavisworksMaterialUnpacker(
if (renderMaterialProxies.TryGetValue(renderMaterialId.ToString(), out RenderMaterialProxy? value))
{
value.objects.Add(finalId);
value.objects.Add(!string.IsNullOrEmpty(hashId) ? hashId : finalId);
}
else
{
@@ -132,7 +149,7 @@ public class NavisworksMaterialUnpacker(
renderColor,
renderMaterialId
),
objects = [finalId]
objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId]
};
}
}
@@ -6,6 +6,7 @@ using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Operations;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using Speckle.Objects.Data;
@@ -25,7 +26,8 @@ public class NavisworksRootObjectBuilder(
ISdkActivityFactory activityFactory,
NavisworksMaterialUnpacker materialUnpacker,
NavisworksColorUnpacker colorUnpacker,
IElementSelectionService elementSelectionService
IElementSelectionService elementSelectionService,
InstanceStoreManager instanceStoreManager
) : IRootObjectBuilder<NAV.ModelItem>
{
private bool SkipNodeMerging { get; set; }
@@ -41,7 +43,7 @@ public class NavisworksRootObjectBuilder(
{
#if DEBUG
// This is a temporary workaround to disable node merging for debugging purposes - false is default, true is for debugging
SkipNodeMerging = false;
SkipNodeMerging = true;
#endif
using var activity = activityFactory.Start("Build");
@@ -50,22 +52,42 @@ public class NavisworksRootObjectBuilder(
// 2. Initialize root collection
var rootCollection = InitializeRootCollection();
// InstanceStoreManager is scoped - starts fresh for each conversion session
// 3. Convert all model items and store results
var (convertedElements, conversionResults) = await ConvertModelItemsAsync(
navisworksModelItems,
projectId,
onOperationProgressed,
cancellationToken
);
(Dictionary<string, Base?> convertedElements, List<SendConversionResult> conversionResults) =
await ConvertModelItemsAsync(navisworksModelItems, projectId, onOperationProgressed, cancellationToken);
ValidateConversionResults(conversionResults);
var groupedNodes = SkipNodeMerging ? [] : GroupSiblingGeometryNodes(navisworksModelItems);
var finalElements = BuildFinalElements(convertedElements, groupedNodes);
List<Base> geometryDefinitions = instanceStoreManager.GetGeometryDefinitions();
await AddProxiesToCollection(rootCollection, navisworksModelItems, groupedNodes);
rootCollection.elements = finalElements;
// rootCollection.elements will contain two Collections: one for geometry definitions and one for the main elements
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);
}
return new RootObjectBuilderResult(rootCollection, conversionResults);
}
@@ -288,6 +310,24 @@ public class NavisworksRootObjectBuilder(
rootCollection[ProxyKeys.COLOR] = colors;
}
// Add instance definition proxies from dual store
var instanceDefinitionProxies = instanceStoreManager.GetInstanceDefinitionProxies();
logger.LogDebug("Retrieved {Count} instance definition proxies from store", instanceDefinitionProxies.Count);
if (instanceDefinitionProxies.Count > 0)
{
rootCollection[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies.ToList();
logger.LogDebug(
"Added {Count} instance definition proxies to root collection under key '{Key}'",
instanceDefinitionProxies.Count,
ProxyKeys.INSTANCE_DEFINITION
);
}
else
{
logger.LogDebug("No instance definition proxies to add to root collection");
}
return Task.CompletedTask;
}
@@ -1,38 +1,19 @@
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models;
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public class DisplayValueExtractor
public class DisplayValueExtractor(GeometryToSpeckleConverter geometryConverter)
{
private readonly IConverterSettingsStore<NavisworksConversionSettings> _converterSettings;
private readonly ILogger<DisplayValueExtractor> _logger;
private readonly GeometryToSpeckleConverter _geometryConverter;
public DisplayValueExtractor(
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
ILogger<DisplayValueExtractor> logger
)
{
_converterSettings = converterSettings;
_logger = logger;
_geometryConverter = new GeometryToSpeckleConverter(_converterSettings.Current);
}
internal List<Base> GetDisplayValue(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
throw new ArgumentNullException(nameof(modelItem));
}
if (!modelItem.HasGeometry)
{
return [];
}
return !IsElementVisible(modelItem) ? [] : _geometryConverter.Convert(modelItem);
}
internal List<Base> GetDisplayValue(NAV.ModelItem modelItem) =>
modelItem == null
? throw new ArgumentNullException(nameof(modelItem))
: !modelItem.HasGeometry
? ([])
: !IsElementVisible(modelItem)
? []
:
// this can be meshes or the instance reference objects
// the un transformed objects stored in a separate collection
geometryConverter.Convert(modelItem);
}
@@ -46,6 +46,19 @@ public static class NavisworksConverterServiceRegistration
serviceCollection.AddScoped<DisplayValueExtractor>();
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
);
return serviceCollection;
}
}
@@ -0,0 +1,60 @@
using System.Runtime.InteropServices;
namespace Speckle.Converter.Navisworks.Helpers;
/// <summary>
/// ComScope - Because Navisworks COM objects are like vampires that never die unless you tell them to.
///
/// This is a RAII (Resource Acquisition Is Initialization) wrapper for COM objects.
/// Think of it as a babysitter that makes sure COM objects get cleaned up properly
/// when you're done with them, preventing memory leaks that would otherwise
/// slowly consume your machine's RAM like a digital Pac-Man.
///
/// Why do we need this?
/// - Navisworks COM API creates objects that live forever unless explicitly released
/// - Forgetting to call Marshal.ReleaseComObject() = memory leak city
/// - Using statements + IDisposable = automatic cleanup when scope ends
/// - One less thing to remember = fewer bugs = happier developers
///
/// Usage: Wrap it in a 'using' statement and let C# handle the cleanup:
/// using var comThing = new ComScope&lt;SomeComType&gt;(myComObject);
/// // Do stuff with comThing.Value
/// // Automatic cleanup happens here when using block ends
///
/// Pro tip: This prevents the "why is Navisworks eating all my RAM?" conversations
/// that happen way too often with COM interop code.
/// </summary>
/// <typeparam name="T">The COM object type we're babysitting</typeparam>
public readonly struct ComScope<T>(T comObject, bool shouldRelease = true) : IDisposable
where T : class
{
public ComScope(T comObject)
: this(comObject, false) { }
private T ComObject { get; } = comObject;
private bool ShouldRelease { get; } = shouldRelease;
public T Value => ComObject;
/// <summary>
/// The magic cleanup method. This gets called automatically when the 'using' block ends.
/// It tells the COM object "your services are no longer required" in a polite way
/// that doesn't crash the application.
/// </summary>
public void Dispose()
{
// Only release if we're supposed to AND the object actually exists
if (ShouldRelease && ComObject != null)
{
try
{
// This is the important bit - tells COM runtime to decrement reference count
Marshal.ReleaseComObject(ComObject);
}
catch (InvalidComObjectException)
{
// Sometimes the object is already gone (maybe someonereleased, ignore
}
}
}
}
@@ -48,9 +48,7 @@ public static class PropertyHelpers
}
// Default case for unsupported types
return value.DataType == NAV.VariantDataType.None || value.DataType == NAV.VariantDataType.Point2D
? null
: value.ToString();
return value.DataType is NAV.VariantDataType.None or NAV.VariantDataType.Point2D ? null : value.ToString();
}
/// <summary>
@@ -0,0 +1,154 @@
using Microsoft.Extensions.Logging;
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 == $"geom_{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 == $"def_{fragmentId}");
/// <summary>
/// Adds a geometry definition and corresponding instance definition proxy for shared geometry.
/// This is a convenience method that handles both stores in one call.
/// </summary>
/// <param name="fragmentId">The fragment-based application ID.</param>
/// <param name="geometry">The untransformed base geometry.</param>
/// <returns>True if both were added (new geometry), false if they already existed.</returns>
public bool AddSharedGeometry(string fragmentId, Base geometry)
{
_logger.LogDebug("AddSharedGeometry called for FragmentId={FragmentId}", fragmentId);
bool geometryAdded = false;
bool proxyAdded = false;
// Create prefixed IDs for 1:1:1 relationship using base fragment hash
var geometryId = $"geom_{fragmentId}";
var definitionId = $"def_{fragmentId}";
_logger.LogDebug("Using GeometryId={GeometryId}, DefinitionId={DefinitionId}", geometryId, definitionId);
// Add geometry definition if not exists
if (!GeometryDefinitionsStore.Contains(geometryId))
{
geometry.applicationId = geometryId;
geometryAdded = GeometryDefinitionsStore.Add(geometry);
_logger.LogDebug("Added geometry definition: {GeometryId}, Success={Success}", geometryId, geometryAdded);
}
else
{
_logger.LogDebug("Geometry definition already exists: {GeometryId}", geometryId);
}
// Add instance definition proxy if not exists
if (!InstanceDefinitionProxiesStore.Contains(definitionId))
{
if (geometry.applicationId == null)
{
_logger.LogWarning(
"Cannot create instance definition proxy - geometry.id is null for FragmentId={FragmentId}",
fragmentId
);
var result = geometryAdded || proxyAdded;
_logger.LogDebug(
"AddSharedGeometry completed: FragmentId={FragmentId}, Result={Result}, GeometryAdded={GeometryAdded}, ProxyAdded={ProxyAdded}",
fragmentId,
result,
geometryAdded,
proxyAdded
);
return result;
}
var definitionProxy = new InstanceDefinitionProxy
{
applicationId = definitionId,
name = $"Shared Geometry {fragmentId[..8]}...", // Show first 8 chars for readability
objects = [geometry.applicationId],
maxDepth = 0
};
proxyAdded = InstanceDefinitionProxiesStore.Add(definitionProxy);
_logger.LogDebug("Added instance definition proxy: {DefinitionId}, Success={Success}", definitionId, proxyAdded);
}
else
{
_logger.LogDebug("Instance definition proxy already exists: {DefinitionId}", definitionId);
}
var conversionSucceededResult = geometryAdded || proxyAdded;
_logger.LogDebug(
"AddSharedGeometry completed: FragmentId={FragmentId}, Result={Result}, GeometryAdded={GeometryAdded}, ProxyAdded={ProxyAdded}",
fragmentId,
conversionSucceededResult,
geometryAdded,
proxyAdded
);
return conversionSucceededResult;
}
/// <summary>
/// Checks if shared geometry already exists in both stores.
/// </summary>
/// <param name="fragmentId">The fragment-based application ID.</param>
/// <returns>True if geometry definition exists in both stores.</returns>
public bool ContainsSharedGeometry(string fragmentId) => GeometryDefinitionsStore.Contains($"geom_{fragmentId}")
// && InstanceDefinitionProxiesStore.Contains($"def_{fragmentId}")
;
}
@@ -0,0 +1,92 @@
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Models;
namespace Speckle.Converter.Navisworks.Services;
[GenerateAutoInterface]
public class SharedGeometryStore : ISharedGeometryStore
{
private readonly HashSet<Base> _geometries = new();
private readonly Dictionary<string, Base> _geometriesByApplicationId = new();
private readonly object _lock = new();
/// <summary>
/// Gets a read-only collection of all stored geometries.
/// </summary>
public IReadOnlyCollection<Base> Geometries
{
get
{
lock (_lock)
{
return _geometries.ToList().AsReadOnly();
}
}
}
/// <summary>
/// Adds a geometry to the store if it doesn't already exist.
/// </summary>
/// <param name="geometry">The geometry to add.</param>
/// <returns>True if the geometry was added, false if it already existed.</returns>
public bool Add(Base geometry)
{
if (geometry == null)
{
throw new ArgumentNullException(nameof(geometry));
}
if (string.IsNullOrEmpty(geometry.applicationId))
{
throw new ArgumentException("Geometry must have an applicationId for deduplication", nameof(geometry));
}
lock (_lock)
{
if (geometry.applicationId != null && _geometriesByApplicationId.ContainsKey(geometry.applicationId))
{
return false; // Already exists
}
_geometries.Add(geometry);
if (geometry.applicationId != null)
{
_geometriesByApplicationId[geometry.applicationId] = geometry;
}
return true; // Added successfully
}
}
/// <summary>
/// Checks if a geometry with the specified application ID already exists in the store.
/// </summary>
/// <param name="applicationId">The application ID to check for.</param>
/// <returns>True if a geometry with the application ID exists, false otherwise.</returns>
public bool Contains(string applicationId)
{
if (string.IsNullOrEmpty(applicationId))
{
return false;
}
lock (_lock)
{
var contains = _geometriesByApplicationId.ContainsKey(applicationId);
return contains;
}
}
/// <summary>
/// Clears all stored geometries for a new conversion session.
/// </summary>
public void Clear()
{
lock (_lock)
{
_geometries.Clear();
_geometriesByApplicationId.Clear();
}
}
}
@@ -9,35 +9,38 @@
<Import_RootNamespace>Speckle.Converters.NavisworksShared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<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)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" />
<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\ComScope.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\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>
</Project>
@@ -1,11 +1,15 @@
using System.Runtime.InteropServices;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using Autodesk.Navisworks.Api.Interop.ComApi;
using Speckle.Converter.Navisworks.Extensions;
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Geometry;
using Speckle.Converter.Navisworks.Helpers;
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 ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
namespace Speckle.Converter.Navisworks.ToSpeckle;
@@ -21,23 +25,59 @@ namespace Speckle.Converter.Navisworks.ToSpeckle;
/// 3. Process each InwOaFragment3 to generate primitives
/// 4. Convert those primitives to Speckle geometry with appropriate transforms
/// </summary>
public class GeometryToSpeckleConverter
public class GeometryToSpeckleConverter(
NavisworksConversionSettings settings,
InstanceStoreManager instanceStoreManager,
ILogger<GeometryToSpeckleConverter> logger
)
{
private readonly NavisworksConversionSettings _settings;
private readonly bool _isUpright;
private readonly SafeVector _transformVector;
private readonly NavisworksConversionSettings _settings =
settings ?? throw new ArgumentNullException(nameof(settings));
private readonly bool _isUpright = settings.Derived.IsUpright;
private readonly SafeVector _transformVector = settings.Derived.TransformVector;
private const double SCALE = 1.0; // Default scale factor
public GeometryToSpeckleConverter(NavisworksConversionSettings settings)
private readonly InstanceStoreManager _instanceStoreManager =
instanceStoreManager ?? throw new ArgumentNullException(nameof(instanceStoreManager));
private readonly ILogger<GeometryToSpeckleConverter> _logger =
logger ?? throw new ArgumentNullException(nameof(logger));
// Fragment ID cache for performance optimization
private readonly ConcurrentDictionary<int, string> _fragmentIdCache = new();
// Geometry cache for repeated items
private readonly ConcurrentDictionary<string, List<Base>> _geometryCache = new();
/// <summary>
/// Clears all internal caches. Should be called when starting a new conversion session.
/// </summary>
public void ClearCaches()
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_isUpright = settings.Derived.IsUpright;
_transformVector = settings.Derived.TransformVector;
_fragmentIdCache.Clear();
_geometryCache.Clear();
}
/// <summary>
/// Gets cache statistics for performance monitoring.
/// </summary>
/// <returns>A record containing cache hit counts and sizes</returns>
public (int FragmentIdCacheSize, int GeometryCacheSize, double CacheMemoryEstimateMB) GetCacheStatistics()
{
var fragmentCacheSize = _fragmentIdCache.Count;
var geometryCacheSize = _geometryCache.Count;
// Rough memory estimate (fragment IDs ~50 bytes, geometry objects ~10KB average)
var estimatedMemoryMb = (fragmentCacheSize * 50 + geometryCacheSize * 10240) / (1024.0 * 1024.0);
return (fragmentCacheSize, geometryCacheSize, Math.Round(estimatedMemoryMb, 2));
}
/// <summary>
/// Converts a ModelItem's geometry to Speckle display geometry by accessing the underlying COM objects.
/// Applies necessary transformations and unit scaling.
/// When path.Fragments().Count > 1, extracts untransformed base geometry once, stores in SharedGeometryStore,
/// and returns instance references. Otherwise, returns transformed geometry directly.
/// </summary>
internal List<Base> Convert(NAV.ModelItem modelItem)
{
@@ -51,59 +91,134 @@ public class GeometryToSpeckleConverter
return [];
}
var comSelection = ComApiBridge.ToInwOpSelection([modelItem]);
// Check geometry cache first
var itemId = modelItem.InstanceGuid.ToString();
if (_geometryCache.TryGetValue(itemId, out var cachedGeometry))
{
return cachedGeometry;
}
using var comSelection = new ComScope<InwOpSelection>(ComApiBridge.ToInwOpSelection([modelItem]));
var fragmentStack = new Stack<InwOaFragment3>();
using var paths = new ComScope<InwSelectionPathsColl>(comSelection.Value.Paths());
try
{
var fragmentStack = new Stack<InwOaFragment3>();
var paths = comSelection.Paths();
try
// Check if this geometry is shared across multiple instances
List<Base> result;
if (paths.Value.Count > 0)
{
// Populate fragment stack with all fragments
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
var firstPath = paths.Value.Cast<InwOaPath>().First();
var fragmentsCollection = firstPath.Fragments();
return ProcessFragments(fragmentStack, paths);
}
finally
{
if (paths != null)
if (fragmentsCollection.Count > 1)
{
Marshal.ReleaseComObject(paths);
// Shared geometry - extract base geometry once and return instance reference
result = ProcessSharedGeometry(paths.Value, fragmentStack);
}
else
{
// Single instance geometry - process normally with transforms
foreach (InwOaPath path in paths.Value)
{
CollectFragments(path, fragmentStack);
}
result = ProcessFragments(fragmentStack, paths.Value, true);
}
}
}
finally
{
if (comSelection != null)
else
{
Marshal.ReleaseComObject(comSelection);
result = [];
}
// Cache the result for future use
if (result.Count > 0)
{
_geometryCache.TryAdd(itemId, result);
}
return result;
}
catch (COMException ex)
{
_logger.LogError(ex, "COM exception converting geometry for ModelItem {ItemId}", itemId);
return [];
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "Invalid operation converting geometry for ModelItem {ItemId}", itemId);
return [];
}
}
private static void CollectFragments(InwOaPath path, Stack<InwOaFragment3> fragmentStack)
{
var fragments = path.Fragments();
foreach (var fragment in fragments.OfType<InwOaFragment3>())
using var fragments = new ComScope<InwNodeFragsColl>(path.Fragments());
foreach (var fragment in fragments.Value.OfType<InwOaFragment3>())
{
if (fragment.path?.ArrayData is not Array pathData1 || path.ArrayData is not Array pathData2)
{
continue;
}
var pathArray1 = pathData1.ToArray<int>();
var pathArray2 = pathData2.ToArray<int>();
if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2))
if (ValidateFragmentPath(fragment, path))
{
fragmentStack.Push(fragment);
}
}
}
private List<Base> ProcessFragments(Stack<InwOaFragment3> fragmentStack, InwSelectionPathsColl paths)
private List<Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
{
// Generate ID from fragment data for shared geometry
var fragmentId = GenerateFragmentId(paths);
if (string.IsNullOrEmpty(fragmentId))
{
// Fallback to normal processing if we can't generate ID
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths, true);
}
// Check if shared geometry already exists in store
if (_instanceStoreManager.ContainsSharedGeometry(fragmentId))
{
// Return instance reference to existing geometry
return CreateInstanceReference(fragmentId, paths);
}
// Extract untransformed base geometry
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
var baseGeometry = ExtractUntransformedGeometry(fragmentStack);
if (baseGeometry == null)
{
return ProcessFragments(fragmentStack, paths);
}
// Store both the geometry definition and create the instance definition proxy
if (!_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometry))
{
return ProcessFragments(fragmentStack, paths);
}
// Return instance reference to the newly stored geometry
return CreateInstanceReference(fragmentId, paths);
// Fallback to normal processing if store failed
}
private List<Base> ProcessFragments(
Stack<InwOaFragment3> fragmentStack,
InwSelectionPathsColl paths,
bool isSingleObject = false
)
{
var callbackListeners = new List<PrimitiveProcessor>();
@@ -111,41 +226,52 @@ public class GeometryToSpeckleConverter
{
var processor = new PrimitiveProcessor(_isUpright);
using var pathFragments = new ComScope<InwNodeFragsColl>(path.Fragments());
var fragmentCount = pathFragments.Value.Count;
foreach (var fragment in fragmentStack)
{
if (!ValidateFragmentPath(fragment, path))
try
{
continue;
}
var matrix = fragment.GetLocalToWorldMatrix();
var transform = matrix as InwLTransform3f3;
if (transform?.Matrix is not Array matrixArray)
{
continue;
}
var matrix = fragment.GetLocalToWorldMatrix();
var transform = matrix as InwLTransform3f3;
if (transform?.Matrix is not Array matrixArray)
double[] makeNoChange = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
if (isSingleObject || fragmentCount == 1)
{
// Apply coordinate system transformation
processor.LocalToWorldTransformation = transformMatrix;
}
else
{
// For multiple objects, process geometry without transforms
processor.LocalToWorldTransformation = makeNoChange;
}
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
catch (COMException ex)
{
continue;
_logger.LogWarning(ex, "COM exception processing fragment, skipping");
}
processor.LocalToWorldTransformation = ConvertArrayToDouble(matrixArray);
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
callbackListeners.Add(processor);
}
var baseGeometries = ProcessGeometries(callbackListeners);
return baseGeometries;
return ProcessGeometries(callbackListeners);
}
private static bool ValidateFragmentPath(InwOaFragment3 fragment, InwOaPath path)
{
if (fragment.path?.ArrayData is not Array fragmentPathData || path.ArrayData is not Array pathData)
{
return false;
}
return IsSameFragmentPath(fragmentPathData, pathData);
}
private static bool ValidateFragmentPath(InwOaFragment3 fragment, InwOaPath path) =>
fragment.path?.ArrayData is Array fragmentPathData
&& path.ArrayData is Array pathData
&& IsSameFragmentPath(fragmentPathData, pathData);
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
{
@@ -204,9 +330,8 @@ public class GeometryToSpeckleConverter
}
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines) =>
(
from line in lines
select new Line
lines
.Select(line => new Line
{
start = new Point(
(line.Start.X + _transformVector.X) * SCALE,
@@ -221,10 +346,395 @@ public class GeometryToSpeckleConverter
_settings.Derived.SpeckleUnits
),
units = _settings.Derived.SpeckleUnits
}
).ToList();
})
.ToList();
private static double[]? ConvertArrayToDouble(Array arr)
/// <summary>
/// Generates an idempotent ID from fragment path data for shared geometry.
/// Uses the path.Fragments() collection to create a reproducible hash.
/// </summary>
public string GenerateFragmentId(InwSelectionPathsColl paths)
{
try
{
if (paths.Count == 0)
{
return string.Empty;
}
// Generate a fast hash code for cache lookup
var pathsHashCode = GenerateFastPathsHashCode(paths);
// Check cache first
if (_fragmentIdCache.TryGetValue(pathsHashCode, out var cachedId))
{
return cachedId;
}
var fragmentHashes = new List<string>();
foreach (InwOaPath path in paths)
{
var fragments = path.Fragments();
var fragmentIndex = 0;
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
{
if (fragment.path?.ArrayData is not Array pathData)
{
fragmentIndex++;
continue;
}
if (pathData.Length == 0)
{
fragmentIndex++;
continue;
}
try
{
// Check array rank first - COM arrays might be multidimensional
if (pathData.Rank != 1)
{
// Try simple enumeration fallback
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 InvalidCastException or OverflowException or FormatException)
{
// Skip invalid array values
}
}
var fragmentHash = string.Join("_", pathInts);
fragmentHashes.Add(fragmentHash);
}
catch (Exception ex) when (ex is InvalidCastException or IndexOutOfRangeException or RankException)
{
// Try simple enumeration as fallback
var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHash))
{
fragmentHashes.Add(fragmentHash);
}
fragmentIndex++;
continue;
}
fragmentIndex++;
}
}
string fragmentId;
if (fragmentHashes.Count > 0)
{
// Sort to ensure consistent ordering
fragmentHashes.Sort();
var rawData = string.Join("__", fragmentHashes);
fragmentId = HashRawData(rawData);
}
else
{
fragmentId = string.Empty;
}
// Cache the result for future use
if (!string.IsNullOrEmpty(fragmentId))
{
_fragmentIdCache.TryAdd(pathsHashCode, fragmentId);
}
return fragmentId;
}
catch (Exception ex)
when (ex
is InvalidCastException
or IndexOutOfRangeException
or OverflowException
or ArgumentException
or COMException
)
{
_logger.LogWarning(ex, "Failed to generate fragment ID due to {ExceptionType}", ex.GetType().Name);
return string.Empty;
}
}
/// <summary>
/// Simple array enumeration fallback when bounds access fails.
/// Tries to enumerate array by simple sequential access.
/// </summary>
private string TrySimpleArrayEnumeration(Array pathData, int fragmentIndex)
{
try
{
var values = new List<string>();
var maxAttempts = Math.Min(pathData.Length, 20); // Limit attempts to avoid infinite loops
for (int i = 0; i < maxAttempts; i++)
{
try
{
var value = pathData.GetValue(i);
var convertedValue = System.Convert.ToInt32(value);
values.Add(convertedValue.ToString());
_logger.LogDebug("Fragment {FragmentIndex} simple enum[{Index}] = {Value}", fragmentIndex, i, convertedValue);
}
catch (IndexOutOfRangeException)
{
// Hit the end of valid indices
_logger.LogDebug("Fragment {FragmentIndex} reached end of array at index {Index}", fragmentIndex, i);
break;
}
catch (Exception ex)
when (ex is InvalidCastException or OverflowException or FormatException or ArgumentException)
{
_logger.LogWarning(ex, "Fragment {FragmentIndex} failed to convert value at index {Index}", fragmentIndex, i);
}
}
if (values.Count <= 0)
{
return string.Empty;
}
var hash = string.Join("_", values);
return hash;
}
catch (Exception ex) when (ex is COMException or InvalidCastException or ArgumentException)
{
_logger.LogWarning(ex, "Simple enumeration failed for fragment {FragmentIndex}", fragmentIndex);
return string.Empty;
}
}
/// <summary>
/// Generates a fast hash code for paths collection for caching purposes
/// </summary>
private static int GenerateFastPathsHashCode(InwSelectionPathsColl paths)
{
unchecked
{
int hash = 17;
hash = hash * 23 + paths.Count;
var processed = 0;
foreach (InwOaPath path in paths)
{
if (path.ArrayData is Array { Length: > 0 } pathData)
{
// Sample first few elements for performance
var sampleSize = Math.Min(pathData.Length, 4);
for (int i = 0; i < sampleSize; i++)
{
hash = hash * 23 + (pathData.GetValue(i)?.GetHashCode() ?? 0);
}
hash = hash * 23 + pathData.Length;
}
// Limit processing for performance
if (++processed >= 8)
{
break;
}
}
return hash;
}
}
/// <summary>
/// Creates a fast hash of the raw fragment data using .NET's HashCode struct.
/// For performance, we use HashCode instead of SHA256 for fragment IDs.
/// </summary>
/// <returns>Hash as hex string</returns>
private static string HashRawData(string rawData)
{
var hashCode = rawData.GetHashCode();
return hashCode.ToString("X8");
}
/// <summary>
/// Extracts untransformed base geometry from fragments.
/// This geometry will be stored once and referenced by instances.
/// </summary>
private Base? ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
{
var processor = new PrimitiveProcessor(_isUpright);
// Process fragments without transforms to get base geometry
foreach (var fragment in fragmentStack)
{
// Use identity transform to get untransformed geometry
double[] identityTransform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
processor.LocalToWorldTransformation = identityTransform;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
// Create mesh from untransformed geometry
return processor.Triangles.Count > 0 ? CreateMesh(processor.Triangles) : null;
}
/// <summary>
/// Creates an instance reference to shared geometry stored in the InstanceStoreManager.
/// This is returned instead of full geometry for shared instances.
/// </summary>
private List<Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
{
var transform = ExtractInstanceTransform(paths);
var instanceReference = new InstanceProxy
{
definitionId = $"def_{fragmentId}",
transform = transform,
units = _settings.Derived.SpeckleUnits,
maxDepth = 0,
applicationId = Guid.NewGuid().ToString()
};
return [instanceReference];
}
/// <summary>
/// Extracts the transform matrix from the first path's fragments for instance placement.
/// </summary>
private Matrix4x4 ExtractInstanceTransform(InwSelectionPathsColl paths)
{
try
{
if (paths.Count == 0)
{
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
var firstPath = paths.Cast<InwOaPath>().First();
using var fragments = new ComScope<InwNodeFragsColl>(firstPath.Fragments());
if (fragments.Value.Count == 0)
{
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
var fragmentStack = new Stack<InwOaFragment3>();
// Get the first fragment's transform matrix
foreach (var frag in fragments.Value.OfType<InwOaFragment3>())
{
try
{
if (frag.path?.ArrayData is not Array pathData1 || firstPath.ArrayData is not Array pathData2)
{
continue;
}
// Use IsSameFragmentPath for consistency and performance
if (IsSameFragmentPath(pathData1, pathData2))
{
fragmentStack.Push(frag);
}
}
catch (COMException ex)
{
_logger.LogWarning(ex, "COM exception accessing fragment path data, skipping fragment");
}
}
if (fragmentStack.Count == 0)
{
_logger.LogWarning("No valid fragments found for transform extraction");
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
var fragment = fragmentStack.First();
var matrix = fragment.GetLocalToWorldMatrix();
if (matrix is InwLTransform3f3 { Matrix: Array matrixArray })
{
var transformArray = ConvertArrayToDouble(matrixArray);
// Apply coordinate system transformation
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);
}
}
catch (Exception ex)
when (ex
is COMException
or InvalidCastException
or IndexOutOfRangeException
or ArgumentException
or NullReferenceException
)
{
_logger.LogWarning(
ex,
"Failed to extract instance transform ({ExceptionType}) - returning identity matrix",
ex.GetType().Name
);
}
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
private double[] ApplyCoordinateTransform(double[] matrixArray)
{
// Apply scale and coordinate transformation
var result = new double[16];
Array.Copy(matrixArray, result, 16);
// Apply translation transformation
result[12] = (result[12] + _transformVector.X) * SCALE;
result[13] = (result[13] + _transformVector.Y) * SCALE;
result[14] = (result[14] + _transformVector.Z) * SCALE;
return result;
}
private static double[] ConvertArrayToDouble(Array arr)
{
if (arr.Rank != 1)
{
@@ -241,5 +751,10 @@ public class GeometryToSpeckleConverter
}
private static bool IsSameFragmentPath(Array a1, Array a2) =>
a1.Length == a2.Length && a1.Cast<int>().SequenceEqual(a2.Cast<int>());
a1.Length == a2.Length
&& (
a1.Length > 4
? a1.Cast<object>().SequenceEqual(a2.Cast<object>())
: !a1.Cast<object>().Where((_, i) => !Equals(a1.GetValue(i), a2.GetValue(i))).Any()
);
}
@@ -1,4 +1,4 @@
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.ToSpeckle.PropertyHandlers;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;