Compare commits

...

37 Commits

Author SHA1 Message Date
Jedd Morgan c0b38f0e12 feat(revit)!: Enable config for disabling API listening events (#1255)
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
* Expose options to disable revit listening events

* fix mistake

* Disable change tracking for revit when document listener is disabled

* Check first
2026-01-21 19:10:34 +00:00
Jedd Morgan 4a88380fd2 Add logging for revitildlemanager reentry misuse (#1253) 2026-01-21 17:52:06 +00:00
Jedd Morgan 637ffbfc54 Merge pull request #1250 from specklesystems/jedd/cnx-2869-revit-crashes-performance-issues-caused-by-speckle-connector
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
fix(revit): Ensure top level exception handler will catch RevitIdleManager calls
2026-01-19 17:12:23 +00:00
Björn Steinhagen c41c57544a Merge pull request #1246 from specklesystems/dev
dev -> main
2026-01-16 18:19:25 +02:00
Björn Steinhagen 6687383ce4 Merge pull request #1245 from specklesystems/main-backmerge
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
main backmerge
2026-01-16 18:13:52 +02:00
Björn f08d52e080 Merge branch 'dev' into main-backmerge 2026-01-16 18:08:34 +02:00
Dogukan Karatas 4dcf9910a5 fix (revit): handle receive for DataObjects with proxified displayValue (#1239)
* dataobject registery

* remove registery

* added helper function

---------

Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2026-01-16 15:08:43 +00:00
Jonathon Broughton 9a61ded43e removes parameters from NavisworksRootObjectBuilder (#1244)
Converts properties for skip node merging and disable grouping
into settable values and removes their initialisation parameters.
This simplifies the class structure and improves flexibility.
2026-01-16 18:04:30 +03:00
Oğuzhan Koral 5acb0b80ab Merge pull request #1243 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Update dev into main
2026-01-16 15:01:22 +03:00
Oğuzhan Koral ba41ceca2f fix(revit): revit crashes and performance issues (#1242)
* Subscribe doc change events only if we have sender model card

* Bring old but GOLD idle manager

* Add tasks back

* fix format

* Simplify event invoke from document model store for cards
2026-01-16 14:56:23 +03:00
Björn Steinhagen 8474088e5b Merge pull request #1240 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
dev -> main
2026-01-15 18:20:53 +02:00
Jedd Morgan fe042d7a1c Merge pull request #1241 from specklesystems/main-backmerge
Main backmerge
2026-01-15 16:16:13 +00:00
Jedd Morgan d681c63a05 Merge branch 'dev' into main-backmerge 2026-01-15 16:13:30 +00:00
Jonathon Broughton bdc0e2b5bd 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>
2026-01-15 13:42:52 +03:00
Dogukan Karatas 6dc1726536 fix (revit): webview2 lazy initialization to avoid pyRevit conflict (#1235)
* lazy init webview

* simplified init
2026-01-15 13:34:02 +03:00
Björn Steinhagen 46198934ec feat(grasshopper): enables application id mutations on passthrough components (#1236)
* feat(grasshopper): enables user application id setting

* chore: prop exposure
2026-01-14 12:10:39 +03:00
kekesidavid b4191d1d65 get host id for curtain panels and mullions (#1234)
Co-authored-by: Mucahit Bilal GOKER <51519350+bimgeek@users.noreply.github.com>
2026-01-13 07:18:01 +03:00
Björn Steinhagen f007e28565 feat(grasshopper): add CollectionsByName component with nested collection support (#1233)
* feat(grasshopper): adds easier collection creation foundation

* feat(grasshopper): adds nesting support

---------

Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2026-01-12 16:37:02 +02:00
Björn Steinhagen d05667dac8 fix(revit): curve double-transform when publishing with reference points (#1231)
* fix(revit): adds logic to not double-transform curves with reference point

* fix(revit): updates rebar logic in light of changes

* chore(docs): updates comments

---------

Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2026-01-12 15:21:12 +03:00
Dogukan Karatas c922976bcd fix (rhino): handle invalid layer names during receive (#1232)
* fix whitepaces

* comment added
2026-01-09 17:47:36 +03:00
Björn Steinhagen c3aa44dfc2 feat(grasshopper): hides application id and speckle id on filter objects component (#1230) 2026-01-08 11:18:24 +02:00
Björn Steinhagen e1a64189c8 feat(grasshopper): adds version id output to publish components 2026-01-06 12:53:11 +02:00
Björn Steinhagen b0e0669cab fix(revit): bypass plane creation for geometry beyond distance limits 2026-01-06 10:57:55 +02:00
Mucahit Bilal GOKER b2a14e055c feat(revit): add parentId param to nested elements (#1227)
* add parent id to nested elements

* chore: formatting

* change from elementId to applicationId

---------

Co-authored-by: Björn Steinhagen <steinhagen.bjoern@gmail.com>
2026-01-05 16:00:11 +03:00
Mucahit Bilal GOKER 74f4525ff2 exclude invalid categories (#1226) 2026-01-05 12:59:04 +03:00
Björn Steinhagen bb57b31ae4 fix(grasshopper): handle mixed InstanceProxy and primitive geometry in DataObject displayValue (#1225) 2025-12-24 13:00:27 +01:00
kekesidavid d33a6ca358 fix(rhino): force initialization RhinoDocumentStore (#1224)
* force initialization RhinoDocumentStore

* commeng changed
2025-12-23 11:11:07 +01:00
Björn Steinhagen a8cc4cebc7 Merge pull request #1222 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
dev -> main
2025-12-22 21:02:10 +07:00
Björn Steinhagen 678ba417d2 Merge pull request #1220 from specklesystems/main-dev
main-dev -> dev
2025-12-22 20:53:23 +07:00
Björn Steinhagen bc9fbe3cf7 Merge remote-tracking branch 'origin/dev' into main-dev 2025-12-22 14:47:34 +01:00
Björn Steinhagen b09f085f07 fix(revit): mep geometry view-driven (#1218) 2025-12-22 14:23:57 +01:00
Björn Steinhagen 539ae1fc78 fix(revit): correct transforms for modified elements and nested instances (#1217)
* fix(revit): correct element transforms and instance proxies

* chore(revit): docs
2025-12-22 13:41:28 +01:00
Jedd Morgan cc47dfaac6 Merge pull request #1216 from specklesystems/jrm/rhino-importer-slug
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
fix(rhino-importer): report correct slug to ingestion
2025-12-16 14:35:57 +00:00
Jedd Morgan 691235a7ac Merge pull request #1215 from specklesystems/main
Main -> Dev backmerge
2025-12-15 16:31:39 +00:00
dependabot[bot] deff607bcb chore(deps): bump actions/cache from 4 to 5 (#1212)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 14:43:54 +00:00
dependabot[bot] cfb8aba55f chore(deps): bump actions/upload-artifact from 5 to 6 (#1213)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 14:38:58 +00:00
Jedd Morgan 4bcc0d83a9 Use payload filename (#1214)
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
2025-12-15 14:25:29 +00:00
90 changed files with 3474 additions and 1902 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
dotnet-version: 8.0.4xx # Align with global.json (including roll forward rules)
- name: Cache Nuget
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
dotnet-version: 8.0.4xx # Align with global.json (including roll forward rules)
- name: Cache Nuget
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
@@ -35,7 +35,7 @@ jobs:
run: ./build.ps1 zip
- name: ⬆️ Upload artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: output-${{ env.SEMVER }}
path: output/*.*
@@ -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,6 +1,6 @@
using Speckle.Connector.Navisworks.Services;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Converter.Navisworks.Services;
namespace Speckle.Connector.Navisworks.Bindings;
@@ -12,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))
@@ -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
>();
}
}
@@ -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
}
@@ -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);
}
}
}
@@ -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;
@@ -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,
@@ -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;
@@ -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:
@@ -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);
}
}
@@ -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)
{
@@ -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];
}
@@ -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,15 @@ public class NavisworksRootObjectBuilder(
ISdkActivityFactory activityFactory,
NavisworksMaterialUnpacker materialUnpacker,
NavisworksColorUnpacker colorUnpacker,
Speckle.Converter.Navisworks.Constants.Registers.IInstanceFragmentRegistry instanceRegistry,
IElementSelectionService elementSelectionService,
IUiUnitsCache uiUnitsCache,
InstanceStoreManager instanceStoreManager
IUiUnitsCache uiUnitsCache
) : IRootObjectBuilder<NAV.ModelItem>
{
#pragma warning disable CA1823
#pragma warning restore CA1823
private bool SkipNodeMerging { get; set; }
internal NavisworksConversionSettings GetCurrentSettings() => converterSettings.Current;
private bool DisableGroupingForInstanceTesting { get; set; }
public async Task<RootObjectBuilderResult> Build(
IReadOnlyList<NAV.ModelItem> navisworksModelItems,
@@ -43,14 +46,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 +61,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 +117,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 +161,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 +188,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 +204,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 +259,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 +322,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,
@@ -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);
}
}
@@ -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
@@ -3,17 +3,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:dui="clr-namespace:Speckle.Connectors.DUI;assembly=Speckle.Connectors.DUI"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<wv2:CoreWebView2CreationProperties x:Key="EvergreenWebView2CreationProperties" UserDataFolder="C:\temp" />
</UserControl.Resources>
<DockPanel>
<wv2:WebView2
CreationProperties="{StaticResource EvergreenWebView2CreationProperties}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Name="Browser" Grid.Row="0" Source="{x:Static dui:Url.Netlify}" />
</DockPanel>
<!-- WebView2 is created lazily -->
<Border Name="BrowserContainer" Background="White">
<TextBlock Name="LoadingText"
Text="Loading Speckle..."
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="14"
Foreground="Gray" />
</Border>
</UserControl>
@@ -2,6 +2,8 @@ using System.Windows.Controls;
using System.Windows.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using Speckle.Connectors.DUI;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.Revit.Plugin;
@@ -12,6 +14,10 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
{
private readonly IServiceProvider _serviceProvider;
private readonly IRevitTask _revitTask;
#pragma warning disable CA2213
private WebView2? _browser;
#pragma warning restore CA2213
private bool _isInitializing;
public RevitControlWebView(IServiceProvider serviceProvider, IRevitTask revitTask)
{
@@ -19,35 +25,61 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
_revitTask = revitTask;
InitializeComponent();
Browser.CoreWebView2InitializationCompleted += (sender, args) =>
// Delay WebView2 creation until the panel is actually visible
// This avoids conflicts with other plugins (like pyRevit) during startup
IsVisibleChanged += OnIsVisibleChanged;
}
private void OnIsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is true && _browser == null && !_isInitializing)
{
_isInitializing = true;
Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, CreateWebView2);
}
}
private void CreateWebView2()
{
_browser = new WebView2
{
CreationProperties = new CoreWebView2CreationProperties { UserDataFolder = "C:\\temp" },
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
VerticalAlignment = System.Windows.VerticalAlignment.Stretch,
Source = Url.Netlify
};
_browser.CoreWebView2InitializationCompleted += (sender, args) =>
_serviceProvider
.GetRequiredService<ITopLevelExceptionHandler>()
.CatchUnhandled(() => OnInitialized(sender, args));
BrowserContainer.Child = _browser;
}
public bool IsBrowserInitialized => Browser.IsInitialized;
public bool IsBrowserInitialized => _browser?.IsInitialized ?? false;
public object BrowserElement => Browser;
public object BrowserElement => _browser!;
public void ExecuteScript(string script)
{
if (!Browser.IsInitialized)
if (_browser == null || !_browser.IsInitialized)
{
throw new InvalidOperationException("Failed to execute script, Webview2 is not initialized yet.");
}
_revitTask.Run(() => Browser.ExecuteScriptAsync(script));
_revitTask.Run(() => _browser.ExecuteScriptAsync(script));
}
public void SendProgress(string script)
{
if (!Browser.IsInitialized)
if (_browser == null || !_browser.IsInitialized)
{
throw new InvalidOperationException("Failed to execute script, Webview2 is not initialized yet.");
}
//always invoke even on the main thread because it's better somehow
Browser.Dispatcher.Invoke(
_browser.Dispatcher.Invoke(
//fire and forget
() => Browser.ExecuteScriptAsync(script),
() => _browser.ExecuteScriptAsync(script),
DispatcherPriority.Background
);
}
@@ -74,11 +106,18 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
private void SetupBinding(IBinding binding)
{
binding.Parent.AssociateWithBinding(binding);
Browser.CoreWebView2.AddHostObjectToScript(binding.Name, binding.Parent);
_browser!.CoreWebView2.AddHostObjectToScript(binding.Name, binding.Parent);
}
public void ShowDevTools() => Browser.CoreWebView2.OpenDevToolsWindow();
public void ShowDevTools() => _browser?.CoreWebView2?.OpenDevToolsWindow();
//https://github.com/MicrosoftEdge/WebView2Feedback/issues/2161
public void Dispose() => Browser.Dispatcher.Invoke(() => Browser.Dispose(), DispatcherPriority.Send);
public void Dispose()
{
if (_browser != null)
{
_browser.Dispatcher.Invoke(() => _browser.Dispose(), DispatcherPriority.Send);
_browser = null;
}
}
}
@@ -24,7 +24,7 @@ namespace Speckle.Connectors.Revit.Bindings;
internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
{
private readonly IAppIdleManager _idleManager;
private readonly RevitIdleManager _revitIdleManager;
private readonly RevitContext _revitContext;
private readonly DocumentModelStore _store;
private readonly ICancellationManager _cancellationManager;
@@ -38,6 +38,9 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private readonly LinkedModelHandler _linkedModelHandler;
private readonly IThreadContext _threadContext;
private readonly ISendOperationManagerFactory _sendOperationManagerFactory;
private bool _isDocChangedSubscribed;
private EventHandler<Autodesk.Revit.DB.Events.DocumentChangedEventArgs>? _documentChangedHandler;
private readonly ConnectorConfig _config;
/// <summary>
/// Used internally to aggregate the changed objects' id. Note we're using a concurrent dictionary here as the expiry check method is not thread safe, and this was causing problems. See:
@@ -48,7 +51,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private ConcurrentHashSet<ElementId> ChangedObjectIds { get; set; } = new();
public RevitSendBinding(
IAppIdleManager idleManager,
RevitIdleManager revitIdleManager,
RevitContext revitContext,
DocumentModelStore store,
ICancellationManager cancellationManager,
@@ -62,11 +65,12 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
LinkedModelHandler linkedModelHandler,
IThreadContext threadContext,
IRevitTask revitTask,
ISendOperationManagerFactory sendOperationManagerFactory
ISendOperationManagerFactory sendOperationManagerFactory,
IConfigStore configStore
)
: base("sendBinding", bridge)
{
_idleManager = idleManager;
_revitIdleManager = revitIdleManager;
_revitContext = revitContext;
_store = store;
_cancellationManager = cancellationManager;
@@ -79,6 +83,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
_linkedModelHandler = linkedModelHandler;
_threadContext = threadContext;
_sendOperationManagerFactory = sendOperationManagerFactory;
_config = configStore.GetConnectorConfig();
Commands = new SendBindingUICommands(bridge);
// TODO expiry events
@@ -86,12 +91,58 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
revitTask.Run(() =>
{
revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
_topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
// revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
// _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
_documentChangedHandler = (_, e) => _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
_store.ModelCardsChanged += (_, e) => OnModelCardsChanged(e);
_store.DocumentChanged += (_, _) => topLevelExceptionHandler.FireAndForget(async () => await OnDocumentChanged());
});
}
private void OnModelCardsChanged(ModelCardsChangedEventArgs e)
{
if (
!_config.DocumentChangeListeningDisabled
&& e.ModelCards.Count > 0
&& e.ModelCards.Any(m => m.TypeDiscriminator == nameof(SenderModelCard))
)
{
SubscribeDocChanged();
}
else
{
UnsubscribeDocChanged();
}
}
private void SubscribeDocChanged()
{
if (_documentChangedHandler == null || _isDocChangedSubscribed)
{
return;
}
_threadContext.RunOnMain(() =>
{
_revitContext.UIApplication.NotNull().Application.DocumentChanged += _documentChangedHandler;
});
_isDocChangedSubscribed = true;
}
private void UnsubscribeDocChanged()
{
if (_documentChangedHandler == null || !_isDocChangedSubscribed)
{
return;
}
_threadContext.RunOnMain(() =>
{
_revitContext.UIApplication.NotNull().Application.DocumentChanged -= _documentChangedHandler;
});
_isDocChangedSubscribed = false;
}
public List<ISendFilter> GetSendFilters() =>
[
new RevitSelectionFilter { IsDefault = true },
@@ -276,7 +327,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
if (addedElementIds.Count > 0)
{
_idleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
_revitIdleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
}
if (HaveUnitsChanged(doc))
@@ -296,8 +347,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
_sendConversionCache.EvictObjects(unpackedObjectIds);
}
_idleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
_idleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
_revitIdleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
_revitIdleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
}
// Keeps track of doc and current units
@@ -1,5 +1,6 @@
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Settings;
using Speckle.Connectors.Revit.Plugin;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Sdk.Common;
@@ -17,27 +18,32 @@ internal sealed class SelectionBinding : RevitBaseBinding, ISelectionBinding, ID
public SelectionBinding(
RevitContext revitContext,
IBrowserBridge parent,
IAppIdleManager idleManager,
RevitIdleManager idleManager,
#if REVIT2022
ITopLevelExceptionHandler topLevelExceptionHandler,
IRevitTask revitTask
#endif
IRevitTask revitTask,
IConfigStore configStore
)
: base("selectionBinding", parent)
{
_revitContext = revitContext;
if (!configStore.GetConnectorConfig().SelectionChangeListeningDisabled)
{
#if REVIT2022
// NOTE: getting the selection data should be a fast function all, even for '000s of elements - and having a timer hitting it every 1s is ok.
_selectionTimer = new System.Timers.Timer(1000);
_selectionTimer.Elapsed += (_, _) => topLevelExceptionHandler.CatchUnhandled(OnSelectionChanged);
_selectionTimer.Start();
// NOTE: getting the selection data should be a fast function all, even for '000s of elements - and having a timer hitting it every 1s is ok.
_selectionTimer = new System.Timers.Timer(1000);
_selectionTimer.Elapsed += (_, _) => topLevelExceptionHandler.CatchUnhandled(OnSelectionChanged);
_selectionTimer.Start();
#else
revitTask.Run(
() =>
_revitContext.UIApplication.NotNull().SelectionChanged += (_, _) =>
idleManager.SubscribeToIdle(nameof(OnSelectionChanged), OnSelectionChanged)
);
revitTask.Run(
() =>
_revitContext.UIApplication.NotNull().SelectionChanged += (_, _) =>
idleManager.SubscribeToIdle(nameof(OnSelectionChanged), OnSelectionChanged)
);
#endif
}
}
private void OnSelectionChanged()
@@ -48,11 +48,12 @@ public static class ServiceRegistration
serviceCollection.AddSingleton<IBinding, SelectionBinding>();
serviceCollection.AddSingleton<IBinding, RevitSendBinding>();
serviceCollection.AddSingleton<IBinding, RevitReceiveBinding>();
serviceCollection.AddSingleton<RevitIdleManager>();
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IBasicConnectorBinding>());
serviceCollection.AddSingleton<IBasicConnectorBinding, BasicConnectorBindingRevit>();
serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
// serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
// send operation and dependencies
serviceCollection.AddScoped<SendOperation<DocumentToConvert>>();
@@ -17,13 +17,16 @@ namespace Speckle.Connectors.Revit.HostApp;
internal sealed class RevitDocumentStore : DocumentModelStore
{
private readonly ILogger<RevitDocumentStore> _logger;
private readonly IAppIdleManager _idleManager;
//private readonly IAppIdleManager _idleManager;
private readonly RevitIdleManager _idleManager;
private readonly RevitContext _revitContext;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
public RevitDocumentStore(
IAppIdleManager idleManager,
//IAppIdleManager idleManager,
RevitIdleManager idleManager,
RevitContext revitContext,
IJsonSerializer jsonSerializer,
ITopLevelExceptionHandler topLevelExceptionHandler,
@@ -34,6 +37,7 @@ internal sealed class RevitDocumentStore : DocumentModelStore
: base(logger, jsonSerializer)
{
_jsonCacheManager = jsonCacheManagerFactory.CreateForUser("ConnectorsFileData");
//_idleManager = idleManager;
_idleManager = idleManager;
_revitContext = revitContext;
_topLevelExceptionHandler = topLevelExceptionHandler;
@@ -50,7 +50,8 @@ public static class SupportedCategoriesUtils
return
#if REVIT2023_OR_GREATER
category.BuiltInCategory != BuiltInCategory.OST_AreaSchemes
&& category.BuiltInCategory != BuiltInCategory.OST_AreaSchemeLines;
&& category.BuiltInCategory != BuiltInCategory.OST_AreaSchemeLines
&& category.BuiltInCategory != BuiltInCategory.INVALID;
#else
category.Name != "OST_AreaSchemeLines" && category.Name != "OST_AreaSchemes";
#endif
@@ -21,6 +21,7 @@ using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
using Transform = Speckle.Objects.Other.Transform;
namespace Speckle.Connectors.Revit.Operations.Receive;
@@ -38,12 +39,15 @@ public sealed class RevitHostObjectBuilder(
IThreadContext threadContext,
RevitToHostCacheSingleton revitToHostCacheSingleton,
ITypedConverter<
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix),
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject),
DirectShape
> localToGlobalDirectShapeConverter,
IReceiveConversionHandler conversionHandler
) : IHostObjectBuilder, IDisposable
{
// Maps atomic object applicationId -> parent DataObject
private readonly Dictionary<string, DataObject> _atomicObjectToParentDataObject = new();
public Task<HostObjectBuilderResult> Build(
Base rootObject,
string projectName,
@@ -102,6 +106,9 @@ public sealed class RevitHostObjectBuilder(
unpackedRoot.ObjectsToConvert.ToList()
);
// Register DataObjects with InstanceProxy displayValues
RegisterDataObjectsWithInstanceProxies(unpackedRoot);
// NOTE: below is 💩... https://github.com/specklesystems/speckle-sharp-connectors/pull/813 broke sketchup to revit workflow
// ids were modified to fix receiving instances [CNX-1707](https://linear.app/speckle/issue/CNX-1707/revit-curves-and-meshes-in-blocks-come-as-duplicated)
// but we then broke sketchup to revit because applicationIds in proxies didn't match modified application ids which cam from #813 hack
@@ -176,6 +183,9 @@ public sealed class RevitHostObjectBuilder(
}
}
// Update DataObject lookup IDs
UpdateAtomicObjectLookupWithModifiedIds(originalToModifiedIds);
// 2 - Bake materials (now with the updated IDs)
if (unpackedRoot.RenderMaterialProxies != null)
{
@@ -234,6 +244,87 @@ public sealed class RevitHostObjectBuilder(
return conversionResults.builderResult;
}
/// <summary>
/// Registers DataObjects that have InstanceProxy displayValues and builds the lookup.
/// </summary>
private void RegisterDataObjectsWithInstanceProxies(RootObjectUnpackerResult unpackedRoot)
{
var definitionToDataObject = new Dictionary<string, DataObject>();
foreach (var tc in unpackedRoot.ObjectsToConvert)
{
if (tc.Current is DataObject dataObject)
{
var instanceProxies = dataObject.displayValue.OfType<InstanceProxy>().ToList();
if (instanceProxies.Count > 0)
{
foreach (var ip in instanceProxies)
{
definitionToDataObject[ip.definitionId] = dataObject;
}
}
}
}
// Build lookup: definition object applicationId -> parent DataObject
_atomicObjectToParentDataObject.Clear();
if (unpackedRoot.DefinitionProxies is not null)
{
foreach (var defProxy in unpackedRoot.DefinitionProxies)
{
if (
defProxy.applicationId is not null
&& definitionToDataObject.TryGetValue(defProxy.applicationId, out var parentDataObject)
)
{
foreach (var objectId in defProxy.objects)
{
_atomicObjectToParentDataObject[objectId] = parentDataObject;
}
}
else
{
logger.LogError(
"Could not find parent DataObject for DefinitionProxy {ApplicationId}",
defProxy.applicationId
);
}
}
}
}
/// <summary>
/// Updates the atomic object lookup with modified IDs
/// </summary>
private void UpdateAtomicObjectLookupWithModifiedIds(Dictionary<string, List<string>> originalToModifiedIds)
{
// Build updated entries first to avoid modifying collection during iteration
var entriesToAdd = new List<KeyValuePair<string, DataObject>>();
var keysToRemove = new List<string>();
foreach (var kvp in _atomicObjectToParentDataObject)
{
if (originalToModifiedIds.TryGetValue(kvp.Key, out var modifiedIds))
{
keysToRemove.Add(kvp.Key);
foreach (var modifiedId in modifiedIds)
{
entriesToAdd.Add(new(modifiedId, kvp.Value));
}
}
}
foreach (var key in keysToRemove)
{
_atomicObjectToParentDataObject.Remove(key);
}
foreach (var entry in entriesToAdd)
{
_atomicObjectToParentDataObject[entry.Key] = entry.Value;
}
}
private Autodesk.Revit.DB.Transform? CalculateNewTransform(
Autodesk.Revit.DB.Transform? receiveTransform,
Autodesk.Revit.DB.Transform? rootTransform
@@ -278,9 +369,17 @@ public sealed class RevitHostObjectBuilder(
onOperationProgressed.Report(new("Converting", (double)++count / localToGlobalMaps.Count));
if (result is DirectShapeDefinitionWrapper)
{
// Look up parent DataObject for this atomic object (handles InstanceProxy displayValue)
var atomicId = localToGlobalMap.AtomicObject.applicationId;
DataObject? parentDataObject = null;
if (atomicId is not null)
{
_atomicObjectToParentDataObject.TryGetValue(atomicId, out parentDataObject);
}
// direct shape creation happens here
DirectShape directShapes = localToGlobalDirectShapeConverter.Convert(
(localToGlobalMap.AtomicObject, localToGlobalMap.Matrix)
(localToGlobalMap.AtomicObject, localToGlobalMap.Matrix, parentDataObject)
);
bakedObjectIds.Add(directShapes.UniqueId);
@@ -351,6 +450,7 @@ public sealed class RevitHostObjectBuilder(
DirectShapeLibrary.GetDirectShapeLibrary(converterSettings.Current.Document).Reset(); // Note: this needs to be cleared, as it is being used in the converter
revitToHostCacheSingleton.MaterialsByObjectId.Clear(); // Massive hack!
_atomicObjectToParentDataObject.Clear();
groupManager.PurgeGroups(baseGroupName);
materialBaker.PurgeMaterials(baseGroupName);
}
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Autodesk.Revit.DB;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Builders;
@@ -7,6 +8,7 @@ using Speckle.Connectors.Common.Extensions;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.DUI.Exceptions;
using Speckle.Connectors.DUI.Settings;
using Speckle.Connectors.Revit.HostApp;
using Speckle.Converters.Common;
using Speckle.Converters.RevitShared.Helpers;
@@ -29,7 +31,8 @@ public class RevitRootObjectBuilder(
SendCollectionManager sendCollectionManager,
ILogger<RevitRootObjectBuilder> logger,
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
LinkedModelHandler linkedModelHandler
LinkedModelHandler linkedModelHandler,
IConfigStore configStore
) : IRootObjectBuilder<DocumentToConvert>
{
public Task<RootObjectBuilderResult> Build(
@@ -42,6 +45,7 @@ public class RevitRootObjectBuilder(
() => Task.FromResult(BuildSync(documentElementContexts, projectId, onOperationProgressed, ct))
);
[SuppressMessage("Maintainability", "CA1506:Avoid excessive class coupling")]
private RootObjectBuilderResult BuildSync(
IReadOnlyList<DocumentToConvert> documentElementContexts,
string projectId,
@@ -134,6 +138,8 @@ public class RevitRootObjectBuilder(
var cacheHitCount = 0;
var skippedObjectCount = 0;
var config = configStore.GetConnectorConfig();
foreach (var atomicObjectByDocumentAndTransform in atomicObjectsByDocumentAndTransform)
{
string? modelDisplayName = null;
@@ -185,7 +191,11 @@ public class RevitRootObjectBuilder(
// TODO: Potential here to transform cached objects and NOT reconvert,
// TODO: we wont do !hasTransform here, and re-set application id before this
if (!hasTransform && sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
if (
!hasTransform
&& !config.DocumentChangeListeningDisabled //This is experimental
&& sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value)
)
{
converted = value;
cacheHitCount++;
@@ -1,43 +1,112 @@
using Autodesk.Revit.UI;
using Autodesk.Revit.UI.Events;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Sdk.Common;
namespace Speckle.Connectors.Revit.Plugin;
public sealed class RevitIdleManager : AppIdleManager
/// <remarks>
/// Please do NOT try and refactor this class.
/// Whether it's to try and generalize with the <see cref="IdleCallManager"/> class
/// or to unnecessary try and make this class thread safe.
/// This class is a simple singleton, targeted to a Revit's host app requirements
/// where everything happens on the main thread, and we can avoid overly complex threading/thread-safty.
///
/// Previous good refactors with good intention have lead to poor debugging experiences, over-engineered threading,
/// and low confidence in the reliability.
/// </remarks>
/// should be registered as singleton
public class RevitIdleManager(
RevitContext revitContext,
ITopLevelExceptionHandler topLevelExceptionHandler,
ILogger<RevitIdleManager> logger
)
{
private readonly UIApplication _uiApplication;
private readonly IIdleCallManager _idleCallManager;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly UIApplication _uiApplication = revitContext.UIApplication.NotNull();
private event EventHandler<IdlingEventArgs>? OnIdle;
private readonly Dictionary<string, Func<Task>> _calls = new();
private bool _hasSubscribed;
public RevitIdleManager(
RevitContext revitContext,
IIdleCallManager idleCallManager,
ITopLevelExceptionHandler topLevelExceptionHandler,
IRevitTask revitTask
)
: base(idleCallManager)
private bool _isExecutingIdle;
/// <summary>
/// Defers the invocation of an <paramref name="action"/> until next Revit idle tick (deduped by name).
/// The <paramref name="action"/> will be called only once.
/// </summary>
/// <param name="name">A key that prevents enqueuing duplicate events</param>
/// <param name="action">The action to be invoked</param>
/// <example>
/// Some events in host app are triggered many times, we might get 10x per object
/// Making this more like a deferred action, so we don't update the UI many times
/// </example>
/// <remarks>
/// This function must be called on the main thread
/// </remarks>
public void SubscribeToIdle(string name, Action action)
{
_topLevelExceptionHandler = topLevelExceptionHandler;
_uiApplication = revitContext.UIApplication.NotNull();
_idleCallManager = idleCallManager;
revitTask.Run(
() => _uiApplication.Idling += (s, e) => OnIdle?.Invoke(s, e) // will be called on the main thread always and fixing the Revit exceptions on subscribing/unsubscribing Idle events
SubscribeToIdle(
name,
() =>
{
action.Invoke();
return Task.CompletedTask;
}
);
}
protected override void AddEvent()
/// <inheritdoc cref="SubscribeToIdle(string, Action)"/>
public void SubscribeToIdle(string name, Func<Task> action)
{
_topLevelExceptionHandler.CatchUnhandled(() =>
if (_isExecutingIdle)
{
OnIdle += RevitAppOnIdle;
});
logger.LogWarning("SubscribeToIdle called while already executing idle events");
}
_calls[name] = action;
if (_hasSubscribed)
{
return;
}
_hasSubscribed = true;
_uiApplication.Idling += RevitAppOnIdle;
}
private void RevitAppOnIdle(object? sender, IdlingEventArgs e) =>
_idleCallManager.AppOnIdle(() => OnIdle -= RevitAppOnIdle);
private void RevitAppOnIdle(object? sender, IdlingEventArgs e)
{
topLevelExceptionHandler.CatchUnhandled(() =>
{
if (_isExecutingIdle)
{
logger.LogWarning("SubscribeToIdle called while already executing idle events");
}
_isExecutingIdle = true;
try
{
try
{
foreach (KeyValuePair<string, Func<Task>> kvp in _calls)
{
topLevelExceptionHandler.FireAndForget(kvp.Value.Invoke);
}
}
finally
{
_calls.Clear();
}
}
finally
{
_uiApplication.Idling -= RevitAppOnIdle;
_isExecutingIdle = false;
// setting last will delay entering re-subscription
_hasSubscribed = false;
}
});
}
}
@@ -54,9 +54,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\DetailLevelSetting.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\IRevitPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitTask.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitExternalApplication.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitThreadContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
@@ -0,0 +1,311 @@
using System.Runtime.InteropServices;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;
using Speckle.Connectors.GrasshopperShared.HostApp;
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Connectors.GrasshopperShared.Properties;
using Speckle.Sdk.Models.Collections;
namespace Speckle.Connectors.GrasshopperShared.Components.Collections;
/// <summary>
/// Creates collections by matching name tree structure to elements tree structure.
/// Each branch in the names tree corresponds to the same-path branch in the elements tree.
/// </summary>
[Guid("7E8F9A1B-2C3D-4E5F-6A7B-8C9D0E1F2A3B")]
public class CollectionsByName : GH_Component
{
public override Guid ComponentGuid => GetType().GUID;
protected override Bitmap Icon => Resources.speckle_collections_create; // TODO: Update to specific icon if available
public override GH_Exposure Exposure => GH_Exposure.primary;
public CollectionsByName()
: base(
"Collections by Name",
"CbN",
"Creates collections by matching name tree structure to objects tree structure. Each branch in the names tree corresponds to the same-path branch in the objects tree.",
ComponentCategories.PRIMARY_RIBBON,
ComponentCategories.COLLECTIONS
) { }
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddTextParameter(
"Names",
"N",
"Collection names (tree structure must match Objects tree structure)",
GH_ParamAccess.tree
);
pManager.AddGenericParameter(
"Objects",
"O",
"Objects to group into collections (tree structure must match Names tree structure)",
GH_ParamAccess.tree
);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
pManager.AddParameter(
new SpeckleCollectionParam(),
"Collection",
"C",
"Root collection containing named sub-collections",
GH_ParamAccess.item
);
protected override void SolveInstance(IGH_DataAccess da)
{
// access the tree data directly from parameters
var namesParam = Params.Input[0];
var elementsParam = Params.Input[1];
var namesTree = namesParam.VolatileData;
var elementsTree = elementsParam.VolatileData;
// validate that both inputs have data
if (namesTree.IsEmpty)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Names tree is empty");
return;
}
if (elementsTree.IsEmpty)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Objects tree is empty");
return;
}
// validate tree structures match exactly
if (!TreeStructuresMatch(namesTree, elementsTree))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Tree structures and topologies must match exactly");
return;
}
// create root collection
var rootCollection = CollectionHelpers.CreateRootCollection(InstanceGuid.ToString());
// process each path
foreach (var path in namesTree.Paths)
{
var nameBranch = namesTree.get_Branch(path);
var elementsBranch = elementsTree.get_Branch(path);
// validate name branch - throw if empty
if (nameBranch.Count == 0)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Name branch at path {path} is empty");
return;
}
// validate name branch - just warn if multiple, use first
if (nameBranch.Count > 1)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Name branch at path {path} has {nameBranch.Count} names - using first name only"
);
}
// get the collection name
string collectionName = GetCollectionName(nameBranch[0]);
if (string.IsNullOrWhiteSpace(collectionName))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Invalid collection name at path {path}");
return;
}
// skip empty element branches with warning
if (elementsBranch.Count == 0)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Skipping empty elements branch at path {path}");
continue;
}
// parse nested collection path (e.g. parent::child)
var collectionNames = collectionName.Split(
new[] { Constants.LAYER_PATH_DELIMITER },
StringSplitOptions.RemoveEmptyEntries
);
// create or get nested collection structure
var targetCollection = GetOrCreateNestedCollection(rootCollection, collectionNames, elementsBranch, path);
// add elements to deepest collection
AddElementsToCollection(targetCollection, elementsBranch, path);
}
// validate collection has content
if (rootCollection.Elements.Count == 0)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Error,
"Collection contains no valid geometry. All branches were empty or contained unsupported types."
);
return;
}
// validate for duplicate application IDs (following CreateCollection pattern)
if (CollectionHelpers.HasDuplicateApplicationIds(rootCollection))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
return;
}
da.SetData(0, new SpeckleCollectionWrapperGoo(rootCollection));
}
/// <summary>
/// Validates that two tree structures have exactly matching paths
/// </summary>
private bool TreeStructuresMatch(
Grasshopper.Kernel.Data.IGH_Structure namesTree,
Grasshopper.Kernel.Data.IGH_Structure elementsTree
)
{
if (namesTree.PathCount != elementsTree.PathCount)
{
return false;
}
// check that all paths match exactly
var namePaths = namesTree.Paths.ToList();
var elementPaths = elementsTree.Paths.ToList();
for (int i = 0; i < namePaths.Count; i++)
{
if (namePaths[i].CompareTo(elementPaths[i]) != 0)
{
return false;
}
}
return true;
}
/// <summary>
/// Extracts collection name, handling GH_String and other text types
/// </summary>
private string GetCollectionName(object nameObj) =>
nameObj switch
{
GH_String ghString => ghString.Value,
IGH_Goo goo => goo.ToString(),
_ => nameObj.ToString()
};
/// <summary>
/// Gets or creates a nested collection structure based on the collection names.
/// </summary>
/// <remarks>
/// Handles paths like "parent::child::grandchild" by creating intermediate collections.
/// </remarks>
private SpeckleCollectionWrapper GetOrCreateNestedCollection(
SpeckleCollectionWrapper rootCollection,
string[] collectionNames,
System.Collections.IList elementsBranch,
Grasshopper.Kernel.Data.GH_Path path
)
{
SpeckleCollectionWrapper currentCollection = rootCollection;
var currentPath = new List<string>(rootCollection.Path);
foreach (var collectionName in collectionNames)
{
// build path for this level
currentPath.Add(collectionName);
// check if child collection already exists
var existingChild = currentCollection
.Elements.OfType<SpeckleCollectionWrapper>()
.FirstOrDefault(c => c.Name == collectionName);
if (existingChild != null)
{
// use existing collection
currentCollection = existingChild;
}
else
{
// create new child collection
var newChild = new SpeckleCollectionWrapper
{
Base = new Collection(),
Name = collectionName,
Path = currentPath.ToList(),
Color = null,
Material = null,
Topology = null, // only set topology on leaf collections
ApplicationId = Guid.NewGuid().ToString()
};
currentCollection.Elements.Add(newChild);
currentCollection = newChild;
}
}
// set topology on the final (leaf) collection
currentCollection.Topology = GetBranchTopology(path, elementsBranch.Count);
return currentCollection;
}
/// <summary>
/// Adds elements from a branch to the target collection
/// </summary>
private void AddElementsToCollection(
SpeckleCollectionWrapper targetCollection,
System.Collections.IList elementsBranch,
Grasshopper.Kernel.Data.GH_Path path
)
{
foreach (var item in elementsBranch)
{
if (item == null)
{
// preserve nulls for topology (CNX-2855 pattern)
targetCollection.Elements.Add(null);
continue;
}
// convert to SpeckleWrapper if possible - cast to IGH_Goo first
if (item is not IGH_Goo goo)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Unsupported object type in branch {path}: {item.GetType().Name}"
);
continue;
}
var wrapper = goo.ToSpeckleObjectWrapper();
if (wrapper == null)
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Unsupported object type in branch {path}: {item.GetType().Name}"
);
continue;
}
if (wrapper is ISpeckleCollectionObject collectionObject)
{
targetCollection.Elements.Add(collectionObject);
}
else
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
$"Object type {wrapper.GetType().Name} is not a valid collection element"
);
}
}
}
/// <summary>
/// Creates topology string for a single branch (following GrasshopperHelpers.GetParamTopology pattern)
/// </summary>
private string GetBranchTopology(Grasshopper.Kernel.Data.GH_Path path, int count) =>
$"{path.ToString(false)}-{count}";
}
@@ -47,7 +47,7 @@ public class CreateCollection : VariableParameterComponentBase
protected override void SolveInstance(IGH_DataAccess dataAccess)
{
var rootCollection = CreateRootCollection();
var rootCollection = CollectionHelpers.CreateRootCollection(InstanceGuid.ToString());
bool hasAnyInput = false;
foreach (var inputParam in Params.Input)
@@ -73,14 +73,14 @@ public class CreateCollection : VariableParameterComponentBase
}
// validate for duplicate application IDs across the entire collection hierarchy
if (HasDuplicateApplicationIds(rootCollection))
if (CollectionHelpers.HasDuplicateApplicationIds(rootCollection))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
return;
}
// validate collection isn't empty (CNX-2855)
if (rootCollection.Elements.Count == 0 || !rootCollection.Elements.Any(HasAnyValidContent))
if (rootCollection.Elements.Count == 0 || !rootCollection.Elements.Any(CollectionHelpers.HasAnyValidContent))
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Error,
@@ -233,56 +233,6 @@ public class CreateCollection : VariableParameterComponentBase
}
}
/// <summary>
/// Validates that all application IDs are unique across the entire collection hierarchy.
/// Shows an error if duplicates are found, indicating objects appear in multiple collections.
/// </summary>
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
private bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
{
// args to CheckForDuplicateApplicationIds passed in since the method can recursively check
var seenIds = new HashSet<string>();
var duplicateIds = new HashSet<string>();
// iterate, create hash set and check all application IDs
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
return duplicateIds.Count > 0;
}
/// <summary>
/// Recursively collects application IDs from all in the collection hierarchy.
/// </summary>
/// <remarks>
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
/// </remarks>
private void ProcessAndCheckForDuplicateApplicationIds(
SpeckleCollectionWrapper collection,
HashSet<string> seenIds,
HashSet<string> duplicateIds
)
{
foreach (var element in collection.Elements)
{
switch (element)
{
case null:
break; // skip nulls (CNX-2855)
case SpeckleCollectionWrapper childCollection:
// recurse into child collections
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
break;
case SpeckleWrapper wrapper:
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
{
duplicateIds.Add(wrapper.ApplicationId);
}
break;
}
}
}
// IGH_VariableParameterComponent implementation
public override bool CanInsertParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input;
@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Parameters;
using Grasshopper.Kernel.Types;
using Speckle.Connectors.GrasshopperShared.Components.BaseComponents;
using Speckle.Connectors.GrasshopperShared.HostApp;
@@ -12,7 +13,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
/// Given a list of objects, this component will filter the list for objects that match the queries.
/// </summary>
[Guid("26AEA046-4DD4-4F61-8251-E92A6D2AC880")]
public class FilterSpeckleObjects : GH_Component
public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
{
public override Guid ComponentGuid => GetType().GUID;
protected override Bitmap Icon => Resources.speckle_objects_filter;
@@ -49,17 +50,6 @@ public class FilterSpeckleObjects : GH_Component
GH_ParamAccess.item
);
Params.Input[3].Optional = true;
pManager.AddTextParameter(
"Application Id",
"aID",
"Find objects with a matching applicationId",
GH_ParamAccess.item
);
Params.Input[4].Optional = true;
pManager.AddTextParameter("Speckle Id", "sID", "Find objects with a matching Speckle id", GH_ParamAccess.item);
Params.Input[5].Optional = true;
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
@@ -103,101 +93,39 @@ public class FilterSpeckleObjects : GH_Component
dataAccess.GetData(2, ref property);
string material = "";
dataAccess.GetData(3, ref material);
// optional parameters - only read if they've been added via ⊕
string appId = "";
dataAccess.GetData(4, ref appId);
string speckleId = "";
dataAccess.GetData(5, ref speckleId);
int? appIdIndex = FindInputIndexByName("Application Id");
int? speckleIdIndex = FindInputIndexByName("Speckle Id");
if (appIdIndex.HasValue)
{
dataAccess.GetData(appIdIndex.Value, ref appId);
}
if (speckleIdIndex.HasValue)
{
dataAccess.GetData(speckleIdIndex.Value, ref speckleId);
}
bool filterByAppId = appIdIndex.HasValue;
bool filterBySpeckleId = speckleIdIndex.HasValue;
List<SpeckleWrapper> matchedObjects = new();
List<SpeckleWrapper> removedObjects = new();
for (int i = 0; i < objects.Count; i++)
foreach (SpeckleWrapper wrapper in objects.Cast<SpeckleWrapper>())
{
SpeckleWrapper wrapper = objects[i]!;
// filter by name
if (!MatchesSearchPattern(name, wrapper.Name))
if (MatchesAllFilters(wrapper, name, property, material, appId, filterByAppId, speckleId, filterBySpeckleId))
{
removedObjects.Add(wrapper);
continue;
}
// filter by property
bool foundProperty = false;
if (string.IsNullOrEmpty(property))
{
foundProperty = true;
matchedObjects.Add(wrapper);
}
else
{
SpecklePropertyGroupGoo? properties = wrapper is SpeckleDataObjectWrapper dataObjPropWrapper
? dataObjPropWrapper.Properties
: wrapper is SpeckleGeometryWrapper geoPropWrapper
? geoPropWrapper.Properties
: null;
if (properties is not null)
{
// use flattened properties to search ALL nested property keys
// fix for [CNX-2512](https://linear.app/speckle/issue/CNX-2512/filter-objects-material-and-property-key-inputs-dont-work-as-expected)
Dictionary<string, SpecklePropertyGoo> flattenedProps = properties.Flatten();
foreach (string key in flattenedProps.Keys)
{
if (MatchesSearchPattern(property, key))
{
foundProperty = true;
break;
}
}
}
}
if (!foundProperty)
{
removedObjects.Add(wrapper);
continue;
}
// filter by material name
bool materialMatches = true;
if (!string.IsNullOrEmpty(material))
{
materialMatches = false;
if (wrapper is SpeckleGeometryWrapper geoWrapper)
{
materialMatches = MatchesSearchPattern(material, geoWrapper.Material?.Name ?? "");
}
else if (wrapper is SpeckleDataObjectWrapper dataObjWrapper)
{
// check if ANY geometry in the data object has a matching material (not sure about this...)
// fix for [CNX-2512](https://linear.app/speckle/issue/CNX-2512/filter-objects-material-and-property-key-inputs-dont-work-as-expected)
materialMatches = dataObjWrapper.Geometries.Any(geo =>
MatchesSearchPattern(material, geo.Material?.Name ?? "")
);
}
}
if (!materialMatches)
{
removedObjects.Add(wrapper);
continue;
}
// filter by application id
if (!MatchesSearchPattern(appId, wrapper.Base.applicationId ?? ""))
{
removedObjects.Add(wrapper);
continue;
}
// filter by speckle id
if (!MatchesSearchPattern(speckleId, wrapper.Base.id ?? ""))
{
removedObjects.Add(wrapper);
continue;
}
matchedObjects.Add(wrapper);
}
// Set output objects
@@ -214,4 +142,190 @@ public class FilterSpeckleObjects : GH_Component
return Operator.IsSymbolNameLike(target, searchPattern);
}
/// <summary>
/// Determines if a wrapper matches all active filter criteria.
/// </summary>
private bool MatchesAllFilters(
SpeckleWrapper wrapper,
string name,
string property,
string material,
string appId,
bool filterByAppId,
string speckleId,
bool filterBySpeckleId
)
{
// filter by name
if (!MatchesSearchPattern(name, wrapper.Name))
{
return false;
}
// filter by property
if (!MatchesPropertyFilter(wrapper, property))
{
return false;
}
// filter by material name
if (!MatchesMaterialFilter(wrapper, material))
{
return false;
}
// filter by application id (only if parameter was added)
if (filterByAppId && !MatchesSearchPattern(appId, wrapper.Base.applicationId ?? ""))
{
return false;
}
// filter by speckle id (only if parameter was added)
if (filterBySpeckleId && !MatchesSearchPattern(speckleId, wrapper.Base.id ?? ""))
{
return false;
}
return true;
}
private bool MatchesPropertyFilter(SpeckleWrapper wrapper, string property)
{
if (string.IsNullOrEmpty(property))
{
return true;
}
SpecklePropertyGroupGoo? properties = wrapper is SpeckleDataObjectWrapper dataObjPropWrapper
? dataObjPropWrapper.Properties
: wrapper is SpeckleGeometryWrapper geoPropWrapper
? geoPropWrapper.Properties
: null;
if (properties is null)
{
return false;
}
// use flattened properties to search ALL nested property keys
return properties.Flatten().Keys.Any(key => MatchesSearchPattern(property, key));
}
private bool MatchesMaterialFilter(SpeckleWrapper wrapper, string material)
{
if (string.IsNullOrEmpty(material))
{
return true;
}
if (wrapper is SpeckleGeometryWrapper geoWrapper)
{
return MatchesSearchPattern(material, geoWrapper.Material?.Name ?? "");
}
if (wrapper is SpeckleDataObjectWrapper dataObjWrapper)
{
// check if ANY geometry in the data object has a matching material
return dataObjWrapper.Geometries.Any(geo => MatchesSearchPattern(material, geo.Material?.Name ?? ""));
}
return false;
}
/// <summary>
/// Finds the index of an input parameter by its Name.
/// Returns null if the parameter doesn't exist.
/// </summary>
private int? FindInputIndexByName(string paramName)
{
for (int i = 0; i < Params.Input.Count; i++)
{
if (Params.Input[i].Name == paramName)
{
return i;
}
}
return null;
}
#region IGH_VariableParameterComponent
public bool CanInsertParameter(GH_ParameterSide side, int index)
{
if (side != GH_ParameterSide.Input)
{
return false;
}
// only allow inserting after the fixed parameters (index 4+)
if (index < 4)
{
return false;
}
// check how many optional params are already added (total inputs - 4 fixed)
int addedOptionalCount = Params.Input.Count - 4;
// we have 2 optional parameters available
return addedOptionalCount < 2;
}
public bool CanRemoveParameter(GH_ParameterSide side, int index) =>
// only allow removing optional input parameters (index 4+)
side == GH_ParameterSide.Input
&& index >= 4;
/// <remarks>
/// The ternary operator for NickName is needed due to a Grasshopper quirk where
/// dynamically created parameters don't respect the "Draw Full Names" setting automatically.
/// We check CanvasFullNames at creation time to set the appropriate NickName.
/// This does not handle the case where the user toggles "Draw Full Names" while the
/// component is already on the canvas. Handling that would require subscribing to
/// Grasshopper.CentralSettings.CanvasFullNamesChanged event, which is overkill for now.
/// </remarks>
public IGH_Param CreateParameter(GH_ParameterSide side, int index)
{
bool hasAppId = FindInputIndexByName("Application Id").HasValue;
bool hasSpeckleId = FindInputIndexByName("Speckle Id").HasValue;
if (!hasAppId)
{
return new Param_String
{
Name = "Application Id",
NickName = Grasshopper.CentralSettings.CanvasFullNames ? "Application Id" : "aID", // see remarks
Description = "Find objects with a matching applicationId",
Access = GH_ParamAccess.item,
Optional = true
};
}
if (!hasSpeckleId)
{
return new Param_String
{
Name = "Speckle Id",
NickName = Grasshopper.CentralSettings.CanvasFullNames ? "Speckle Id" : "sID", // see remarks
Description = "Find objects with a matching Speckle id",
Access = GH_ParamAccess.item,
Optional = true
};
}
return new Param_String();
}
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input && index >= 4;
public void VariableParameterMaintenance()
{
// ensure all optional parameters stay marked as optional
for (int i = 4; i < Params.Input.Count; i++)
{
Params.Input[i].Optional = true;
}
}
#endregion
}
@@ -9,7 +9,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("8D2E3F4A-1B5C-4E7F-9A8B-3C6D9E2F1A4B")]
public class SpeckleBlockDefinitionPassthrough()
: SpeckleSolveInstance(
: SpecklePassthroughComponentBase(
"Speckle Block Definition",
"SBD",
"Create or modify a Speckle Block Definition",
@@ -21,6 +21,9 @@ public class SpeckleBlockDefinitionPassthrough()
protected override Bitmap Icon => Resources.speckle_objects_block_def;
public override GH_Exposure Exposure => GH_Exposure.tertiary;
protected override int FixedInputCount => 3;
protected override int FixedOutputCount => 3;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddParameter(
@@ -122,12 +125,16 @@ public class SpeckleBlockDefinitionPassthrough()
result.Value.Name = inputName;
}
// no need to process application Id.
// New definitions should have a new appID generated in the new() constructor, and we want to preserve old appID otherwise for changetracking.
// process application id (only if user provided one, otherwise preserve existing)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.Value.ApplicationId = inputAppId;
}
// set outputs
da.SetData(0, result);
da.SetDataList(1, result.Value.Objects.Select(o => o.CreateGoo()));
da.SetData(2, result.Value.Name);
SetApplicationIdOutput(da, result.Value.ApplicationId);
}
}
@@ -9,7 +9,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("2F8A9B1C-3D4E-5F6A-7B8C-9D0E1F2A3B4C")]
public class SpeckleBlockInstancePassthrough()
: SpeckleSolveInstance(
: SpecklePassthroughComponentBase(
"Speckle Block Instance",
"SBI",
"Create or modify a Speckle Block Instance",
@@ -21,6 +21,9 @@ public class SpeckleBlockInstancePassthrough()
protected override Bitmap Icon => Resources.speckle_objects_block_inst;
public override GH_Exposure Exposure => GH_Exposure.tertiary;
protected override int FixedInputCount => 7;
protected override int FixedOutputCount => 7;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
int instanceIndex = pManager.AddParameter(
@@ -205,8 +208,11 @@ public class SpeckleBlockInstancePassthrough()
result.Value.Material = inputMaterial.Value;
}
// no need to process application id.
// new appids are generated if this is a new object, otherwise the input object appID should be preserved for change tracking.
// process application id (only if user provided one, otherwise preserve existing)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.Value.ApplicationId = inputAppId;
}
// Set outputs
da.SetData(0, result);
@@ -216,6 +222,7 @@ public class SpeckleBlockInstancePassthrough()
da.SetData(4, result.Value.Properties);
da.SetData(5, result.Value.Color);
da.SetData(6, result.Value.Material);
SetApplicationIdOutput(da, result.Value.ApplicationId);
}
private Transform? ExtractTransform(IGH_Goo input) =>
@@ -8,7 +8,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("5CE8AA40-7706-4893-853D-4C77604548FA")]
public class SpeckleDataObjectPassthrough()
: SpeckleSolveInstance(
: SpecklePassthroughComponentBase(
"Speckle Data Object",
"SDO",
"Create or modify a Speckle Data Object",
@@ -20,6 +20,9 @@ public class SpeckleDataObjectPassthrough()
protected override Bitmap Icon => Resources.speckle_objects_dataobject;
public override GH_Exposure Exposure => GH_Exposure.secondary;
protected override int FixedInputCount => 4;
protected override int FixedOutputCount => 5;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
int objIndex = pManager.AddParameter(
@@ -158,9 +161,17 @@ public class SpeckleDataObjectPassthrough()
result.Properties = inputProperties;
}
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
result.ApplicationId ??= Guid.NewGuid().ToString();
// process application id (only if user provided one)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.ApplicationId = inputAppId;
}
else
{
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
result.ApplicationId ??= Guid.NewGuid().ToString();
}
// get the path
string? path =
@@ -172,5 +183,6 @@ public class SpeckleDataObjectPassthrough()
da.SetData(2, result.Name);
da.SetData(3, result.Properties);
da.SetData(4, path);
SetApplicationIdOutput(da, result.ApplicationId);
}
}
@@ -10,7 +10,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
[Guid("F9418610-ACAE-4417-B010-19EBEA6A121F")]
public class SpeckleGeometryPassthrough()
: SpeckleSolveInstance(
: SpecklePassthroughComponentBase(
"Speckle Geometry",
"SG",
"Create or modify a Speckle Geometry",
@@ -22,6 +22,9 @@ public class SpeckleGeometryPassthrough()
protected override Bitmap Icon => Resources.speckle_objects_geometry;
public override GH_Exposure Exposure => GH_Exposure.secondary;
protected override int FixedInputCount => 6;
protected override int FixedOutputCount => 7;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
int objIndex = pManager.AddGenericParameter(
@@ -220,8 +223,11 @@ public class SpeckleGeometryPassthrough()
result.Material = inputMaterial.Value;
}
// no need to process application Id.
// New definitions should have a new appID generated in the new() constructor, and we want to preserve old appID otherwise for changetracking.
// process application id (only if user provided one, otherwise preserve existing)
if (TryGetApplicationIdInput(da, out string? inputAppId))
{
result.ApplicationId = inputAppId;
}
// get the path
string? path =
@@ -235,6 +241,7 @@ public class SpeckleGeometryPassthrough()
da.SetData(4, result.Color);
da.SetData(5, result.Material);
da.SetData(6, path);
SetApplicationIdOutput(da, result.ApplicationId);
}
// keeps the geometry and wrapped base the same while assigning all other props from the inut wrapper
@@ -87,6 +87,7 @@ public class SendAsyncComponent : GH_AsyncComponent<SendAsyncComponent>
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddParameter(new SpeckleUrlModelResourceParam());
pManager.AddTextParameter("Version ID", "V", "ID of the created version", GH_ParamAccess.item);
}
public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
@@ -321,6 +322,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
private Stopwatch? _stopwatch;
public SpeckleUrlModelResource? OutputParam { get; set; }
public string? OutputVersionId { get; set; }
private List<(GH_RuntimeMessageLevel, string)> RuntimeMessages { get; } = new();
public override WorkerInstance<SendAsyncComponent> Duplicate(string id, CancellationToken cancellationToken)
@@ -332,6 +334,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
{
_stopwatch = new Stopwatch();
_stopwatch.Start();
OutputVersionId = null;
}
public override void SetData(IGH_DataAccess da)
@@ -342,6 +345,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
{
Parent.JustPastedIn = false;
da.SetData(0, Parent.OutputParam);
da.SetData(1, OutputVersionId);
return;
}
@@ -357,6 +361,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
}
da.SetData(0, OutputParam);
da.SetData(1, OutputVersionId);
Parent.CurrentComponentState = ComponentState.UpToDate;
Parent.OutputParam = OutputParam; // ref the outputs in the parent too, so we can serialise them on write/read
@@ -373,7 +378,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
*/
Parent.AddRuntimeMessage(
GH_RuntimeMessageLevel.Remark,
$"Successfully published to Speckle. Right-click on the component to view online."
"Successfully published to Speckle. Right-click on the component to view online."
);
Parent.AddRuntimeMessage(
GH_RuntimeMessageLevel.Remark,
@@ -471,6 +476,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
result.VersionId
);
OutputParam = createdVersion;
OutputVersionId = result.VersionId;
Parent.Url = $"{createdVersion.Account.Server}/projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}";
}
}
@@ -36,9 +36,10 @@ public class SendComponentInput
}
}
public class SendComponentOutput(SpeckleUrlModelResource? resource)
public class SendComponentOutput(SpeckleUrlModelResource? resource, string? versionId = null)
{
public SpeckleUrlModelResource? Resource { get; } = resource;
public string? VersionId { get; } = versionId;
}
public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, SendComponentOutput>
@@ -86,8 +87,11 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
pManager.AddBooleanParameter("Run", "r", "Run the publish operation", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddParameter(new SpeckleUrlModelResourceParam());
pManager.AddTextParameter("Version ID", "V", "ID of the created version", GH_ParamAccess.item);
}
protected override SendComponentInput GetInput(IGH_DataAccess da)
{
@@ -134,6 +138,7 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
else
{
da.SetData(0, result.Resource);
da.SetData(1, result.VersionId);
Message = "Done";
}
}
@@ -216,7 +221,7 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
using var client = clientFactory.Create(account);
var sendInfo = await input.Resource.GetSendInfo(client, cancellationToken).ConfigureAwait(false);
await sendOperation
var result = await sendOperation
.Execute(
new List<SpeckleCollectionWrapperGoo> { collectionToSend },
sendInfo,
@@ -244,6 +249,6 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
sendInfo.ModelId
);
Url = $"{sendInfo.Account.serverInfo.url}/projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}";
return new SendComponentOutput(createdVersionResource);
return new SendComponentOutput(createdVersionResource, result.VersionId);
}
}
@@ -0,0 +1,231 @@
using GH_IO.Serialization;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Parameters;
namespace Speckle.Connectors.GrasshopperShared.Components;
/// <summary>
/// Base class for passthrough components with "hidden" Application ID parameter.
/// </summary>
/// <remarks>
/// Users can click ⊕ to add an optional Application ID input and output.
/// </remarks>
public abstract class SpecklePassthroughComponentBase : SpeckleSolveInstance, IGH_VariableParameterComponent
{
private const string APP_ID_NAME = "Application Id";
private const string APP_ID_NICKNAME = "aID";
private const string APP_ID_DESCRIPTION = "The application id of the Speckle objects";
protected abstract int FixedInputCount { get; }
protected abstract int FixedOutputCount { get; }
private bool HasApplicationIdParam => Params.Input.Count > FixedInputCount;
protected SpecklePassthroughComponentBase(
string name,
string nickname,
string description,
string category,
string subCategory
)
: base(name, nickname, description, category, subCategory) { }
/// <summary>
/// Reads the optional Application Id input. Returns true if user provided a valid value.
/// </summary>
protected bool TryGetApplicationIdInput(IGH_DataAccess da, out string? applicationId)
{
applicationId = null;
if (!HasApplicationIdParam)
{
return false;
}
string appId = string.Empty;
if (da.GetData(FixedInputCount, ref appId))
{
if (string.IsNullOrWhiteSpace(appId))
{
AddRuntimeMessage(
GH_RuntimeMessageLevel.Warning,
"Empty Application Id ignored - existing or auto-generated id will be used"
);
return false;
}
applicationId = appId;
return true;
}
return false;
}
/// <summary>
/// Sets the Application Id output (if the parameter exists).
/// </summary>
protected void SetApplicationIdOutput(IGH_DataAccess da, string? applicationId)
{
if (!HasApplicationIdParam)
{
return;
}
da.SetData(FixedOutputCount, applicationId);
}
public bool CanInsertParameter(GH_ParameterSide side, int index)
{
// only allow inserting if not yet added
if (HasApplicationIdParam)
{
return false;
}
// only allow at the end position
return side switch
{
GH_ParameterSide.Input => index == FixedInputCount,
GH_ParameterSide.Output => index == FixedOutputCount,
_ => false
};
}
public bool CanRemoveParameter(GH_ParameterSide side, int index)
{
if (!HasApplicationIdParam)
{
return false;
}
return side switch
{
GH_ParameterSide.Input => index == FixedInputCount,
GH_ParameterSide.Output => index == FixedOutputCount,
_ => false
};
}
/// <remarks>
/// The ternary for NickName handles a Grasshopper quirk where dynamically created parameters
/// don't respect the "Draw Full Names" setting automatically.
/// </remarks>
public IGH_Param CreateParameter(GH_ParameterSide side, int index)
{
// when adding on either side, add both input and output together
if (side == GH_ParameterSide.Input && Params.Output.Count == FixedOutputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => AddApplicationIdOutput());
}
else if (side == GH_ParameterSide.Output && Params.Input.Count == FixedInputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => AddApplicationIdInput());
}
return CreateApplicationIdParam();
}
public bool DestroyParameter(GH_ParameterSide side, int index)
{
// when removing from either side, remove both input and output together
if (side == GH_ParameterSide.Input && index == FixedInputCount && Params.Output.Count > FixedOutputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => RemoveApplicationIdOutput());
}
else if (side == GH_ParameterSide.Output && index == FixedOutputCount && Params.Input.Count > FixedInputCount)
{
OnPingDocument()?.ScheduleSolution(5, _ => RemoveApplicationIdInput());
}
return side switch
{
GH_ParameterSide.Input => index == FixedInputCount,
GH_ParameterSide.Output => index == FixedOutputCount,
_ => false
};
}
public void VariableParameterMaintenance()
{
// ensure the Application Id input stays optional
if (HasApplicationIdParam && Params.Input.Count > FixedInputCount)
{
Params.Input[FixedInputCount].Optional = true;
}
}
private static IGH_Param CreateApplicationIdParam() =>
new Param_String
{
Name = APP_ID_NAME,
NickName = Grasshopper.CentralSettings.CanvasFullNames ? APP_ID_NAME : APP_ID_NICKNAME,
Description = APP_ID_DESCRIPTION,
Access = GH_ParamAccess.item,
Optional = true
};
private void AddApplicationIdInput()
{
if (Params.Input.Count > FixedInputCount)
{
return;
}
Params.RegisterInputParam(CreateApplicationIdParam());
Params.OnParametersChanged();
VariableParameterMaintenance();
ExpireSolution(true);
}
private void AddApplicationIdOutput()
{
if (Params.Output.Count > FixedOutputCount)
{
return;
}
Params.RegisterOutputParam(CreateApplicationIdParam());
Params.OnParametersChanged();
ExpireSolution(true);
}
private void RemoveApplicationIdInput()
{
if (Params.Input.Count <= FixedInputCount)
{
return;
}
Params.UnregisterInputParameter(Params.Input[FixedInputCount]);
Params.OnParametersChanged();
ExpireSolution(true);
}
private void RemoveApplicationIdOutput()
{
if (Params.Output.Count <= FixedOutputCount)
{
return;
}
Params.UnregisterOutputParameter(Params.Output[FixedOutputCount]);
Params.OnParametersChanged();
ExpireSolution(true);
}
public override bool Write(GH_IWriter writer)
{
var result = base.Write(writer);
writer.SetBoolean("HasApplicationIdParam", HasApplicationIdParam);
return result;
}
public override bool Read(GH_IReader reader)
{
var result = base.Read(reader);
// parameters are restored by GH serialization, this flag is for reference
bool hasAppIdParam = false;
reader.TryGetBoolean("HasApplicationIdParam", ref hasAppIdParam);
return result;
}
}
@@ -0,0 +1,83 @@
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Sdk.Models.Collections;
namespace Speckle.Connectors.GrasshopperShared.Components.Collections;
/// <summary>
/// Shared helper methods for collection components to avoid code duplication
/// </summary>
public static class CollectionHelpers
{
/// <summary>
/// Creates a root collection wrapper with default values
/// </summary>
public static SpeckleCollectionWrapper CreateRootCollection(string instanceGuid) =>
new SpeckleCollectionWrapper
{
Base = new Collection(),
Name = "Unnamed",
Path = new List<string> { "Unnamed" },
Color = null,
Material = null,
ApplicationId = instanceGuid
};
/// <summary>
/// Validates that all application IDs are unique across the entire collection hierarchy.
/// </summary>
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
public static bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
{
var seenIds = new HashSet<string>();
var duplicateIds = new HashSet<string>();
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
return duplicateIds.Count > 0;
}
/// <summary>
/// Recursively collects application IDs from all wrappers in the collection hierarchy.
/// </summary>
/// <remarks>
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
/// </remarks>
private static void ProcessAndCheckForDuplicateApplicationIds(
SpeckleCollectionWrapper collection,
HashSet<string> seenIds,
HashSet<string> duplicateIds
)
{
foreach (var element in collection.Elements)
{
switch (element)
{
case null:
break; // skip nulls (CNX-2855)
case SpeckleCollectionWrapper childCollection:
// recurse into child collections
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
break;
case SpeckleWrapper wrapper:
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
{
duplicateIds.Add(wrapper.ApplicationId);
}
break;
}
}
}
/// <summary>
/// Recursively checks if collection or any descendants contain valid geometry/data objects
/// </summary>
public static bool HasAnyValidContent(ISpeckleCollectionObject? element) =>
element switch
{
SpeckleGeometryWrapper => true,
SpeckleDataObjectWrapper => true,
SpeckleCollectionWrapper collection => collection.Elements.Any(HasAnyValidContent),
_ => false
};
}
@@ -225,6 +225,14 @@ internal sealed class LocalToGlobalMapHandler
var entry = _dataObjectInstanceRegistry.GetEntries()[dataObjectId];
var resolvedGeometries = ResolveInstanceProxiesToGeometries(entry.InstanceProxies);
var primitiveConverted = dataObject
.displayValue.Where(item => item is not InstanceProxy)
.SelectMany(item => SpeckleConversionContext.Current.ConvertToHost(item))
.ToList();
resolvedGeometries.AddRange(ConvertToGeometryWrappers(primitiveConverted));
var dataObjectWrapper = CreateDataObjectWrapper(dataObject, resolvedGeometries, path, objectCollection);
CollectionRebuilder.AppendSpeckleGrasshopperObject(dataObjectWrapper, path, _colorUnpacker, _materialUnpacker);
@@ -14,6 +14,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Components\BaseComponents\ValueSet.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\BaseComponents\VariableParameterComponentBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CollectionPathsSelector.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CollectionsByName.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CreateCollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\ExpandCollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Objects\ExpandSpeckleProperties.cs" />
@@ -41,7 +42,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\VersionMenuHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\WorkspaceMenuHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\SpeckleOperationWizard.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\SpecklePassthroughComponentBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Components\SpeckleSolveInstance.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\CollectionHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extras\StateTag.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\KeyWatcher.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\GrasshopperBlockUnpacker.cs" />
@@ -122,7 +122,9 @@ public class RhinoLayerBaker : TraversalContextUnpacker
continue;
}
var cleanNewLayerName = RhinoUtils.CleanLayerName(collection.name);
var cleanNewLayerName = string.IsNullOrWhiteSpace(collection.name)
? "unnamed"
: RhinoUtils.CleanLayerName(collection.name);
if (!ModelComponent.IsValidComponentName(cleanNewLayerName))
{
@@ -14,6 +14,7 @@ public static class RhinoUtils
public static string CleanLayerName(string str)
{
var sb = new StringBuilder(str.Length);
bool lastWasSpace = true;
foreach (char c in str)
{
@@ -30,10 +31,29 @@ public static class RhinoUtils
if (s_replaceWithHyphen.Contains(c))
{
sb.Append('-');
lastWasSpace = false;
continue;
}
// Collapse double spaces into one and skip leading spaces.
// e.g. " Items Name " -> "Items Name"
if (c == ' ')
{
if (!lastWasSpace)
{
sb.Append(c);
lastWasSpace = true;
}
continue;
}
sb.Append(c);
lastWasSpace = false;
}
if (sb.Length > 0 && sb[^1] == ' ')
{
sb.Length--;
}
return sb.ToString();
@@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
using Rhino.PlugIns;
using Speckle.Connectors.Common;
using Speckle.Connectors.DUI;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.Rhino.DependencyInjection;
using Speckle.Converters.Rhino;
using Speckle.Sdk;
@@ -50,6 +51,10 @@ public class SpeckleConnectorsRhinoPlugin : PlugIn
// but the Rhino connector has `.rhp` as it is extension.
Container = services.BuildServiceProvider();
// FORCE INITIALIZATION RhinoDocumentStore to register event handlers for BeginOpenDocument and EndOpenDocument
// this is needed when the user opens a Rhino file by double clicking the file,
// instead of opening a file in an already running Rhino instance
Container.GetRequiredService<DocumentModelStore>();
Container.UseDUI();
return LoadReturnCode.Success;
@@ -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_";
}
@@ -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 = ">";
}
@@ -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,
@@ -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));
}
@@ -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;
}
@@ -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>
@@ -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();
@@ -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());
}
@@ -20,7 +20,7 @@ public class HierarchicalPropertyHandler(
public override Dictionary<string, object?> GetProperties(NAV.ModelItem modelItem)
{
var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem) ?? [];
var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem);
// Interop-lite mapping for Revit built-in categories
if (_mapRevit && revitCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
@@ -1,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; }
@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
}
@@ -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
@@ -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);
}
@@ -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}");
}
@@ -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
{
@@ -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
);
@@ -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);
}
@@ -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>
@@ -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);
@@ -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
{
@@ -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));
}
@@ -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;
@@ -4,6 +4,7 @@ using Speckle.Converters.Common.ToSpeckle;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Converters.RevitShared.ToSpeckle;
using Speckle.DoubleNumerics;
using Speckle.Objects;
using Speckle.Sdk;
@@ -74,7 +75,8 @@ public sealed class DisplayValueExtractor
}
return areaDisplay;
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView + apply reference point transform
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView
// Reference point transform is handled by point converters during conversion
case DB.Structure.Rebar rebar:
return _converterSettings.Current.SendRebarsAsVolumetric
? GetRebarVolumetricDisplayValue(rebar)
@@ -107,15 +109,16 @@ public sealed class DisplayValueExtractor
using DB.Transform? documentToLocal = localToDocument?.Inverse;
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
using DB.Transform? compoundTransform =
localToDocument is not null && documentToWorld is not null
? documentToWorld.Multiply(localToDocument)
: localToDocument;
DB.Transform? localToWorld = compoundTransform ?? documentToWorld;
DB.Transform? localToWorld = (localToDocument, documentToWorld) switch
{
(not null, not null) => documentToWorld.Multiply(localToDocument),
(not null, null) => localToDocument,
(null, not null) => documentToWorld,
(null, null) => null
};
var collections = GetSortedGeometryFromElement(element, options, documentToLocal);
return ProcessGeometryCollections(element, collections, localToWorld);
return ProcessGeometryCollections(element, collections, localToWorld, localToDocument);
}
/// <summary>
@@ -164,44 +167,42 @@ public sealed class DisplayValueExtractor
}
/// <summary>
/// Processes collections of different geometry types and converts them to display values.
/// Extracted as a common method to reduce code duplication between regular geometry processing and special cases like rebar.
/// Converts sorted geometry into DisplayValueResults <see cref="ElementTopLevelConverterToSpeckle"/>.
/// </summary>
/// <remarks>
/// Essentially all the ensuing steps after the common get_Geometry element method
/// Meshes get localToWorld attached as transform metadata (for instancing).
/// Curves, polylines, and points get curveTransform applied (instance transform only) -
/// reference point transform is handled by the point converters.
/// </remarks>
private List<DisplayValueResult> ProcessGeometryCollections(
DB.Element element,
GeometryCollections collections,
DB.Transform? localToWorld
DB.Transform? localToWorld,
DB.Transform? curveTransform
)
{
// handle all solids and meshes by their material
var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids);
List<SOG.Mesh> displayMeshes = _meshByMaterialConverter.Convert(
var displayMeshes = _meshByMaterialConverter.Convert(
(meshesByMaterial, element.Id, ShouldSetElementDisplayToTransparent(element))
);
List<DisplayValueResult> displayValue = new(collections.TotalCount);
Matrix4x4? matrix = localToWorld is not null ? TransformToMatrix(localToWorld) : null;
foreach (SOG.Mesh mesh in displayMeshes)
foreach (var mesh in displayMeshes)
{
// if we have a transform, keep mesh in symbol space and attach transform
displayValue.Add(
matrix.HasValue
? DisplayValueResult.WithTransform(mesh, matrix.Value)
localToWorld != null
? DisplayValueResult.WithTransform(mesh, TransformToMatrix(localToWorld))
: DisplayValueResult.WithoutTransform(mesh)
);
}
// transform curves, polylines, and points to world coordinates before conversion.
// Unlike meshes/solids which are proxified with transform matrices, these geometry
// types must have their final world coordinates baked directly into their geometry.
foreach (var curve in collections.Curves)
{
if (localToWorld is not null)
if (curveTransform is not null)
{
using var transformedCurve = curve.CreateTransformed(localToWorld);
using var transformedCurve = curve.CreateTransformed(curveTransform);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
@@ -210,15 +211,12 @@ public sealed class DisplayValueExtractor
}
}
// Note: Creating new polyline/point instances for transformation isn't ideal for perf,
// but Revit API doesn't provide in-place transform methods. Trade-off is acceptable since
// family instances typically don't have massive numbers of raw polylines/points in their geometry.
foreach (var polyline in collections.Polylines)
{
if (localToWorld is not null)
if (curveTransform is not null)
{
var coords = polyline.GetCoordinates();
var transformedCoords = coords.Select(coord => localToWorld.OfPoint(coord)).ToList();
var transformedCoords = coords.Select(coord => curveTransform.OfPoint(coord)).ToList();
using var transformedPolyline = DB.PolyLine.Create(transformedCoords);
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(transformedPolyline)));
}
@@ -230,9 +228,9 @@ public sealed class DisplayValueExtractor
foreach (var point in collections.Points)
{
if (localToWorld is not null)
if (curveTransform is not null)
{
using var transformedPoint = DB.Point.Create(localToWorld.OfPoint(point.Coord));
using var transformedPoint = DB.Point.Create(curveTransform.OfPoint(point.Coord));
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(transformedPoint)));
}
else
@@ -330,23 +328,17 @@ public sealed class DisplayValueExtractor
};
/// <summary>
/// According to the remarks on the GeometryInstance class in the RevitAPIDocs,
/// https://www.revitapidocs.com/2024/fe25b14f-5866-ca0f-a660-c157484c3a56.htm,
/// a family instance geometryElement should have a top-level geometry instance when the symbol
/// does not have modified geometry (the docs say that modified geometry will not have a geom instance,
/// however in my experience, all family instances have a top-level geom instance, but if the family instance
/// is modified, then the geom instance won't contain any geometry.)
///
/// This remark also leads me to think that a family instance will not have top-level solids and geom instances.
/// We are logging cases where this is not true.
///
/// Note: this is basically a geometry unpacker for all types of geometry
/// Sorts element geometry into solids, meshes, curves, polylines, points.
/// </summary>
/// <remarks>
/// GeometryInstances are processed via GetSymbolGeometry() with accumulated transforms,
/// keeping meshes in symbol space and avoiding double transforms.
/// </remarks>
private void SortGeometry(
DB.Element element,
GeometryCollections collections,
DB.GeometryElement geom,
DB.Transform? worldToLocal
DB.Transform? accumulatedTransform
)
{
foreach (DB.GeometryObject geomObj in geom)
@@ -359,56 +351,62 @@ public sealed class DisplayValueExtractor
switch (geomObj)
{
case DB.Solid solid:
// skip invalid solid
if (solid.Faces.Size == 0)
{
continue;
}
if (worldToLocal is not null)
if (accumulatedTransform != null)
{
solid = DB.SolidUtils.CreateTransformed(solid, worldToLocal);
// apply transform to bring solid into document/world space
// only apply once to avoid double-transform bugs
solid = DB.SolidUtils.CreateTransformed(solid, accumulatedTransform);
}
collections.Solids.Add(solid);
break;
case DB.Mesh mesh:
if (worldToLocal is not null)
if (accumulatedTransform != null)
{
mesh = mesh.get_Transformed(worldToLocal);
// apply accumulated transform to mesh
// prevents geometry from being incorrectly transformed later [Ref: CNX-2875]
mesh = mesh.get_Transformed(accumulatedTransform);
}
collections.Meshes.Add(mesh);
break;
// curves, polylines, and points are transformed to world space in ProcessGeometryCollections,
// not here, because they cannot be proxified like meshes.
case DB.Curve curve:
// curves are stored as-is; transforms are applied later in ProcessGeometryCollections
collections.Curves.Add(curve);
break;
case DB.PolyLine polyline:
// polylines also handled later during display value processing
collections.Polylines.Add(polyline);
break;
case DB.Point point:
// points remain in local space; transformed later if needed
collections.Points.Add(point);
break;
case DB.GeometryInstance instance:
// element transforms should not be carried down into nested geometryInstances.
// Nested geomInstances should have their geom retrieved with GetInstanceGeom, not GetSymbolGeom
if (worldToLocal == null) //see remark on method for why this is safe to do...
{
SortGeometry(element, collections, instance.GetInstanceGeometry(), null);
}
else
{
SortGeometry(element, collections, instance.GetSymbolGeometry(), null);
}
// GeometryInstance.Transform: symbol → parent coordinate system
// multiply with accumulatedTransform to handle nested instances
var instanceTransform = instance.Transform;
var nextTransform =
accumulatedTransform != null ? accumulatedTransform.Multiply(instanceTransform) : instanceTransform;
// always use symbol geometry, never GetInstanceGeometry() [Ref: CNX-2875]
SortGeometry(element, collections, instance.GetSymbolGeometry(), nextTransform);
break;
case DB.GeometryElement geometryElement:
SortGeometry(element, collections, geometryElement, null);
// raw GeometryElement: it has no transform of its own
// pass accumulatedTransform from parent if present
SortGeometry(element, collections, geometryElement, accumulatedTransform);
break;
}
}
@@ -500,6 +498,26 @@ public sealed class DisplayValueExtractor
return currentOptions;
}
// cable trays (and fittings) are MEP system families whose geometry detail is effectively view-driven.
// So, we've seen that, Options.DetailLevel is ignored by get_Geometry() for these categories unless a View is
// explicitly supplied, and Revit will always return a medium-detail representation otherwise [Ref: CNX-2735]
// We force extraction through the active view here (if there is one!)
if (
elementBuiltInCategory == DB.BuiltInCategory.OST_CableTray
|| elementBuiltInCategory == DB.BuiltInCategory.OST_CableTrayFitting
)
{
try
{
return new DB.Options { View = _converterSettings.Current.Document.NotNull().ActiveView };
}
catch (Exception ex) when (!ex.IsFatal())
{
// linked docs or invalid view context fall back to non-view-specific options
return currentOptions;
}
}
// NOTE: On steel elements. This is an incomplete solution.
// If steel element proxies will be sucked in via category selection, and they are not visible in the current view, they will not be extracted out.
// I'm inclined to go with this as a semi-permanent limitation. See:
@@ -552,7 +570,7 @@ public sealed class DisplayValueExtractor
{
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
SortGeometry(rebar, collections, geometryElements, null);
return ProcessGeometryCollections(rebar, collections, documentToWorld);
return ProcessGeometryCollections(rebar, collections, documentToWorld, null);
}
// Return empty list if no geometry is found - imo not critical
@@ -593,20 +611,11 @@ public sealed class DisplayValueExtractor
)
);
}
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
List<DisplayValueResult> displayValue = new();
foreach (var curve in curves)
{
if (documentToWorld is not null)
{
using var transformedCurve = curve.CreateTransformed(documentToWorld);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
return displayValue;
@@ -624,7 +633,7 @@ public sealed class DisplayValueExtractor
{
var collections = GetSortedGeometryFromElement(element, null, null);
// pass null for transform - curves are already in correct document coordinates
return ProcessGeometryCollections(element, collections, null);
return ProcessGeometryCollections(element, collections, null, null);
}
/// <summary>
@@ -633,9 +642,10 @@ public sealed class DisplayValueExtractor
/// and reduce the risk of parameter ordering errors.
/// </summary>
/// <remarks>
/// <see cref="Solids"/> and <see cref="Meshes"/> are transformed to local coordinate space in SortGeometry.
/// <see cref="Solids"/> and <see cref="Meshes"/> are transformed to symbol space in SortGeometry.
/// <see cref="Curves"/>, <see cref="Polylines"/>, and <see cref="Points"/> remain in their original coordinate space
/// and are transformed to world space during processing in ProcessGeometryCollections.
/// and receive only the instance transform (if any) in ProcessGeometryCollections - reference point
/// transform is handled by the point converters during conversion.
/// </remarks>
private sealed record GeometryCollections
{
@@ -61,6 +61,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveArrArrayToSpecklePolycurveConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveArrayConversionToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveConversionToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveOriginToPlaneConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\EllipseToSpeckleConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\HermiteSplineToSpeckleConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\LineConversionToSpeckle.cs" />
@@ -2,6 +2,7 @@ using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects.Data;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Extensions;
@@ -10,11 +11,14 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
/// <summary>
/// Converts local to global maps to direct shapes.
/// Spirit of the LocalToGlobalMap, we can't pass that object directly here bc it lives in Connectors.Common which I (ogu) don't want to bother with it.
/// All this is poc that should be burned, once we enable proper block support to revit.
/// When atomicObject comes from an InstanceProxy displayValue, parentDataObject
/// provides the original DataObject's metadata (category, name) for semantic preservation.
/// </summary>
public class LocalToGlobalToDirectShapeConverter
: ITypedConverter<(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix), DB.DirectShape>
: ITypedConverter<
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject),
DB.DirectShape
>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<(Matrix4x4 matrix, string units), DB.Transform> _transformConverter;
@@ -28,22 +32,13 @@ public class LocalToGlobalToDirectShapeConverter
_transformConverter = transformConverter;
}
public DB.DirectShape Convert((Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix) target)
public DB.DirectShape Convert(
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject) target
)
{
// 1- set ds category
// NOTE: previously, builtInCategory was on the atomicObject level. this was subsequently moved to properties
string? category = null;
// NOTE: no longer limited to DataObject since the introduction of mapper
// The change from `if (target.atomicObject is DataObject dataObject)` is very hacky, but nothing else to do for now
// TODO: better define prop interfaces for different applications
if (
target.atomicObject["properties"] is Dictionary<string, object?> properties
&& properties.TryGetValue("builtInCategory", out var builtInCategory)
)
{
category = builtInCategory?.ToString();
}
var category = ExtractBuiltInCategory(target.parentDataObject, target.atomicObject);
var name = target.parentDataObject?.name ?? target.atomicObject.TryGetName();
var dsCategory = DB.BuiltInCategory.OST_GenericModel;
if (category is not null)
@@ -62,10 +57,6 @@ public class LocalToGlobalToDirectShapeConverter
// 2 - init DirectShape
var result = DB.DirectShape.CreateElement(_converterSettings.Current.Document, new DB.ElementId(dsCategory));
// NOTE: this should technically be in a property extraction class / helper method
// This change is localised to [CNX-1825](https://linear.app/speckle/issue/CNX-1825/set-directshape-name)
// TODO: Property extraction is a greater conversation which needs to be had: [CNX-1830](https://linear.app/speckle/issue/CNX-1830/data-exchange-investigations)
var name = target.atomicObject.TryGetName();
if (name is not null)
{
result.SetName(name);
@@ -121,4 +112,24 @@ public class LocalToGlobalToDirectShapeConverter
result.SetShape(transformedGeometries);
return result;
}
private static string? ExtractBuiltInCategory(DataObject? parentDataObject, Base atomicObject)
{
// Try parent DataObject first (for InstanceProxy displayValue case)
if (parentDataObject?.properties.TryGetValue("builtInCategory", out var cat) == true)
{
return cat?.ToString();
}
// Fallback to atomicObject properties
if (
atomicObject["properties"] is Dictionary<string, object?> props
&& props.TryGetValue("builtInCategory", out var fallbackCat)
)
{
return fallbackCat?.ToString();
}
return null;
}
}
@@ -1,3 +1,4 @@
using Autodesk.Revit.DB;
using Speckle.Converters.Common;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Settings;
@@ -80,24 +81,45 @@ public class ClassPropertiesExtractor
// get room id if applicable (only for FamilyInstance elements)
if (familyInstance.Room is not null)
{
elementProperties.Add("roomId", familyInstance.Room.Id.ToString());
elementProperties.Add("roomApplicationId", familyInstance.Room.UniqueId.ToString());
}
// get space id if applicable (only for FamilyInstance elements)
if (familyInstance.Space is not null)
{
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
elementProperties.Add("spaceApplicationId", familyInstance.Space.UniqueId.ToString());
}
// get toRoom and fromRoom for FamilyInstance elements with those properties (e.g. Doors)
if (familyInstance.ToRoom is not null)
{
elementProperties.Add("toRoomId", familyInstance.ToRoom.Id.ToString());
elementProperties.Add("toRoomApplicationId", familyInstance.ToRoom.UniqueId.ToString());
}
if (familyInstance.FromRoom is not null)
{
elementProperties.Add("fromRoomId", familyInstance.FromRoom.Id.ToString());
elementProperties.Add("fromRoomApplicationId", familyInstance.FromRoom.UniqueId.ToString());
}
Element? parent = null;
#if REVIT2023_OR_GREATER
BuiltInCategory bic = familyInstance.Category.BuiltInCategory;
#else
// Cast for 2022 and older
BuiltInCategory bic = (BuiltInCategory)familyInstance.Category.Id.IntegerValue;
#endif
if (bic == BuiltInCategory.OST_CurtainWallMullions || bic == BuiltInCategory.OST_CurtainWallPanels)
{
parent = familyInstance.Host;
}
parent ??= familyInstance.SuperComponent;
if (parent != null)
{
elementProperties.Add("parentApplicationId", parent.UniqueId);
}
}
catch (Exception e) when (!e.IsFatal())
@@ -1,6 +1,5 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Objects.Primitive;
@@ -10,34 +9,34 @@ public class ArcToSpeckleConverter : ITypedConverter<DB.Arc, SOG.Arc>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
private readonly ScalingServiceToSpeckle _scalingService;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
public ArcToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
ScalingServiceToSpeckle scalingService
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter
)
{
_converterSettings = converterSettings;
_xyzToPointConverter = xyzToPointConverter;
_planeConverter = planeConverter;
_scalingService = scalingService;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
}
public SOG.Arc Convert(DB.Arc target)
{
// Revit arcs are always counterclockwise in the arc normal direction. This aligns with Speckle arc plane convention.
var arcPlane = DB.Plane.CreateByOriginAndBasis(target.Center, target.XDirection, target.YDirection);
DB.XYZ start = target.Evaluate(0, true);
DB.XYZ end = target.Evaluate(1, true);
DB.XYZ mid = target.Evaluate(0.5, true);
return new SOG.Arc()
{
plane = _planeConverter.Convert(arcPlane),
plane = _curveOriginToPlaneConverter.Convert(
(target.Center, target.XDirection, target.YDirection, target.Normal)
),
units = _converterSettings.Current.SpeckleUnits,
endPoint = _xyzToPointConverter.Convert(end),
startPoint = _xyzToPointConverter.Convert(start),
@@ -9,17 +9,20 @@ public class BoundingBoxXYZToSpeckleConverter : ITypedConverter<DB.BoundingBoxXY
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
public BoundingBoxXYZToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter
)
{
_converterSettings = converterSettings;
_xyzToPointConverter = xyzToPointConverter;
_planeConverter = planeConverter;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
}
public SOG.Box Convert(DB.BoundingBoxXYZ target)
@@ -30,21 +33,19 @@ public class BoundingBoxXYZToSpeckleConverter : ITypedConverter<DB.BoundingBoxXY
// get the base plane of the bounding box from the transform
var transform = target.Transform;
var plane = DB.Plane.CreateByOriginAndBasis(
transform.Origin,
transform.BasisX.Normalize(),
transform.BasisY.Normalize()
);
var box = new SOG.Box()
// assemble components for getting origin plane
var xDir = transform.BasisX.Normalize();
var yDir = transform.BasisY.Normalize();
var normal = xDir.CrossProduct(yDir).Normalize();
return new SOG.Box()
{
xSize = new Interval { start = min.x, end = max.x },
ySize = new Interval { start = min.y, end = max.y },
zSize = new Interval { start = min.z, end = max.z },
plane = _planeConverter.Convert(plane),
plane = _curveOriginToPlaneConverter.Convert((transform.Origin, xDir, yDir, normal)),
units = _converterSettings.Current.SpeckleUnits
};
return box;
}
}
@@ -8,33 +8,32 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
public class CircleToSpeckleConverter : ITypedConverter<DB.Arc, SOG.Circle>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
private readonly ScalingServiceToSpeckle _scalingService;
public CircleToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter,
ScalingServiceToSpeckle scalingService
)
{
_converterSettings = converterSettings;
_planeConverter = planeConverter;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
_scalingService = scalingService;
}
public SOG.Circle Convert(DB.Arc target)
{
public SOG.Circle Convert(DB.Arc target) =>
// POC: should we check for arc of 360 and throw? Original CircleToSpeckle did not do this.
// see https://forums.autodesk.com/t5/revit-api-forum/how-to-retrieve-startangle-and-endangle-of-arc-object/td-p/7637128
var arcPlane = DB.Plane.CreateByNormalAndOrigin(target.Normal, target.Center);
var c = new SOG.Circle()
new()
{
plane = _planeConverter.Convert(arcPlane),
plane = _curveOriginToPlaneConverter.Convert(
(target.Center, target.XDirection, target.YDirection, target.Normal)
),
radius = _scalingService.ScaleLength(target.Radius),
units = _converterSettings.Current.SpeckleUnits
};
return c;
}
}
@@ -0,0 +1,61 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Settings;
namespace Speckle.Converters.RevitShared.ToSpeckle;
/// <summary>
/// Converts curve origin plane components directly to Speckle plane.
/// </summary>
/// <remarks>
/// <para>
/// Revit's <see cref="DB.Plane.CreateByOriginAndBasis"/> fails when the origin is ~10+ km from the internal origin,
/// even though the documented limit is 16 km. This is a known API limitation:
/// https://forums.autodesk.com/t5/revit-api-forum/the-input-point-lies-outside-of-revit-design-limits/td-p/12689066
///</para>
/// <para>
/// This converter bypasses Revit plane creation entirely and builds Speckle planes directly from
/// the curve's origin and basis vectors.
/// </para>
/// </remarks>
public class CurveOriginToPlaneConverter
: ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<DB.XYZ, SOG.Vector> _xyzToVectorConverter;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
public CurveOriginToPlaneConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<DB.XYZ, SOG.Vector> xyzToVectorConverter,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter
)
{
_converterSettings = converterSettings;
_xyzToPointConverter = xyzToPointConverter;
_xyzToVectorConverter = xyzToVectorConverter;
_planeConverter = planeConverter;
}
public SOG.Plane Convert((DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal) target)
{
// within limits? then use standard Revit plane creation
if (DB.XYZ.IsWithinLengthLimits(target.origin))
{
using var revitPlane = DB.Plane.CreateByOriginAndBasis(target.origin, target.xDir, target.yDir);
return _planeConverter.Convert(revitPlane);
}
// beyond limits? then build Speckle plane directly
return new SOG.Plane
{
origin = _xyzToPointConverter.Convert(target.origin),
xdir = _xyzToVectorConverter.Convert(target.xDir),
ydir = _xyzToVectorConverter.Convert(target.yDir),
normal = _xyzToVectorConverter.Convert(target.normal),
units = _converterSettings.Current.SpeckleUnits
};
}
}
@@ -9,40 +9,40 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
public class EllipseToSpeckleConverter : ITypedConverter<DB.Ellipse, SOG.Ellipse>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
private readonly ITypedConverter<
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
SOG.Plane
> _curveOriginToPlaneConverter;
private readonly ScalingServiceToSpeckle _scalingService;
public EllipseToSpeckleConverter(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter,
ScalingServiceToSpeckle scalingService
)
{
_converterSettings = converterSettings;
_planeConverter = planeConverter;
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
_scalingService = scalingService;
}
public SOG.Ellipse Convert(DB.Ellipse target)
{
using (DB.Plane basePlane = DB.Plane.CreateByOriginAndBasis(target.Center, target.XDirection, target.YDirection))
{
var trim = target.IsBound
? new Interval { start = target.GetEndParameter(0), end = target.GetEndParameter(1) }
: null;
var trim = target.IsBound
? new Interval { start = target.GetEndParameter(0), end = target.GetEndParameter(1) }
: null;
return new SOG.Ellipse()
{
plane = _planeConverter.Convert(basePlane),
// POC: scale length correct? seems right?
firstRadius = _scalingService.ScaleLength(target.RadiusX),
secondRadius = _scalingService.ScaleLength(target.RadiusY),
// POC: original EllipseToSpeckle() method was setting this twice
domain = Interval.UnitInterval,
trimDomain = trim,
length = _scalingService.ScaleLength(target.Length),
units = _converterSettings.Current.SpeckleUnits
};
}
return new SOG.Ellipse()
{
plane = _curveOriginToPlaneConverter.Convert(
(target.Center, target.XDirection, target.YDirection, target.Normal)
),
firstRadius = _scalingService.ScaleLength(target.RadiusX),
secondRadius = _scalingService.ScaleLength(target.RadiusY),
domain = Interval.UnitInterval,
trimDomain = trim,
length = _scalingService.ScaleLength(target.Length),
units = _converterSettings.Current.SpeckleUnits
};
}
}
@@ -1,10 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Utils;
using Speckle.Connectors.DUI.Settings;
using Speckle.Connectors.Logging;
using Speckle.Sdk;
using Speckle.Sdk.SQLite;
namespace Speckle.Connectors.DUI.Bindings;
@@ -15,26 +12,10 @@ namespace Speckle.Connectors.DUI.Bindings;
/// ['Rhino', serialised config]
/// ['Revit', serialised config]
/// </summary>
public class ConfigBinding : IBinding
public class ConfigBinding(IConfigStore configStore, IBrowserBridge bridge) : IBinding
{
public string Name => "configBinding";
public IBrowserBridge Parent { get; }
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
private readonly ISpeckleApplication _speckleApplication;
private readonly IJsonSerializer _serializer;
public ConfigBinding(
IJsonSerializer serializer,
ISpeckleApplication speckleApplication,
IBrowserBridge bridge,
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory
)
{
Parent = bridge;
_jsonCacheManager = sqLiteJsonCacheManagerFactory.CreateForUser("DUI3Config"); // POC: maybe inject? (if we ever want to use a different storage for configs later down the line)
_speckleApplication = speckleApplication;
_serializer = serializer;
}
public IBrowserBridge Parent { get; } = bridge;
#pragma warning disable CA1024
public bool GetIsDevMode()
@@ -47,176 +28,32 @@ public class ConfigBinding : IBinding
#endif
}
public ConnectorConfig GetConfig()
{
var rawConfig = _jsonCacheManager.GetObject(_speckleApplication.HostApplication);
if (rawConfig is null)
{
return SeedConfig();
}
public ConnectorConfig GetConfig() => configStore.GetConnectorConfig();
try
{
var config = _serializer.Deserialize<ConnectorConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize config");
}
return config;
}
catch (SerializationException)
{
return SeedConfig();
}
}
private ConnectorConfig SeedConfig()
{
var cfg = new ConnectorConfig();
UpdateConfig(cfg);
return cfg;
}
public void UpdateConfig(ConnectorConfig config)
{
var str = _serializer.Serialize(config);
_jsonCacheManager.UpdateObject(_speckleApplication.HostApplication, str);
}
public void UpdateConfig(ConnectorConfig config) => configStore.UpdateConnectorConfig(config);
public void SetUserSelectedAccountId(string userSelectedAccountId)
{
var str = _serializer.Serialize(new AccountsConfig() { UserSelectedAccountId = userSelectedAccountId });
_jsonCacheManager.UpdateObject("accounts", str);
var config = new AccountsConfig() { UserSelectedAccountId = userSelectedAccountId };
configStore.UpdateAccountConfig(config);
}
// TODO: need to be replaced with `GetAccountsConfig` function after some amount of time to not confuse ourselves.
public AccountsConfig? GetUserSelectedAccountId()
{
var rawConfig = _jsonCacheManager.GetObject("accounts");
if (rawConfig is null)
{
return null;
}
public AccountsConfig? GetUserSelectedAccountId() => GetAccountsConfig();
try
{
var config = _serializer.Deserialize<AccountsConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize accounts config");
}
public GlobalConfig? GetGlobalConfig() => configStore.GetGlobalConfig();
return config;
}
catch (SerializationException)
{
return null;
}
}
public GlobalConfig? GetGlobalConfig()
{
var rawConfig = _jsonCacheManager.GetObject("global");
if (rawConfig is null)
{
return null;
}
try
{
var config = _serializer.Deserialize<GlobalConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize global config");
}
return config;
}
catch (SerializationException)
{
return null;
}
}
public AccountsConfig? GetAccountsConfig()
{
var rawConfig = _jsonCacheManager.GetObject("accounts");
if (rawConfig is null)
{
return null;
}
try
{
var config = _serializer.Deserialize<AccountsConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize accounts config");
}
return config;
}
catch (SerializationException)
{
return null;
}
}
public AccountsConfig? GetAccountsConfig() => configStore.GetAccountsConfig();
public void SetUserSelectedWorkspaceId(string workspaceId)
{
var str = _serializer.Serialize(new WorkspacesConfig() { UserSelectedWorkspaceId = workspaceId });
_jsonCacheManager.UpdateObject("workspaces", str);
var config = new WorkspacesConfig() { UserSelectedWorkspaceId = workspaceId };
configStore.UpdateWorkspacesConfig(config);
}
public WorkspacesConfig? GetWorkspacesConfig()
{
var rawConfig = _jsonCacheManager.GetObject("workspaces");
if (rawConfig is null)
{
return null;
}
try
{
var config = _serializer.Deserialize<WorkspacesConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize workspaces config");
}
return config;
}
catch (SerializationException)
{
return null;
}
}
public WorkspacesConfig? GetWorkspacesConfig() => configStore.GetWorkspacesConfig();
[SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Expose to UI")]
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Expose to UI")]
public string GetSessionId() => Consts.StaticSessionId;
}
/// <summary>
/// POC: A simple POCO for keeping track of settings. I see this as extensible in the future by each host application if and when we will need global per-app connector settings.
/// </summary>
public class ConnectorConfig
{
public bool DarkTheme { get; set; } = true;
}
public class GlobalConfig
{
public bool IsUpdateNotificationDisabled { get; set; }
}
public class AccountsConfig
{
public string? UserSelectedAccountId { get; set; }
}
public class WorkspacesConfig
{
public string? UserSelectedWorkspaceId { get; set; }
}
@@ -3,6 +3,12 @@ using Speckle.InterfaceGenerator;
namespace Speckle.Connectors.DUI.Bridge;
/// <remarks>
/// This class was initially designed as an evolution
/// of hostapp specific idle managers, since they followed a similar logic.
/// However, has ended up a little over-engineered, so since then, for Revit connector
/// we've started to prefer a simpler solution that fits only the needs of said host app.
/// </remarks>
//should be registered as singleton
[GenerateAutoInterface]
public sealed class IdleCallManager(ITopLevelExceptionHandler topLevelExceptionHandler) : IIdleCallManager
@@ -14,6 +14,7 @@ namespace Speckle.Connectors.DUI.Models;
public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJsonSerializer serializer)
: IDocumentModelStore
{
public event EventHandler<ModelCardsChangedEventArgs>? ModelCardsChanged;
private readonly List<ModelCard> _models = new();
/// <summary>
@@ -152,6 +153,7 @@ public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJs
{
var state = Serialize();
HostAppSaveState(state);
ModelCardsChanged?.Invoke(this, new ModelCardsChangedEventArgs(_models.ToList().AsReadOnly()));
}
}
@@ -0,0 +1,8 @@
using Speckle.Connectors.DUI.Models.Card;
namespace Speckle.Connectors.DUI.Models;
public sealed class ModelCardsChangedEventArgs(IReadOnlyList<ModelCard> modelCards) : EventArgs
{
public IReadOnlyList<ModelCard> ModelCards { get; } = modelCards;
}
@@ -0,0 +1,193 @@
using System.Runtime.Serialization;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Utils;
using Speckle.InterfaceGenerator;
using Speckle.Sdk;
using Speckle.Sdk.SQLite;
namespace Speckle.Connectors.DUI.Settings;
/// <summary>
/// See <see cref="ConfigBinding"/>, as it was driving Dim nuts he couldn't swap to a dark theme.
/// How does it store configs? In a sqlite db called 'DUI3Config', we create a row for each host application:
/// [ hash, contents ]
/// ['Rhino', serialised config]
/// ['Revit', serialised config]
/// </summary>
/// <remarks>
/// We separated the business logic that's in this class from the <see cref="ConfigBinding"/> so that
/// <see cref="ConfigStore"/> can be injected into other bindings (you can't inject one binding into another)
/// </remarks>
[GenerateAutoInterface]
public sealed class ConfigStore : IConfigStore
{
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
private readonly ISpeckleApplication _speckleApplication;
private readonly IJsonSerializer _serializer;
public ConfigStore(
IJsonSerializer serializer,
ISpeckleApplication speckleApplication,
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory
)
{
_jsonCacheManager = sqLiteJsonCacheManagerFactory.CreateForUser("DUI3Config"); // POC: maybe inject? (if we ever want to use a different storage for configs later down the line)
_speckleApplication = speckleApplication;
_serializer = serializer;
}
public ConnectorConfig GetConnectorConfig()
{
var rawConfig = _jsonCacheManager.GetObject(_speckleApplication.HostApplication);
if (rawConfig is null)
{
return SeedConnectorConfig();
}
try
{
var config = _serializer.Deserialize<ConnectorConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize config");
}
return config;
}
catch (SerializationException)
{
return SeedConnectorConfig();
}
}
private ConnectorConfig SeedConnectorConfig()
{
var cfg = new ConnectorConfig();
UpdateConnectorConfig(cfg);
return cfg;
}
public void UpdateConnectorConfig(ConnectorConfig config)
{
var str = _serializer.Serialize(config);
_jsonCacheManager.UpdateObject(_speckleApplication.HostApplication, str);
}
public void UpdateAccountConfig(AccountsConfig accountsConfig)
{
var str = _serializer.Serialize(accountsConfig);
_jsonCacheManager.UpdateObject("accounts", str);
}
public GlobalConfig? GetGlobalConfig()
{
var rawConfig = _jsonCacheManager.GetObject("global");
if (rawConfig is null)
{
return null;
}
try
{
var config = _serializer.Deserialize<GlobalConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize global config");
}
return config;
}
catch (SerializationException)
{
return null;
}
}
public AccountsConfig? GetAccountsConfig()
{
var rawConfig = _jsonCacheManager.GetObject("accounts");
if (rawConfig is null)
{
return null;
}
try
{
var config = _serializer.Deserialize<AccountsConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize accounts config");
}
return config;
}
catch (SerializationException)
{
return null;
}
}
public WorkspacesConfig? GetWorkspacesConfig()
{
var rawConfig = _jsonCacheManager.GetObject("workspaces");
if (rawConfig is null)
{
return null;
}
try
{
var config = _serializer.Deserialize<WorkspacesConfig>(rawConfig);
if (config is null)
{
throw new SerializationException("Failed to deserialize workspaces config");
}
return config;
}
catch (SerializationException)
{
return null;
}
}
public void UpdateWorkspacesConfig(WorkspacesConfig workspacesConfig)
{
var str = _serializer.Serialize(workspacesConfig);
_jsonCacheManager.UpdateObject("workspaces", str);
}
}
/// <summary>
/// POC: A simple POCO for keeping track of settings. I see this as extensible in the future by each host application if and when we will need global per-app connector settings.
/// </summary>
public sealed class ConnectorConfig
{
public bool DarkTheme { get; init; } = true;
/// <remarks>
/// Only used by Revit Connector !!
/// We're exposing some settings to disable event listening inorder to debug app crash issues caused by Revit event handlers
/// Normal users are expected to have both enabled.
/// We can consider removing this in future once issues are resolved.
/// </remarks>
public bool SelectionChangeListeningDisabled { get; init; }
/// <inheritdoc cref="SelectionChangeListeningDisabled" />
public bool DocumentChangeListeningDisabled { get; init; }
}
public sealed class GlobalConfig
{
public bool IsUpdateNotificationDisabled { get; init; }
}
public sealed class AccountsConfig
{
public string? UserSelectedAccountId { get; init; }
}
public sealed class WorkspacesConfig
{
public string? UserSelectedWorkspaceId { get; init; }
}
@@ -41,7 +41,12 @@ internal sealed class RhinoJobHandler(
ingestionId: ingestion.id,
projectId: job.Payload.ProjectId,
progressMessage: "Starting Up Importer",
sourceData: new(application.Slug, application.HostApplicationVersion, file.FileInfo.Name, file.FileInfo.Length)
sourceData: new(
handlerApplication.Slug,
application.HostApplicationVersion,
job.Payload.FileName,
file.FileInfo.Length
)
),
cancellationToken
);
@@ -97,17 +97,14 @@ public class SendOperationTests : MoqTest
var commitObject = new TestBase();
var projectId = "projectId";
var modelId = "modelId";
var url = new Uri("https://localhost");
var token = "token";
var sourceApplication = "sourceApplication";
var account = new Account()
{
userInfo = new UserInfo() { email = "test_user@example.com" },
serverInfo = new ServerInfo() { url = url.ToString() },
token = token
};
var sendInfo = new SendInfo(account, projectId, modelId, sourceApplication);
var progress = Create<IProgress<CardProgress>>(MockBehavior.Loose);
var ct = new CancellationToken();