Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79a6c42cc7 | |||
| e2318df87a | |||
| 78d7814351 | |||
| 17a320ee53 | |||
| 396ef981ee | |||
| 763c413871 | |||
| 57a5b41ec1 | |||
| 8385532b96 | |||
| e109515852 | |||
| fdf2425ec6 | |||
| c067cf6f91 | |||
| 60e26d85c6 |
+4
@@ -16,6 +16,7 @@ using Speckle.Connectors.DUI.Models;
|
|||||||
using Speckle.Connectors.DUI.Models.Card.SendFilter;
|
using Speckle.Connectors.DUI.Models.Card.SendFilter;
|
||||||
using Speckle.Connectors.DUI.WebView;
|
using Speckle.Connectors.DUI.WebView;
|
||||||
using Speckle.Converter.Navisworks.Settings;
|
using Speckle.Converter.Navisworks.Settings;
|
||||||
|
using Speckle.Converter.Navisworks.Services;
|
||||||
using Speckle.Converters.Common;
|
using Speckle.Converters.Common;
|
||||||
using Speckle.Sdk.Models.GraphTraversal;
|
using Speckle.Sdk.Models.GraphTraversal;
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ public static class NavisworksConnectorServiceRegistration
|
|||||||
serviceCollection.AddScoped<NavisworksMaterialUnpacker>();
|
serviceCollection.AddScoped<NavisworksMaterialUnpacker>();
|
||||||
serviceCollection.AddScoped<NavisworksColorUnpacker>();
|
serviceCollection.AddScoped<NavisworksColorUnpacker>();
|
||||||
|
|
||||||
|
// Register dual shared geometry stores for instancing pattern
|
||||||
|
serviceCollection.AddScoped<InstanceStoreManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IAppIdleManager, NavisworksIdleManager>();
|
serviceCollection.AddSingleton<IAppIdleManager, NavisworksIdleManager>();
|
||||||
|
|
||||||
// Sending operations
|
// Sending operations
|
||||||
|
|||||||
+20
-3
@@ -1,7 +1,10 @@
|
|||||||
|
using Autodesk.Navisworks.Api.ComApi;
|
||||||
|
using Autodesk.Navisworks.Api.Interop.ComApi;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Speckle.Connector.Navisworks.Services;
|
using Speckle.Connector.Navisworks.Services;
|
||||||
using Speckle.Converter.Navisworks.Helpers;
|
using Speckle.Converter.Navisworks.Helpers;
|
||||||
using Speckle.Converter.Navisworks.Settings;
|
using Speckle.Converter.Navisworks.Settings;
|
||||||
|
using Speckle.Converter.Navisworks.ToSpeckle;
|
||||||
using Speckle.Converters.Common;
|
using Speckle.Converters.Common;
|
||||||
using Speckle.Objects.Other;
|
using Speckle.Objects.Other;
|
||||||
using Speckle.Sdk;
|
using Speckle.Sdk;
|
||||||
@@ -11,7 +14,8 @@ namespace Speckle.Connector.Navisworks.HostApp;
|
|||||||
public class NavisworksMaterialUnpacker(
|
public class NavisworksMaterialUnpacker(
|
||||||
ILogger<NavisworksMaterialUnpacker> logger,
|
ILogger<NavisworksMaterialUnpacker> logger,
|
||||||
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
|
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
|
||||||
IElementSelectionService selectionService
|
IElementSelectionService selectionService,
|
||||||
|
GeometryToSpeckleConverter converter
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Helper function to select a property based on the representation mode
|
// Helper function to select a property based on the representation mode
|
||||||
@@ -64,6 +68,19 @@ public class NavisworksMaterialUnpacker(
|
|||||||
|
|
||||||
var navisworksObjectId = selectionService.GetModelItemPath(navisworksObject);
|
var navisworksObjectId = selectionService.GetModelItemPath(navisworksObject);
|
||||||
var finalId = mergedIds.TryGetValue(navisworksObjectId, out var mergedId) ? mergedId : navisworksObjectId;
|
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 geometry = navisworksObject.Geometry;
|
||||||
var mode = converterSettings.Current.User.VisualRepresentationMode;
|
var mode = converterSettings.Current.User.VisualRepresentationMode;
|
||||||
|
|
||||||
@@ -120,7 +137,7 @@ public class NavisworksMaterialUnpacker(
|
|||||||
|
|
||||||
if (renderMaterialProxies.TryGetValue(renderMaterialId.ToString(), out RenderMaterialProxy? value))
|
if (renderMaterialProxies.TryGetValue(renderMaterialId.ToString(), out RenderMaterialProxy? value))
|
||||||
{
|
{
|
||||||
value.objects.Add(finalId);
|
value.objects.Add(!string.IsNullOrEmpty(hashId) ? hashId : finalId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -132,7 +149,7 @@ public class NavisworksMaterialUnpacker(
|
|||||||
renderColor,
|
renderColor,
|
||||||
renderMaterialId
|
renderMaterialId
|
||||||
),
|
),
|
||||||
objects = [finalId]
|
objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-9
@@ -6,6 +6,7 @@ using Speckle.Connectors.Common.Caching;
|
|||||||
using Speckle.Connectors.Common.Conversion;
|
using Speckle.Connectors.Common.Conversion;
|
||||||
using Speckle.Connectors.Common.Operations;
|
using Speckle.Connectors.Common.Operations;
|
||||||
using Speckle.Converter.Navisworks.Helpers;
|
using Speckle.Converter.Navisworks.Helpers;
|
||||||
|
using Speckle.Converter.Navisworks.Services;
|
||||||
using Speckle.Converter.Navisworks.Settings;
|
using Speckle.Converter.Navisworks.Settings;
|
||||||
using Speckle.Converters.Common;
|
using Speckle.Converters.Common;
|
||||||
using Speckle.Objects.Data;
|
using Speckle.Objects.Data;
|
||||||
@@ -25,7 +26,8 @@ public class NavisworksRootObjectBuilder(
|
|||||||
ISdkActivityFactory activityFactory,
|
ISdkActivityFactory activityFactory,
|
||||||
NavisworksMaterialUnpacker materialUnpacker,
|
NavisworksMaterialUnpacker materialUnpacker,
|
||||||
NavisworksColorUnpacker colorUnpacker,
|
NavisworksColorUnpacker colorUnpacker,
|
||||||
IElementSelectionService elementSelectionService
|
IElementSelectionService elementSelectionService,
|
||||||
|
InstanceStoreManager instanceStoreManager
|
||||||
) : IRootObjectBuilder<NAV.ModelItem>
|
) : IRootObjectBuilder<NAV.ModelItem>
|
||||||
{
|
{
|
||||||
private bool SkipNodeMerging { get; set; }
|
private bool SkipNodeMerging { get; set; }
|
||||||
@@ -41,7 +43,7 @@ public class NavisworksRootObjectBuilder(
|
|||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// This is a temporary workaround to disable node merging for debugging purposes - false is default, true is for debugging
|
// This is a temporary workaround to disable node merging for debugging purposes - false is default, true is for debugging
|
||||||
SkipNodeMerging = false;
|
SkipNodeMerging = true;
|
||||||
#endif
|
#endif
|
||||||
using var activity = activityFactory.Start("Build");
|
using var activity = activityFactory.Start("Build");
|
||||||
|
|
||||||
@@ -50,22 +52,42 @@ public class NavisworksRootObjectBuilder(
|
|||||||
// 2. Initialize root collection
|
// 2. Initialize root collection
|
||||||
var rootCollection = InitializeRootCollection();
|
var rootCollection = InitializeRootCollection();
|
||||||
|
|
||||||
|
// InstanceStoreManager is scoped - starts fresh for each conversion session
|
||||||
|
|
||||||
// 3. Convert all model items and store results
|
// 3. Convert all model items and store results
|
||||||
var (convertedElements, conversionResults) = await ConvertModelItemsAsync(
|
(Dictionary<string, Base?> convertedElements, List<SendConversionResult> conversionResults) =
|
||||||
navisworksModelItems,
|
await ConvertModelItemsAsync(navisworksModelItems, projectId, onOperationProgressed, cancellationToken);
|
||||||
projectId,
|
|
||||||
onOperationProgressed,
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
ValidateConversionResults(conversionResults);
|
ValidateConversionResults(conversionResults);
|
||||||
|
|
||||||
var groupedNodes = SkipNodeMerging ? [] : GroupSiblingGeometryNodes(navisworksModelItems);
|
var groupedNodes = SkipNodeMerging ? [] : GroupSiblingGeometryNodes(navisworksModelItems);
|
||||||
var finalElements = BuildFinalElements(convertedElements, groupedNodes);
|
var finalElements = BuildFinalElements(convertedElements, groupedNodes);
|
||||||
|
List<Base> geometryDefinitions = instanceStoreManager.GetGeometryDefinitions();
|
||||||
|
|
||||||
await AddProxiesToCollection(rootCollection, navisworksModelItems, groupedNodes);
|
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);
|
return new RootObjectBuilderResult(rootCollection, conversionResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +310,24 @@ public class NavisworksRootObjectBuilder(
|
|||||||
rootCollection[ProxyKeys.COLOR] = colors;
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-32
@@ -1,38 +1,19 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Speckle.Sdk.Models;
|
||||||
using Speckle.Converter.Navisworks.Settings;
|
|
||||||
using Speckle.Converters.Common;
|
|
||||||
using Speckle.Sdk.Models;
|
|
||||||
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
|
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
|
||||||
|
|
||||||
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||||
|
|
||||||
public class DisplayValueExtractor
|
public class DisplayValueExtractor(GeometryToSpeckleConverter geometryConverter)
|
||||||
{
|
{
|
||||||
private readonly IConverterSettingsStore<NavisworksConversionSettings> _converterSettings;
|
internal List<Base> GetDisplayValue(NAV.ModelItem modelItem) =>
|
||||||
private readonly ILogger<DisplayValueExtractor> _logger;
|
modelItem == null
|
||||||
private readonly GeometryToSpeckleConverter _geometryConverter;
|
? throw new ArgumentNullException(nameof(modelItem))
|
||||||
|
: !modelItem.HasGeometry
|
||||||
public DisplayValueExtractor(
|
? ([])
|
||||||
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
|
: !IsElementVisible(modelItem)
|
||||||
ILogger<DisplayValueExtractor> logger
|
? []
|
||||||
)
|
:
|
||||||
{
|
// this can be meshes or the instance reference objects
|
||||||
_converterSettings = converterSettings;
|
// the un transformed objects stored in a separate collection
|
||||||
_logger = logger;
|
geometryConverter.Convert(modelItem);
|
||||||
_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -46,6 +46,19 @@ public static class NavisworksConverterServiceRegistration
|
|||||||
serviceCollection.AddScoped<DisplayValueExtractor>();
|
serviceCollection.AddScoped<DisplayValueExtractor>();
|
||||||
serviceCollection.AddScoped<GeometryToSpeckleConverter>();
|
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;
|
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<SomeComType>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-3
@@ -48,9 +48,7 @@ public static class PropertyHelpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default case for unsupported types
|
// Default case for unsupported types
|
||||||
return value.DataType == NAV.VariantDataType.None || value.DataType == NAV.VariantDataType.Point2D
|
return value.DataType is NAV.VariantDataType.None or NAV.VariantDataType.Point2D ? null : value.ToString();
|
||||||
? null
|
|
||||||
: value.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+154
@@ -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}")
|
||||||
|
;
|
||||||
|
}
|
||||||
+92
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-31
@@ -9,35 +9,38 @@
|
|||||||
<Import_RootNamespace>Speckle.Converters.NavisworksShared</Import_RootNamespace>
|
<Import_RootNamespace>Speckle.Converters.NavisworksShared</Import_RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ComScope.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)PathConstants.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)PathConstants.cs"/>
|
||||||
<Folder Include="$(MSBuildThisFileDirectory)Models\" />
|
<Compile Include="$(MSBuildThisFileDirectory)Services\SharedGeometryStores.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Services\InstanceStoreManager.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs" />
|
<Folder Include="$(MSBuildThisFileDirectory)Models\"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettingsFactory.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\NavisworksRootToSpeckleConverter.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoundingBoxToSpeckleRawConverter.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs"/>
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\ModelItemTopLevelConverterToSpeckle.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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+587
-72
@@ -1,11 +1,15 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using Autodesk.Navisworks.Api.Interop.ComApi;
|
using Autodesk.Navisworks.Api.Interop.ComApi;
|
||||||
using Speckle.Converter.Navisworks.Extensions;
|
using Microsoft.Extensions.Logging;
|
||||||
using Speckle.Converter.Navisworks.Geometry;
|
using Speckle.Converter.Navisworks.Geometry;
|
||||||
using Speckle.Converter.Navisworks.Helpers;
|
using Speckle.Converter.Navisworks.Helpers;
|
||||||
|
using Speckle.Converter.Navisworks.Services;
|
||||||
using Speckle.Converter.Navisworks.Settings;
|
using Speckle.Converter.Navisworks.Settings;
|
||||||
|
using Speckle.DoubleNumerics;
|
||||||
using Speckle.Objects.Geometry;
|
using Speckle.Objects.Geometry;
|
||||||
using Speckle.Sdk.Models;
|
using Speckle.Sdk.Models;
|
||||||
|
using Speckle.Sdk.Models.Instances;
|
||||||
using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
|
using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
|
||||||
|
|
||||||
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||||
@@ -21,23 +25,59 @@ namespace Speckle.Converter.Navisworks.ToSpeckle;
|
|||||||
/// 3. Process each InwOaFragment3 to generate primitives
|
/// 3. Process each InwOaFragment3 to generate primitives
|
||||||
/// 4. Convert those primitives to Speckle geometry with appropriate transforms
|
/// 4. Convert those primitives to Speckle geometry with appropriate transforms
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GeometryToSpeckleConverter
|
public class GeometryToSpeckleConverter(
|
||||||
|
NavisworksConversionSettings settings,
|
||||||
|
InstanceStoreManager instanceStoreManager,
|
||||||
|
ILogger<GeometryToSpeckleConverter> logger
|
||||||
|
)
|
||||||
{
|
{
|
||||||
private readonly NavisworksConversionSettings _settings;
|
private readonly NavisworksConversionSettings _settings =
|
||||||
private readonly bool _isUpright;
|
settings ?? throw new ArgumentNullException(nameof(settings));
|
||||||
private readonly SafeVector _transformVector;
|
|
||||||
|
private readonly bool _isUpright = settings.Derived.IsUpright;
|
||||||
|
private readonly SafeVector _transformVector = settings.Derived.TransformVector;
|
||||||
private const double SCALE = 1.0; // Default scale factor
|
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));
|
_fragmentIdCache.Clear();
|
||||||
_isUpright = settings.Derived.IsUpright;
|
_geometryCache.Clear();
|
||||||
_transformVector = settings.Derived.TransformVector;
|
}
|
||||||
|
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// Converts a ModelItem's geometry to Speckle display geometry by accessing the underlying COM objects.
|
/// 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>
|
/// </summary>
|
||||||
internal List<Base> Convert(NAV.ModelItem modelItem)
|
internal List<Base> Convert(NAV.ModelItem modelItem)
|
||||||
{
|
{
|
||||||
@@ -51,59 +91,134 @@ public class GeometryToSpeckleConverter
|
|||||||
return [];
|
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
|
try
|
||||||
{
|
{
|
||||||
var fragmentStack = new Stack<InwOaFragment3>();
|
// Check if this geometry is shared across multiple instances
|
||||||
var paths = comSelection.Paths();
|
List<Base> result;
|
||||||
try
|
if (paths.Value.Count > 0)
|
||||||
{
|
{
|
||||||
// Populate fragment stack with all fragments
|
var firstPath = paths.Value.Cast<InwOaPath>().First();
|
||||||
foreach (InwOaPath path in paths)
|
var fragmentsCollection = firstPath.Fragments();
|
||||||
{
|
|
||||||
CollectFragments(path, fragmentStack);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProcessFragments(fragmentStack, paths);
|
if (fragmentsCollection.Count > 1)
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (paths != null)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (comSelection != null)
|
|
||||||
{
|
{
|
||||||
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)
|
private static void CollectFragments(InwOaPath path, Stack<InwOaFragment3> fragmentStack)
|
||||||
{
|
{
|
||||||
var fragments = path.Fragments();
|
using var fragments = new ComScope<InwNodeFragsColl>(path.Fragments());
|
||||||
foreach (var fragment in fragments.OfType<InwOaFragment3>())
|
|
||||||
|
foreach (var fragment in fragments.Value.OfType<InwOaFragment3>())
|
||||||
{
|
{
|
||||||
if (fragment.path?.ArrayData is not Array pathData1 || path.ArrayData is not Array pathData2)
|
if (ValidateFragmentPath(fragment, path))
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathArray1 = pathData1.ToArray<int>();
|
|
||||||
var pathArray2 = pathData2.ToArray<int>();
|
|
||||||
|
|
||||||
if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2))
|
|
||||||
{
|
{
|
||||||
fragmentStack.Push(fragment);
|
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>();
|
var callbackListeners = new List<PrimitiveProcessor>();
|
||||||
|
|
||||||
@@ -111,41 +226,52 @@ public class GeometryToSpeckleConverter
|
|||||||
{
|
{
|
||||||
var processor = new PrimitiveProcessor(_isUpright);
|
var processor = new PrimitiveProcessor(_isUpright);
|
||||||
|
|
||||||
|
using var pathFragments = new ComScope<InwNodeFragsColl>(path.Fragments());
|
||||||
|
var fragmentCount = pathFragments.Value.Count;
|
||||||
|
|
||||||
foreach (var fragment in fragmentStack)
|
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();
|
double[] makeNoChange = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
||||||
var transform = matrix as InwLTransform3f3;
|
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
|
||||||
if (transform?.Matrix is not Array 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);
|
callbackListeners.Add(processor);
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseGeometries = ProcessGeometries(callbackListeners);
|
return ProcessGeometries(callbackListeners);
|
||||||
|
|
||||||
return baseGeometries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ValidateFragmentPath(InwOaFragment3 fragment, InwOaPath path)
|
private static bool ValidateFragmentPath(InwOaFragment3 fragment, InwOaPath path) =>
|
||||||
{
|
fragment.path?.ArrayData is Array fragmentPathData
|
||||||
if (fragment.path?.ArrayData is not Array fragmentPathData || path.ArrayData is not Array pathData)
|
&& path.ArrayData is Array pathData
|
||||||
{
|
&& IsSameFragmentPath(fragmentPathData, pathData);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return IsSameFragmentPath(fragmentPathData, pathData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
|
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
|
||||||
{
|
{
|
||||||
@@ -204,9 +330,8 @@ public class GeometryToSpeckleConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines) =>
|
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines) =>
|
||||||
(
|
lines
|
||||||
from line in lines
|
.Select(line => new Line
|
||||||
select new Line
|
|
||||||
{
|
{
|
||||||
start = new Point(
|
start = new Point(
|
||||||
(line.Start.X + _transformVector.X) * SCALE,
|
(line.Start.X + _transformVector.X) * SCALE,
|
||||||
@@ -221,10 +346,395 @@ public class GeometryToSpeckleConverter
|
|||||||
_settings.Derived.SpeckleUnits
|
_settings.Derived.SpeckleUnits
|
||||||
),
|
),
|
||||||
units = _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)
|
if (arr.Rank != 1)
|
||||||
{
|
{
|
||||||
@@ -241,5 +751,10 @@ public class GeometryToSpeckleConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsSameFragmentPath(Array a1, Array a2) =>
|
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
-1
@@ -1,4 +1,4 @@
|
|||||||
using Speckle.Converter.Navisworks.Settings;
|
using Speckle.Converter.Navisworks.Settings;
|
||||||
using Speckle.Converter.Navisworks.ToSpeckle.PropertyHandlers;
|
using Speckle.Converter.Navisworks.ToSpeckle.PropertyHandlers;
|
||||||
using Speckle.Converters.Common;
|
using Speckle.Converters.Common;
|
||||||
using Speckle.Converters.Common.Objects;
|
using Speckle.Converters.Common.Objects;
|
||||||
|
|||||||
Reference in New Issue
Block a user