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