Compare commits

..

8 Commits

Author SHA1 Message Date
Oğuzhan Koral 0b246cf95c Merge pull request #1193 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
2025-11-18 15:50:22 +03:00
Jonathon Broughton 003e310089 Adds room ID properties for family instances (#1192)
Introduces extraction of 'toRoomId' and 'fromRoomId' for family instance elements, enhancing the representation of relationships between doors and associated rooms.

Improves data completeness for elements with room associations.
2025-11-18 12:44:40 +00:00
Oğuzhan Koral 5917530761 Add account via binding (#1191) 2025-11-18 13:26:19 +03:00
Claire Kuang 349009314b feat(rhino/revit): publishes views (#1160)
* adds view proxies and converter

* adds rhino views

* updates with new proxy field

* adds views to revit send

* updates views to not include parallel projections

also adds rootkeys to connectors common

* updates to 3.8.0 sdk

* only add views if any
2025-11-17 17:50:41 +00:00
Jonathon Broughton 659a29a294 Improves property value conversion and extraction (#1183)
* Refactors property extraction to use model units

Uses the model's units when extracting properties to
ensure consistency and accuracy of converted values.

Extracts property sets as a static function to provide
re-usability without the class instance.

* Refactors Revit category extraction

Improves Revit category extraction by utilizing UI units for property conversion, ensuring consistent unit handling.

Additionally, refactors the extractor to a static class.

* Improves property conversion and handling

Introduces robust property conversion and handling logic.

Leverages user interface units for length, area, and volume property conversions,
ensuring consistency with the Navisworks UI.

Enhances property data handling by using dictionaries to represent
numerical properties with associated units, providing more context
for downstream applications.

Adds property name sanitization to ensure compatibility with
Speckle's object model.

* Removes the unused `Speckle.Converter.Navisworks.Helpers` import from the PropertySetsExtractor class to reduce clutter and improve code maintainability.

Relates to CNX-2784

* Standardizes numerical property dictionary creation.

Simplifies the creation of numerical property dictionaries
by removing the `internalDefinitionName` parameter from the
`NumObj` method. This ensures a consistent format for numerical
properties across the connector.

Relates to CNX-2784

* Refactors property conversion to service

Moves property conversion logic into a dedicated service.

This improves code organization and testability and allows
to reuse logic and manage UI units consistently.

* Refactors property conversion for consistency

Standardizes property conversion by introducing a dedicated `IPropertyConverter` service.

This change ensures consistent handling of property values across different extractors,
improving data accuracy and reducing inconsistencies in quantity extraction.
It also adds resets of the property converter to ensure clean conversions for each item.

Relates to CNX-2784

* Update Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/PropertyHelpers.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/PropertyHelpers.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Converters/Navisworks/Speckle.Converters.NavisworksShared/Helpers/PropertyHelpers.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Moves UI Units cache to service

Moves the UI Units cache logic from the helpers to a dedicated service.

This improves the separation of concerns and makes the code more maintainable and testable.

Relates to CNX-2784

* Fixes unit label typos.

Corrects minor spelling errors in the unit labels for
centimeters, millimeters, micrometers, and kilometers.

Relates to CNX-2784

* Ensures correct units are used on send

Uses the UI Units Cache service to ensure the correct
units are being applied to objects when sending them to Speckle.

Relates to CNX-2784

* Enhances the property helper functionality to support additional features. Adjusts the constructor parameters to accommodate new requirements.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 20:42:52 +03:00
Jonathon Broughton 1ffeddbc2c Implements geometry instancing and optimization in Navisworks connector (#1188)
* Introduces shared geometry store service

Creates a singleton service for deduplicating geometry during conversion.

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

It uses a dictionary and a hash set to optimize lookup and storage, and includes thread safety mechanisms.

* Adds instance store manager for geometry

Introduces an instance store manager to handle shared geometry, which includes separate stores for geometry definitions and instance proxies.

This change prepares for the implementation of an instancing pattern compatible with .NET Framework.
It also registers settings from a factory to resolve conversion settings.

* Adds shared project file for Navisworks converter

Introduces a shared project file for the Navisworks converter, promoting code reuse and consistency across different Navisworks converter implementations.

This file includes common data extractors, data handlers, helpers, services, and settings, reducing duplication and improving maintainability.

* Implements geometry instancing for Navisworks

Adds geometry instancing to improve performance and reduce data size when converting Navisworks models with shared geometry.

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

Improves handling of COM array data for fragment ID generation and includes comprehensive logging for debugging instancing issues.

* Introduces instance store management

Adds a service to manage shared geometry and instance definition proxies.

This system uses two stores for geometry definitions and their instance proxies,
facilitating efficient handling of shared geometry. It provides methods for
adding, retrieving, and clearing geometries, ensuring deduplication and
optimized memory usage.

* Refactors display value extraction

Streamlines display value extraction by injecting the geometry converter.

This change simplifies the DisplayValueExtractor by removing its dependencies on settings and logging.
It now directly uses a GeometryToSpeckleConverter instance, which is passed in, for converting model item geometry.

* Adds instance geometry support to Navisworks connector

Introduces instance geometry handling for more efficient and accurate Navisworks data streams.

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

Addresses issues with duplicate geometry and improves stream performance.

* Fixes geometry processing for instance handling

Refactors geometry processing logic to handle instances more efficiently.
It now correctly applies transformations for single objects, ensuring accurate placement in the scene.
Simplifies processing of transforms where necessary for single objects.
Removes redundant logging.

* LFG!!!

Matrix4x4 Transposed
Adds an application ID to instance references.

* Fixes RenderMaterials

* Refactors dependency injection for settings

* Reduces comment clutter in Navisworks converters

Removes unnecessary comments to enhance code readability
and maintainability.

Simplifies the logic flow by eliminating obsolete comments
that do not provide value, promoting a cleaner codebase
moving forward.

Relates to issue jonathon/cnx-2817-adopt-displayvalue-proxification-pattern-in-navisworks.

* Renames methods for clarity and updates logic

Refactors method names to improve readability and understanding of functionality.

Consolidates selection logic related to representation modes and enhances the cohesiveness of the code.

Also updates the property extraction method for better clarity on its purpose.

Relates to the adoption of the display-value proxification pattern.

* Refines COM object management and method naming

Improves memory safety by ensuring all COM objects are explicitly released in try-finally blocks.

* Refactors constants usage for geometry identification

Introduces new constant definitions to standardise fragment ID prefixing.

* 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 geometry retrieval and checks

Refactors geometry definition retrieval for better readability.

* Refactors property name sanitisation logic

Consolidates the logic for sanitising property names into a more concise format.

* Refactors method signature for clarity

* Update Converters/Navisworks/Speckle.Converters.NavisworksShared/Services/InstanceStoreManager.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enhances fragment processing logic and error handling

Improves null handling for selection paths to prevent crashes.

* Changes fragment ID method visibility to public

Updates the visibility of the fragment ID generation method to public, allowing it to be accessed from outside the class.

* implementing more robust error handling during COM
interop operations.

* Updates method signatures to replace base geometry types with shared models, ensuring consistent handling of geometry data.

* Refactors parameter list for clarity

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2025-11-17 19:37:02 +03:00
Björn Steinhagen 9d9a27d9cb fix: bake transforms into curves/polylines/points for family instances (#1190)
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2025-11-17 16:02:58 +00:00
Björn Steinhagen b2a885c193 fix(grasshopper): dispayValue proxies broke grasshopper load (#1187)
* fix: poc

* refactor: consolidating poc

* fix: tweaking through testing

* fix: async component

* fix: ci

* chore: is null and is not null form

* refactor: using NotNull()

* chore: csharpier

* refactor: ogu bey
2025-11-17 17:52:56 +02:00
35 changed files with 1144 additions and 378 deletions
@@ -17,7 +17,12 @@ public class NavisworksColorUnpacker(
)
{
private static T SelectByRepresentationMode<T>(
RepresentationMode mode, T active, T permanent, T original, T defaultValue) =>
RepresentationMode mode,
T active,
T permanent,
T original,
T defaultValue
) =>
mode switch
{
RepresentationMode.Active => active,
@@ -20,7 +20,12 @@ public class NavisworksMaterialUnpacker(
)
{
private static T SelectByRepresentationMode<T>(
RepresentationMode mode, T active, T permanent, T original, T defaultValue) =>
RepresentationMode mode,
T active,
T permanent,
T original,
T defaultValue
) =>
mode switch
{
RepresentationMode.Active => active,
@@ -120,9 +125,12 @@ public class NavisworksMaterialUnpacker(
}
}
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);
{ // 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 = "";
}
@@ -186,12 +194,7 @@ public class NavisworksMaterialUnpacker(
{
renderMaterialProxies[renderMaterialId.ToString()] = new RenderMaterialProxy()
{
value = CreateRenderMaterial(
materialName,
renderTransparency,
renderColor,
renderMaterialId
),
value = CreateRenderMaterial(materialName, renderTransparency, renderColor, renderMaterialId),
objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId]
};
}
@@ -216,7 +219,9 @@ 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
: $"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}",
opacity = 1 - transparency,
metalness = 0,
roughness = 1,
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Speckle.Connector.Navisworks.HostApp;
using Speckle.Connector.Navisworks.Services;
using Speckle.Connectors.Common.Builders;
@@ -27,6 +27,7 @@ public class NavisworksRootObjectBuilder(
NavisworksMaterialUnpacker materialUnpacker,
NavisworksColorUnpacker colorUnpacker,
IElementSelectionService elementSelectionService,
IUiUnitsCache uiUnitsCache,
InstanceStoreManager instanceStoreManager
) : IRootObjectBuilder<NAV.ModelItem>
{
@@ -255,12 +256,14 @@ public class NavisworksRootObjectBuilder(
(string name, string path) = GetElementNameAndPath(convertedBase.applicationId);
var units = uiUnitsCache.Ensure();
return new NavisworksObject
{
name = name,
displayValue = convertedBase["displayValue"] as List<Base> ?? [],
properties = convertedBase["properties"] as Dictionary<string, object?> ?? [],
units = converterSettings.Current.Derived.SpeckleUnits,
units = units.ToString(),
applicationId = convertedBase.applicationId,
["path"] = path
};
@@ -58,6 +58,7 @@ public static class ServiceRegistration
serviceCollection.AddScoped<SendOperation<DocumentToConvert>>();
serviceCollection.AddScoped<ElementUnpacker>();
serviceCollection.AddScoped<LevelUnpacker>();
serviceCollection.AddScoped<ViewUnpacker>();
serviceCollection.AddScoped<SendCollectionManager>();
serviceCollection.AddScoped<IRootObjectBuilder<DocumentToConvert>, RevitRootObjectBuilder>();
serviceCollection.AddSingleton<ISendConversionCache, SendConversionCache>();
@@ -0,0 +1,87 @@
using Autodesk.Revit.DB;
using Microsoft.Extensions.Logging;
using Speckle.Objects.Other;
using Speckle.Sdk;
namespace Speckle.Connectors.Revit.HostApp;
/// <summary>
/// Unpacks Revit Views for sending
/// </summary>
public class ViewUnpacker
{
private readonly ILogger<ViewUnpacker> _logger;
private readonly Converters.Common.IRootToSpeckleConverter _rootToSpeckleConverter;
public ViewUnpacker(Converters.Common.IRootToSpeckleConverter rootToSpeckleConverter, ILogger<ViewUnpacker> logger)
{
_rootToSpeckleConverter = rootToSpeckleConverter;
_logger = logger;
}
private Camera? ConvertViewToCamera(View3D view)
{
try
{
var converted = (Camera)_rootToSpeckleConverter.Convert(view);
if (converted is null)
{
_logger.LogError("Failed to create a view from {view}", view.Name);
return null;
}
return converted;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to create a view from {view}", view.Name);
return null;
}
}
/// <summary>
/// Iterates through the 3D views in the provided document to create cameras
/// </summary>
/// <param name="doc">Document to retrieve 3D views from</param>
/// <returns></returns>
public List<Camera> Unpack(Document doc)
{
List<Camera> cameras = new();
using FilteredElementCollector collector = new(doc);
List<View> views = collector
.WhereElementIsNotElementType()
.OfCategory(BuiltInCategory.OST_Views)
.Cast<View>()
.Where(x => x.ViewType == ViewType.ThreeD)
.ToList();
foreach (View view in views)
{
if (view is not View3D view3D)
{
continue;
}
// not supporting parallel project yet, since it is too complex to match in the viewer for now
try
{
if (!view3D.IsPerspective)
{
continue;
}
}
catch (Autodesk.Revit.Exceptions.InvalidOperationException)
{
continue; // some threed views will throw an exception: returns true if view is not a view template
}
if (ConvertViewToCamera(view3D) is Camera camera)
{
cameras.Add(camera);
}
}
return cameras;
}
}
@@ -24,6 +24,7 @@ public class RevitRootObjectBuilder(
ISendConversionCache sendConversionCache,
ElementUnpacker elementUnpacker,
LevelUnpacker levelUnpacker,
ViewUnpacker viewUnpacker,
IThreadContext threadContext,
SendCollectionManager sendCollectionManager,
ILogger<RevitRootObjectBuilder> logger,
@@ -240,6 +241,7 @@ public class RevitRootObjectBuilder(
throw new SpeckleException("Failed to convert all objects.");
}
// STEP 5: Unpack proxies to attach to root collection
var flatElements = atomicObjectsByDocumentAndTransform.SelectMany(t => t.Elements).ToList();
var idsAndSubElementIds = elementUnpacker.GetElementsAndSubelementIdsFromAtomicObjects(flatElements);
@@ -260,6 +262,13 @@ public class RevitRootObjectBuilder(
}
);
// STEP 6: Unpack all other objects to attach to root collection
List<Objects.Other.Camera> views = viewUnpacker.Unpack(converterSettings.Current.Document);
if (views.Count > 0)
{
rootObject[RootKeys.VIEW] = views;
}
// NOTE: these are currently not used anywhere, we'll skip them until someone calls for it back
// rootObject[ProxyKeys.PARAMETER_DEFINITIONS] = _parameterDefinitionHandler.Definitions;
@@ -24,6 +24,7 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LevelUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LinkedModelHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitMaterialBaker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ViewUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\SupportedCategoriesUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitViewManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\HideWarningsFailuresPreprocessor.cs" />
@@ -60,4 +61,4 @@
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
</ItemGroup>
</Project>
</Project>
@@ -447,73 +447,89 @@ public sealed class ReceiveComponentWorker : WorkerInstance<ReceiveAsyncComponen
}
using var scope = PriorityLoader.CreateScopeForActiveDocument();
Root = await scope
.Get<GrasshopperReceiveOperation>()
.ReceiveCommitObject(receiveInfo, progress, CancellationToken)
.ConfigureAwait(false);
CancellationToken.ThrowIfCancellationRequested();
SpecklePropertyGroupGoo? rootPropertiesGoo = null;
if (Root is RootCollection rootCollection && rootCollection.properties.Count > 0)
try
{
rootPropertiesGoo = new SpecklePropertyGroupGoo(rootCollection.properties);
Root = await scope
.Get<GrasshopperReceiveOperation>()
.ReceiveCommitObject(receiveInfo, progress, CancellationToken)
.ConfigureAwait(false);
CancellationToken.ThrowIfCancellationRequested();
SpecklePropertyGroupGoo? rootPropertiesGoo = null;
if (Root is RootCollection rootCollection && rootCollection.properties.Count > 0)
{
rootPropertiesGoo = new SpecklePropertyGroupGoo(rootCollection.properties);
}
// Step 2 - CONVERT
//receiveComponent.Message = $"Unpacking...";
SpeckleConversionContext.SetupCurrent(scope);
var unpackedRoot = scope.Get<RootObjectUnpacker>().Unpack(Root);
// separate atomic objects from block instances
var (atomicObjects, blockInstances) = scope
.Get<RootObjectUnpacker>()
.SplitAtomicObjectsAndInstances(unpackedRoot.ObjectsToConvert);
// initialize unpackers and collection builder (data holders - created with new)
var colorUnpacker = new GrasshopperColorUnpacker(unpackedRoot);
var materialUnpacker = new GrasshopperMaterialUnpacker(unpackedRoot);
var collectionRebuilder = new GrasshopperCollectionRebuilder(
(Root as Collection) ?? new Collection { name = "unnamed" }
);
// get handler from DI and initialize with per-operation data
var mapHandler = scope
.Get<LocalToGlobalMapHandler>()
.Initialize(
scope.Get<TraversalContextUnpacker>(),
colorUnpacker,
materialUnpacker,
collectionRebuilder,
unpackedRoot.DefinitionProxies
);
// handler deals with two-pass conversion: normal objects first, then DataObjects with InstanceProxies
mapHandler.ConvertAtomicObjects(atomicObjects);
// process block instances using converted atomic objects
// internally filters out InstanceProxies that belong to registered DataObjects
// block processing needs converted objects, but object filtering needs block definitions.
mapHandler.ConvertBlockInstances(blockInstances);
Result = new SpeckleCollectionWrapperGoo(collectionRebuilder.RootCollectionWrapper);
RootProperties = rootPropertiesGoo;
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
var customProperties = new Dictionary<string, object>()
{
{ "isAsync", true },
{ "sourceHostApp", HostApplications.GetSlugFromHostAppNameAndVersion(receiveInfo.SourceApplication) },
{ "auto", Parent.AutoReceive }
};
if (receiveInfo.WorkspaceId != null)
{
customProperties.Add("workspace_id", receiveInfo.WorkspaceId);
}
if (receiveInfo.SelectedVersionUserId != null)
{
customProperties.Add(
"isMultiplayer",
receiveInfo.SelectedVersionUserId != Parent.ApiClient.Account.userInfo.id
);
}
await scope
.Get<IMixPanelManager>()
.TrackEvent(MixPanelEvents.Receive, Parent.ApiClient.Account, customProperties);
}
// Step 2 - CONVERT
//receiveComponent.Message = $"Unpacking...";
TraversalContextUnpacker traversalContextUnpacker = new();
var unpackedRoot = scope.Get<RootObjectUnpacker>().Unpack(Root);
// separate atomic objects from block instances
var (atomicObjects, blockInstances) = scope
.Get<RootObjectUnpacker>()
.SplitAtomicObjectsAndInstances(unpackedRoot.ObjectsToConvert);
// initialize unpackers and collection builder
var colorUnpacker = new GrasshopperColorUnpacker(unpackedRoot);
var materialUnpacker = new GrasshopperMaterialUnpacker(unpackedRoot);
var collectionRebuilder = new GrasshopperCollectionRebuilder(
(Root as Collection) ?? new Collection { name = "unnamed" }
);
// convert atomic objects directly
var mapHandler = new LocalToGlobalMapHandler(
traversalContextUnpacker,
collectionRebuilder,
colorUnpacker,
materialUnpacker
);
foreach (var atomicContext in atomicObjects)
finally
{
mapHandler.ConvertAtomicObject(atomicContext);
SpeckleConversionContext.EndCurrent();
}
// process block instances using converted atomic objects
// block processing needs converted objects, but object filtering needs block definitions.
mapHandler.ConvertBlockInstances(blockInstances, unpackedRoot.DefinitionProxies);
Result = new SpeckleCollectionWrapperGoo(collectionRebuilder.RootCollectionWrapper);
RootProperties = rootPropertiesGoo;
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
var customProperties = new Dictionary<string, object>()
{
{ "isAsync", true },
{ "sourceHostApp", HostApplications.GetSlugFromHostAppNameAndVersion(receiveInfo.SourceApplication) },
{ "auto", Parent.AutoReceive }
};
if (receiveInfo.WorkspaceId != null)
{
customProperties.Add("workspace_id", receiveInfo.WorkspaceId);
}
if (receiveInfo.SelectedVersionUserId != null)
{
customProperties.Add("isMultiplayer", receiveInfo.SelectedVersionUserId != Parent.ApiClient.Account.userInfo.id);
}
await scope.Get<IMixPanelManager>().TrackEvent(MixPanelEvents.Receive, Parent.ApiClient.Account, customProperties);
}
}
@@ -143,95 +143,102 @@ public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInpu
}
using var scope = PriorityLoader.CreateScopeForActiveDocument();
var clientFactory = scope.ServiceProvider.GetRequiredService<IClientFactory>();
var receiveOperation = scope.ServiceProvider.GetRequiredService<GrasshopperReceiveOperation>();
// Do the thing 👇🏼
Account? account = input.Resource.Account.GetAccount(scope);
if (account is null)
try
{
throw new SpeckleAccountManagerException("No default account was found");
var clientFactory = scope.ServiceProvider.GetRequiredService<IClientFactory>();
var receiveOperation = scope.ServiceProvider.GetRequiredService<GrasshopperReceiveOperation>();
// Do the thing 👇🏼
Account? account = input.Resource.Account.GetAccount(scope);
if (account is null)
{
throw new SpeckleAccountManagerException("No default account was found");
}
using var client = clientFactory.Create(account);
var receiveInfo = await input.Resource.GetReceiveInfo(client, cancellationToken).ConfigureAwait(false);
// store version id for tracking
_lastVersionId = receiveInfo.SelectedVersionId;
var progress = new Progress<CardProgress>(_ =>
{
// TODO: Progress only makes sense in non-blocking async receive, which is not supported yet.
// Message = $"{progress.Status}: {progress.Progress}";
});
var root = await receiveOperation
.ReceiveCommitObject(receiveInfo, progress, cancellationToken)
.ConfigureAwait(false);
// extract model-wide root properties (see cnx-2722)
SpecklePropertyGroupGoo? rootPropertiesGoo = null;
if (root is RootCollection rootCollection && rootCollection.properties.Count > 0)
{
rootPropertiesGoo = new SpecklePropertyGroupGoo(rootCollection.properties);
}
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
var customProperties = new Dictionary<string, object>
{
{ "isAsync", false },
{ "sourceHostApp", HostApplications.GetSlugFromHostAppNameAndVersion(receiveInfo.SourceApplication) }
};
if (receiveInfo.WorkspaceId != null)
{
customProperties.Add("workspace_id", receiveInfo.WorkspaceId);
}
if (receiveInfo.SelectedVersionUserId != null)
{
customProperties.Add("isMultiplayer", receiveInfo.SelectedVersionUserId != client.Account.userInfo.id);
}
var mixpanel = PriorityLoader.Container.GetRequiredService<IMixPanelManager>();
await mixpanel.TrackEvent(MixPanelEvents.Receive, account, customProperties);
// Setup conversion context BEFORE unpacking (which triggers DataObjectConverter)
SpeckleConversionContext.SetupCurrent(scope);
var rootObjectUnpacker = scope.ServiceProvider.GetService<RootObjectUnpacker>();
var unpackedRoot = rootObjectUnpacker.Unpack(root);
// split atomic objects from block components before conversion
var (atomicObjects, blockInstances) = rootObjectUnpacker.SplitAtomicObjectsAndInstances(
unpackedRoot.ObjectsToConvert
);
// Initialize unpackers and collection builder (data holders - created with new)
var colorUnpacker = new GrasshopperColorUnpacker(unpackedRoot);
var materialUnpacker = new GrasshopperMaterialUnpacker(unpackedRoot);
var collectionRebuilder = new GrasshopperCollectionRebuilder(
(root as Collection) ?? new Collection { name = "unnamed" }
);
// get handler from DI and initialize with per-operation data
var mapHandler = scope
.ServiceProvider.GetRequiredService<LocalToGlobalMapHandler>()
.Initialize(
scope.ServiceProvider.GetRequiredService<TraversalContextUnpacker>(),
colorUnpacker,
materialUnpacker,
collectionRebuilder,
unpackedRoot.DefinitionProxies
);
// two-pass conversion: normal objects first, then DataObjects with InstanceProxies
mapHandler.ConvertAtomicObjects(atomicObjects);
// process block instances (internally filters InstanceProxies belonging to registered DataObjects)
mapHandler.ConvertBlockInstances(blockInstances);
var goo = new SpeckleCollectionWrapperGoo(collectionRebuilder.RootCollectionWrapper);
return new ReceiveComponentOutput { RootObject = goo, RootProperties = rootPropertiesGoo };
}
using var client = clientFactory.Create(account);
var receiveInfo = await input.Resource.GetReceiveInfo(client, cancellationToken).ConfigureAwait(false);
// store version id for tracking
_lastVersionId = receiveInfo.SelectedVersionId;
var progress = new Progress<CardProgress>(_ =>
finally
{
// TODO: Progress only makes sense in non-blocking async receive, which is not supported yet.
// Message = $"{progress.Status}: {progress.Progress}";
});
var root = await receiveOperation
.ReceiveCommitObject(receiveInfo, progress, cancellationToken)
.ConfigureAwait(false);
// extract model-wide root properties (see cnx-2722)
SpecklePropertyGroupGoo? rootPropertiesGoo = null;
if (root is RootCollection rootCollection && rootCollection.properties.Count > 0)
{
rootPropertiesGoo = new SpecklePropertyGroupGoo(rootCollection.properties);
SpeckleConversionContext.EndCurrent();
}
// TODO: If we have NodeRun events later, better to have `ComponentTracker` to use across components
var customProperties = new Dictionary<string, object>
{
{ "isAsync", false },
{ "sourceHostApp", HostApplications.GetSlugFromHostAppNameAndVersion(receiveInfo.SourceApplication) }
};
if (receiveInfo.WorkspaceId != null)
{
customProperties.Add("workspace_id", receiveInfo.WorkspaceId);
}
if (receiveInfo.SelectedVersionUserId != null)
{
customProperties.Add("isMultiplayer", receiveInfo.SelectedVersionUserId != client.Account.userInfo.id);
}
var mixpanel = PriorityLoader.Container.GetRequiredService<IMixPanelManager>();
await mixpanel.TrackEvent(MixPanelEvents.Receive, account, customProperties);
// We need to rethink these lovely unpackers, there's a bit too many of 'em
var rootObjectUnpacker = scope.ServiceProvider.GetService<RootObjectUnpacker>();
var traversalContextUnpacker = new TraversalContextUnpacker();
var unpackedRoot = rootObjectUnpacker.Unpack(root);
// split atomic objects from block components before conversion
var (atomicObjects, blockInstances) = rootObjectUnpacker.SplitAtomicObjectsAndInstances(
unpackedRoot.ObjectsToConvert
);
// Initialize unpackers and collection builder
var colorUnpacker = new GrasshopperColorUnpacker(unpackedRoot);
var materialUnpacker = new GrasshopperMaterialUnpacker(unpackedRoot);
var collectionRebuilder = new GrasshopperCollectionRebuilder(
(root as Collection) ?? new Collection { name = "unnamed" }
);
// convert atomic objects directly
var mapHandler = new LocalToGlobalMapHandler(
traversalContextUnpacker,
collectionRebuilder,
colorUnpacker,
materialUnpacker
);
foreach (var atomicContext in atomicObjects)
{
mapHandler.ConvertAtomicObject(atomicContext);
}
// process block instances using converted atomic objects
// block processing needs converted objects, but object filtering needs block definitions.
mapHandler.ConvertBlockInstances(blockInstances, unpackedRoot.DefinitionProxies);
// var x = new SpeckleCollectionGoo { Value = collGen.RootCollection };
var goo = new SpeckleCollectionWrapperGoo(collectionRebuilder.RootCollectionWrapper);
return new ReceiveComponentOutput { RootObject = goo, RootProperties = rootPropertiesGoo };
}
private void SetupSubscription(SpeckleUrlModelResource resource)
@@ -29,13 +29,13 @@ public class SpeckleConversionContext(IRootToSpeckleConverter speckleConverter,
}
}
public static void SetupCurrent()
public static void SetupCurrent(IServiceScope? scope = null)
{
if (s_currentContext != null)
{
return;
}
s_scope = PriorityLoader.CreateScopeForActiveDocument();
s_scope = scope ?? PriorityLoader.CreateScopeForActiveDocument();
s_currentContext = s_scope.Get<SpeckleConversionContext>();
}
@@ -60,6 +60,7 @@ public class SpeckleConversionContext(IRootToSpeckleConverter speckleConverter,
{
GeometryBase geometry => [(geometry, input)],
List<GeometryBase> geometryList => geometryList.Select(o => ((object)o, input)).ToList(),
List<(GeometryBase, Base)> pairList when pairList.Count == 0 => [],
IEnumerable<(GeometryBase, Base)> fallbackConversionResult
=> fallbackConversionResult.Select(o => ((object)o.Item1, o.Item2)).ToList(),
object obj => [(obj, input)],
@@ -1,12 +1,18 @@
using Microsoft.Extensions.Logging;
using Rhino.Geometry;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Connectors.GrasshopperShared.HostApp;
using Speckle.Connectors.GrasshopperShared.Operations.Receive;
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Converters.Common.ToHost;
using Speckle.Sdk;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.GraphTraversal;
using Speckle.Sdk.Models.Instances;
using DataObject = Speckle.Objects.Data.DataObject;
namespace Speckle.Connectors.GrasshopperShared.Operations.Receive;
/// <summary>
/// Handles conversion of atomic objects from TraversalContexts into Grasshopper wrapper objects.
@@ -19,184 +25,388 @@ using Speckle.Sdk.Models.Instances;
internal sealed class LocalToGlobalMapHandler
{
public Dictionary<string, SpeckleGeometryWrapper> ConvertedObjectsMap { get; } = new();
public readonly GrasshopperCollectionRebuilder CollectionRebuilder;
private readonly TraversalContextUnpacker _traversalContextUnpacker;
private readonly GrasshopperColorUnpacker _colorUnpacker;
private readonly GrasshopperMaterialUnpacker _materialUnpacker;
// injected via constructor (DI-managed)
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
private readonly ILogger<LocalToGlobalMapHandler> _logger;
// set via Initialize() method (per-operation data)
private TraversalContextUnpacker _traversalContextUnpacker = null!;
private GrasshopperColorUnpacker _colorUnpacker = null!;
private GrasshopperMaterialUnpacker _materialUnpacker = null!;
private IReadOnlyCollection<InstanceDefinitionProxy>? _definitionProxies;
// auto property (fixes IDE0032)
public GrasshopperCollectionRebuilder CollectionRebuilder { get; private set; } = null!;
public LocalToGlobalMapHandler(
IDataObjectInstanceRegistry dataObjectInstanceRegistry,
ILogger<LocalToGlobalMapHandler> logger
)
{
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
_logger = logger;
}
/// <summary>
/// Initializes the handler with per-operation data.
/// Must be called before using ConvertAtomicObjects or ConvertBlockInstances.
/// </summary>
public LocalToGlobalMapHandler Initialize(
TraversalContextUnpacker traversalContextUnpacker,
GrasshopperCollectionRebuilder collectionRebuilder,
GrasshopperColorUnpacker colorUnpacker,
GrasshopperMaterialUnpacker materialUnpacker
GrasshopperMaterialUnpacker materialUnpacker,
GrasshopperCollectionRebuilder collectionRebuilder,
IReadOnlyCollection<InstanceDefinitionProxy>? definitionProxies
)
{
_traversalContextUnpacker = traversalContextUnpacker;
_colorUnpacker = colorUnpacker;
_materialUnpacker = materialUnpacker;
CollectionRebuilder = collectionRebuilder;
_definitionProxies = definitionProxies;
return this;
}
/// <summary>
/// Converts atomic object from TraversalContext to SpeckleObjectWrapper.
/// Converts all atomic objects in two passes:
/// Pass 1 - Convert normal objects and populate ConvertedObjectsMap
/// Pass 2 - Resolve registered DataObjects with InstanceProxies using the populated map
/// </summary>
public void ConvertAtomicObject(TraversalContext atomicContext)
public void ConvertAtomicObjects(IEnumerable<TraversalContext> atomicContexts)
{
// Cache to avoid re-iterating for registered check
var atomicList = atomicContexts as IList<TraversalContext> ?? atomicContexts.ToList();
// Pass 1: Convert all non-registered DataObjects to populate ConvertedObjectsMap
foreach (var atomicContext in atomicList)
{
ConvertObjectToCache(atomicContext);
}
// Pass 2: Process registered DataObjects (definitions now available in ConvertedObjectsMap)
foreach (var atomicContext in atomicList)
{
if (atomicContext.Current is DataObject dataObject)
{
var dataObjectId = dataObject.applicationId ?? dataObject.id;
if (dataObjectId is not null && _dataObjectInstanceRegistry.IsRegistered(dataObjectId))
{
ResolveDataObjectInstanceProxies(atomicContext);
}
}
}
}
/// <summary>
/// Converts and caches an atomic object for later lookup.
/// Skips registered DataObjects (displayValue is InstanceProxy) - they are resolved in ResolveDataObjectInstanceProxies.
/// </summary>
private void ConvertObjectToCache(TraversalContext atomicContext)
{
var obj = atomicContext.Current;
var objId = obj.applicationId ?? obj.id;
if (objId == null || ConvertedObjectsMap.ContainsKey(objId))
if (objId is null || ConvertedObjectsMap.ContainsKey(objId))
{
return;
}
// skip registered DataObjects - they'll be processed in second pass
if (obj is DataObject dataObject)
{
var id = dataObject.applicationId ?? dataObject.id.NotNull();
if (_dataObjectInstanceRegistry.IsRegistered(id))
{
return;
}
}
try
{
List<(object, Base)> converted = SpeckleConversionContext.Current.ConvertToHost(obj);
if (converted.Count == 0)
{
return;
}
// get path and collection
var path = _traversalContextUnpacker.GetCollectionPath(atomicContext).ToList();
// Always create collection - consumed objects will be cleaned up later
var objectCollection = CollectionRebuilder.GetOrCreateSpeckleCollectionFromPath(
path,
_colorUnpacker,
_materialUnpacker
);
if (obj is Speckle.Objects.Data.DataObject dataObject)
// nothing converted - nothing to do
if (converted.Count == 0)
{
// get color and mat on dataobject first
Color? dataObjColor = _colorUnpacker.Cache.TryGetValue(
dataObject.applicationId ?? "",
out var cachedDataObjColor
)
? cachedDataObjColor
: null;
SpeckleMaterialWrapper? dataObjMat = _materialUnpacker.Cache.TryGetValue(
dataObject.applicationId ?? "",
out var cachedDataObjMaterial
)
? cachedDataObjMaterial
: null;
// get geometries
List<SpeckleGeometryWrapper> geometries = new();
foreach ((object convertedObj, Base original) in converted)
{
if (convertedObj is GeometryBase geometryBase)
{
SpeckleGeometryWrapper wrapper =
new()
{
Base = original,
GeometryBase = geometryBase,
Color = _colorUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjColor)
? cachedObjColor
: dataObjColor,
Material = _materialUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjMaterial)
? cachedObjMaterial
: dataObjMat,
};
geometries.Add(wrapper);
}
}
SpecklePropertyGroupGoo propertyGroup = new();
propertyGroup.CastFrom(dataObject.properties);
// remove the displayvalue of the original dataobject since these are now processed and stored on the wrapper
// to prevent storing of duplicate Base
dataObject.displayValue.Clear();
var dataObjectWrapper = new SpeckleDataObjectWrapper()
{
Base = dataObject,
Geometries = geometries,
Path = path.Select(p => p.name).ToList(),
Parent = objectCollection,
Name = dataObject.name,
Properties = propertyGroup,
ApplicationId = dataObject.applicationId,
};
// Add to collections (not to map since these won't be definition objects)
CollectionRebuilder.AppendSpeckleGrasshopperObject(dataObjectWrapper, path, _colorUnpacker, _materialUnpacker);
return;
}
else
// handle normal DataObject (has converted geometry)
if (obj is DataObject normalDataObject)
{
SpecklePropertyGroupGoo propertyGroup = new();
if (obj[Constants.PROPERTIES_PROP] is Dictionary<string, object?> props)
{
propertyGroup.CastFrom(props);
}
var geometries = ConvertToGeometryWrappers(converted);
var dataObjectWrapper = CreateDataObjectWrapper(normalDataObject, geometries, path, objectCollection);
foreach ((object convertedObj, Base original) in converted)
CollectionRebuilder.AppendSpeckleGrasshopperObject(dataObjectWrapper, path, _colorUnpacker, _materialUnpacker);
return;
}
// handle normal geometry (not DataObject)
SpecklePropertyGroupGoo propertyGroup = new();
if (obj[Constants.PROPERTIES_PROP] is Dictionary<string, object?> props)
{
propertyGroup.CastFrom(props);
}
foreach ((object convertedObj, Base original) in converted)
{
if (convertedObj is GeometryBase geometryBase)
{
if (convertedObj is GeometryBase geometryBase)
var wrapper = new SpeckleGeometryWrapper()
{
var wrapper = new SpeckleGeometryWrapper()
{
Base = original,
Path = path.Select(p => p.name).ToList(),
Parent = objectCollection,
GeometryBase = geometryBase,
Properties = propertyGroup,
Name = obj[Constants.NAME_PROP] as string ?? "",
Color = _colorUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjColor)
? cachedObjColor
: null,
Material = _materialUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjMaterial)
? cachedObjMaterial
: null,
ApplicationId = objId
};
Base = original,
Path = path.Select(p => p.name).ToList(),
Parent = objectCollection,
GeometryBase = geometryBase,
Properties = propertyGroup,
Name = obj[Constants.NAME_PROP] as string ?? "",
Color = _colorUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjColor)
? cachedObjColor
: null,
Material = _materialUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjMaterial)
? cachedObjMaterial
: null,
ApplicationId = objId
};
// Always add to both map and collections
ConvertedObjectsMap[objId] = wrapper;
CollectionRebuilder.AppendSpeckleGrasshopperObject(wrapper, path, _colorUnpacker, _materialUnpacker);
}
ConvertedObjectsMap[objId] = wrapper;
CollectionRebuilder.AppendSpeckleGrasshopperObject(wrapper, path, _colorUnpacker, _materialUnpacker);
}
}
}
catch (Exception ex) when (!ex.IsFatal())
{
// TODO: throw?
// don't throw - continue processing other objects
_logger.LogError(ex, "Failed to convert object {objectId} of type {objectType}", objId, obj.speckle_type);
}
}
/// <summary>
/// Resolves a registered DataObject by transforming its InstanceProxy definition objects.
/// Requires definition objects to exist in ConvertedObjectsMap (populated by ConvertObjectToCache).
/// </summary>
private void ResolveDataObjectInstanceProxies(TraversalContext atomicContext)
{
var obj = atomicContext.Current;
if (obj is not DataObject dataObject)
{
return;
}
var dataObjectId = dataObject.applicationId ?? dataObject.id.NotNull();
if (!_dataObjectInstanceRegistry.IsRegistered(dataObjectId))
{
return;
}
try
{
var path = _traversalContextUnpacker.GetCollectionPath(atomicContext).ToList();
var objectCollection = CollectionRebuilder.GetOrCreateSpeckleCollectionFromPath(
path,
_colorUnpacker,
_materialUnpacker
);
var entry = _dataObjectInstanceRegistry.GetEntries()[dataObjectId];
var resolvedGeometries = ResolveInstanceProxiesToGeometries(entry.InstanceProxies);
var dataObjectWrapper = CreateDataObjectWrapper(dataObject, resolvedGeometries, path, objectCollection);
CollectionRebuilder.AppendSpeckleGrasshopperObject(dataObjectWrapper, path, _colorUnpacker, _materialUnpacker);
}
catch (Exception ex) when (!ex.IsFatal())
{
// don't throw - continue processing other DataObjects
_logger.LogError(ex, "Failed to resolve DataObject {dataObjectId} with InstanceProxies", dataObjectId);
}
}
/// <summary>
/// Converts block instances and definitions from traversal contexts into Grasshopper wrapper objects.
/// Automatically filters out InstanceProxies belonging to registered DataObjects.
/// Automatically handles cleanup of consumed objects from the collection hierarchy.
/// </summary>
/// <remarks>
/// Deliberately handles both block conversion AND consumed object cleanup in a single operation.
/// Too much, I know, BUT it ensures the cleanup always occurs immediately after block processing without
/// requiring receive components to call a separate cleanup method in the correct order.
/// </remarks>
public void ConvertBlockInstances(
IReadOnlyCollection<TraversalContext> blocks,
IReadOnlyCollection<InstanceDefinitionProxy>? definitionProxies
)
public void ConvertBlockInstances(IReadOnlyCollection<TraversalContext> blockInstances)
{
// build set of registered InstanceProxy IDs for fast lookup
var registeredProxyIds = new HashSet<string>();
foreach (var entry in _dataObjectInstanceRegistry.GetEntries().Values)
{
foreach (var proxy in entry.InstanceProxies)
{
var proxyId = proxy.applicationId ?? proxy.id;
if (proxyId is not null)
{
registeredProxyIds.Add(proxyId);
}
}
}
// filter out InstanceProxies that belong to registered DataObjects
var filteredBlockInstances = blockInstances
.Where(tc =>
{
if (tc.Current is InstanceProxy proxy)
{
var proxyId = proxy.applicationId ?? proxy.id;
return proxyId is null || !registeredProxyIds.Contains(proxyId);
}
return true;
})
.ToList();
var blockUnpacker = new GrasshopperBlockUnpacker(_traversalContextUnpacker, _colorUnpacker, _materialUnpacker);
// Get consumed object IDs from unpacker
// get consumed object IDs from unpacker
var consumedObjectIds = blockUnpacker.UnpackBlocks(
blocks,
definitionProxies,
filteredBlockInstances,
_definitionProxies,
ConvertedObjectsMap,
CollectionRebuilder
);
// Clean up consumed objects from collections
// clean up consumed objects from collections
CollectionRebuilder.RemoveConsumedObjects(consumedObjectIds);
}
/// <summary>
/// Creates a DataObjectWrapper from a DataObject and its geometries.
/// Handles color/material inheritance and property extraction.
/// </summary>
private SpeckleDataObjectWrapper CreateDataObjectWrapper(
DataObject dataObject,
List<SpeckleGeometryWrapper> geometries,
List<Collection> path,
SpeckleCollectionWrapper objectCollection
)
{
// Get color and material on DataObject
Color? dataObjColor = _colorUnpacker.Cache.TryGetValue(dataObject.applicationId ?? "", out var cachedDataObjColor)
? cachedDataObjColor
: null;
SpeckleMaterialWrapper? dataObjMat = _materialUnpacker.Cache.TryGetValue(
dataObject.applicationId ?? "",
out var cachedDataObjMaterial
)
? cachedDataObjMaterial
: null;
// Apply DataObject color/material to geometries that don't have their own
foreach (var geometry in geometries)
{
geometry.Color ??= dataObjColor;
geometry.Material ??= dataObjMat;
}
// Create property group
SpecklePropertyGroupGoo propertyGroup = new();
propertyGroup.CastFrom(dataObject.properties);
// Clear the displayValue to prevent storing duplicate Base
dataObject.displayValue.Clear();
return new SpeckleDataObjectWrapper()
{
Base = dataObject,
Geometries = geometries,
Path = path.Select(p => p.name).ToList(),
Parent = objectCollection,
Name = dataObject.name,
Properties = propertyGroup,
ApplicationId = dataObject.applicationId,
};
}
/// <summary>
/// Resolves InstanceProxy displayValues to transformed geometries.
/// Returns the list of resolved geometries that can be used as DataObject displayValue replacements.
/// </summary>
private List<SpeckleGeometryWrapper> ResolveInstanceProxiesToGeometries(List<InstanceProxy> instanceProxies)
{
var resolvedGeometries = new List<SpeckleGeometryWrapper>();
// build a lookup of definitionId -> definition objects for quick access
var definitionObjectsMap = new Dictionary<string, List<string>>();
if (_definitionProxies is not null)
{
foreach (var defProxy in _definitionProxies)
{
var defId = defProxy.applicationId ?? defProxy.id;
if (defId is not null)
{
definitionObjectsMap[defId] = defProxy.objects;
}
}
}
foreach (var instanceProxy in instanceProxies)
{
// get the definition objects for this instance
if (!definitionObjectsMap.TryGetValue(instanceProxy.definitionId, out var definitionObjectIds))
{
continue; // definition not found, skip this proxy
}
// get transform from the instance proxy
var transform = GrasshopperHelpers.MatrixToTransform(instanceProxy.transform, instanceProxy.units);
// apply transform to each definition object
foreach (var objectId in definitionObjectIds)
{
if (ConvertedObjectsMap.TryGetValue(objectId, out var definitionObject))
{
// deep copy and transform the geometry
var transformedWrapper = definitionObject.DeepCopy();
transformedWrapper.GeometryBase.NotNull().Transform(transform);
resolvedGeometries.Add(transformedWrapper);
}
}
}
return resolvedGeometries;
}
/// <summary>
/// Converts the raw converted objects to SpeckleGeometryWrappers for DataObject display values.
/// Does NOT apply DataObject-level colors/materials - that's handled by CreateDataObjectWrapper.
/// </summary>
private List<SpeckleGeometryWrapper> ConvertToGeometryWrappers(List<(object, Base)> converted)
{
var geometries = new List<SpeckleGeometryWrapper>();
foreach ((object convertedObj, Base original) in converted)
{
if (convertedObj is GeometryBase geometryBase)
{
SpeckleGeometryWrapper wrapper =
new()
{
Base = original,
GeometryBase = geometryBase,
// try to get color/material from the individual geometry first
Color = _colorUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjColor)
? cachedObjColor
: null,
Material = _materialUnpacker.Cache.TryGetValue(original.applicationId ?? "", out var cachedObjMaterial)
? cachedObjMaterial
: null,
};
geometries.Add(wrapper);
}
}
return geometries;
}
}
@@ -15,6 +15,7 @@ using Speckle.Connectors.GrasshopperShared.Operations.Send;
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Connectors.GrasshopperShared.Properties;
using Speckle.Converters.Common;
using Speckle.Converters.Common.ToHost;
using Speckle.Converters.Rhino;
using Speckle.Sdk;
using Speckle.Sdk.Models.GraphTraversal;
@@ -58,6 +59,8 @@ public class PriorityLoader : GH_AssemblyPriority
services.AddTransient<GrasshopperReceiveOperation>();
services.AddSingleton(DefaultTraversal.CreateTraversalFunc());
services.AddTransient<TraversalContextUnpacker>();
services.AddScoped<IDataObjectInstanceRegistry, DataObjectInstanceRegistry>();
services.AddTransient<LocalToGlobalMapHandler>();
// send
services.AddTransient<IRootObjectBuilder<SpeckleCollectionWrapperGoo>, GrasshopperRootObjectBuilder>();
@@ -0,0 +1,65 @@
using Microsoft.Extensions.Logging;
using Rhino.DocObjects;
using Rhino.DocObjects.Tables;
using Speckle.Converters.Common;
using Speckle.Objects.Other;
using Speckle.Sdk;
namespace Speckle.Connectors.Rhino.HostApp;
public class RhinoViewUnpacker
{
private readonly IRootToSpeckleConverter _rootToSpeckleConverter;
private readonly ILogger<RhinoViewUnpacker> _logger;
public RhinoViewUnpacker(IRootToSpeckleConverter rootToSpeckleConverter, ILogger<RhinoViewUnpacker> logger)
{
_rootToSpeckleConverter = rootToSpeckleConverter;
_logger = logger;
}
private Camera? ConvertViewToCamera(ViewInfo view)
{
try
{
var converted = (Speckle.Objects.Other.Camera)_rootToSpeckleConverter.Convert(view);
if (converted is null)
{
return null;
}
return converted;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to create a view from {view}", view.Name);
return null;
}
}
/// <summary>
/// Iterates through a given set of rhino named views to create proxies
/// </summary>
/// <param name="views">current document named views</param>
/// <returns></returns>
public List<Camera> UnpackViews(NamedViewTable views)
{
List<Camera> cameras = new();
foreach (ViewInfo view in views)
{
// skip isometric views for now.
// getting the orthographic match between host apps and the viewer requires too much effort atm.
if (view.Viewport.IsParallelProjection)
{
continue;
}
if (ConvertViewToCamera(view) is Camera camera)
{
cameras.Add(camera);
}
}
return cameras;
}
}
@@ -33,6 +33,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
private readonly RhinoGroupUnpacker _groupUnpacker;
private readonly RhinoMaterialUnpacker _materialUnpacker;
private readonly RhinoColorUnpacker _colorUnpacker;
private readonly RhinoViewUnpacker _viewUnpacker;
private readonly PropertiesExtractor _propertiesExtractor;
private readonly ILogger<RhinoRootObjectBuilder> _logger;
private readonly ISdkActivityFactory _activityFactory;
@@ -46,6 +47,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
RhinoGroupUnpacker groupUnpacker,
RhinoMaterialUnpacker materialUnpacker,
RhinoColorUnpacker colorUnpacker,
RhinoViewUnpacker viewUnpacker,
PropertiesExtractor propertiesExtractor,
ILogger<RhinoRootObjectBuilder> logger,
ISdkActivityFactory activityFactory
@@ -59,6 +61,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
_rootToSpeckleConverter = rootToSpeckleConverter;
_materialUnpacker = materialUnpacker;
_colorUnpacker = colorUnpacker;
_viewUnpacker = viewUnpacker;
_propertiesExtractor = propertiesExtractor;
_logger = logger;
_activityFactory = activityFactory;
@@ -122,20 +125,30 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
throw new SpeckleException("Failed to convert all objects."); // fail fast instead creating empty commit! It will appear as model card error with red color.
}
// 4 - Unpack all proxies for the root
// Get all layers from the created collections on the root object commit for proxy processing
List<Layer> layers = _layerUnpacker.GetUsedLayers().ToList();
using (var _ = _activityFactory.Start("UnpackRenderMaterials"))
{
// 4 - Unpack the render material proxies
rootObjectCollection[ProxyKeys.RENDER_MATERIAL] = _materialUnpacker.UnpackRenderMaterials(atomicObjects, layers);
}
using (var _ = _activityFactory.Start("UnpackColors"))
{
// 5 - Unpack the color proxies
rootObjectCollection[ProxyKeys.COLOR] = _colorUnpacker.UnpackColors(atomicObjects, layers);
}
// 5 - Unpack all other objects for the root
using (var _ = _activityFactory.Start("UnpackViews"))
{
List<Objects.Other.Camera> views = _viewUnpacker.UnpackViews(_converterSettings.Current.Document.NamedViews);
if (views.Count > 0)
{
rootObjectCollection[RootKeys.VIEW] = views;
}
}
return new RootObjectBuilderResult(rootObjectCollection, results);
}
@@ -92,6 +92,8 @@ public static class ServiceRegistration
serviceCollection.AddScoped<RhinoColorBaker>();
serviceCollection.AddScoped<RhinoColorUnpacker>();
serviceCollection.AddScoped<RhinoViewUnpacker>();
serviceCollection.AddScoped<PropertiesExtractor>();
serviceCollection.AddScoped<RevitMappingResolver>();
@@ -25,6 +25,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AttributeExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\DataObjectInstanceGrouper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Properties\PropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoViewUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoLayerHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoObjectHelper.cs" />
@@ -63,4 +64,4 @@
<Compile Include="$(MSBuildThisFileDirectory)Plugin\Speckle.Connectors.RhinoCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\Speckle.Connectors.RhinoPlugin.cs" />
</ItemGroup>
</Project>
</Project>
@@ -17,4 +17,4 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellSectionResolver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\EtabsObjectToSpeckleConverter.cs" />
</ItemGroup>
</Project>
</Project>
@@ -1,10 +1,14 @@
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using static Speckle.Converter.Navisworks.Helpers.PropertyHelpers;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
public class PropertySetsExtractor(
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
IPropertyConverter propertyConverter
)
{
internal Dictionary<string, object?>? GetPropertySets(NAV.ModelItem modelItem)
{
@@ -18,9 +22,30 @@ public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionS
return propertyDictionary;
}
private static NAV.Units GetModelUnits(NAV.ModelItem modelItem)
{
NAV.ModelItem? ancestor = modelItem;
while (ancestor != null && !ancestor.HasModel)
{
ancestor = ancestor.Parent;
}
return ancestor != null ? ancestor.Model.Units : NAV.Units.Meters;
}
/// <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
/// arbitrary in nature.
/// </summary>
/// <param name="modelItem">The NAV.ModelItem from which property sets are extracted.</param>
/// <returns>A dictionary containing property sets of the modelItem.</returns>
private Dictionary<string, object?> ExtractPropertySets(NAV.ModelItem modelItem)
{
var propertySetDictionary = new Dictionary<string, object?>();
var modelUnits = GetModelUnits(modelItem);
propertyConverter.Reset();
foreach (var propertyCategory in modelItem.PropertyCategories)
{
@@ -33,23 +58,18 @@ public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionS
foreach (var property in propertyCategory.Properties)
{
string sanitizedName = SanitizePropertyName(property.DisplayName);
var propertyValue = ConvertPropertyValue(property.Value, settingsStore.Current.Derived.SpeckleUnits);
var sanitizedName = SanitizePropertyName(property.DisplayName);
var propertyValue = propertyConverter.ConvertPropertyValue(property.Value, modelUnits, property.DisplayName);
if (propertyValue != null)
{
propertySet[sanitizedName] = propertyValue;
}
}
if (propertySet.Count <= 0)
if (propertySet.Count > 0)
{
continue;
propertySetDictionary[SanitizePropertyName(propertyCategory.DisplayName)] = propertySet;
}
string categoryName = SanitizePropertyName(propertyCategory.DisplayName);
propertySetDictionary[categoryName] = propertySet;
}
return propertySetDictionary;
@@ -1,8 +1,10 @@
using static Speckle.Converter.Navisworks.Helpers.PropertyHelpers;
using Speckle.Converter.Navisworks.Services;
using Speckle.InterfaceGenerator;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public sealed class RevitBuiltInCategoryExtractor
[GenerateAutoInterface]
public class RevitBuiltInCategoryExtractor(IPropertyConverter converter) : IRevitBuiltInCategoryExtractor
{
private const int ANCESTOR_AND_SELF_COUNT = 4; // It seems like this is the maximum depth found needed in practice
private const string REVIT_CAT_GROUP = "LcRevitData_Element";
@@ -13,28 +15,28 @@ public sealed class RevitBuiltInCategoryExtractor
/// Attempts to map a Navisworks/Revit display category from the given model item or its ancestors
/// to a known Revit built-in category constant (e.g., "OST_Walls").
/// </summary>
internal static bool TryGetBuiltInCategory(
NAV.ModelItem item,
out string mapped,
int maxDepth = ANCESTOR_AND_SELF_COUNT
)
public bool TryGetBuiltInCategory(NAV.ModelItem item, out string mapped, int maxDepth = ANCESTOR_AND_SELF_COUNT)
{
mapped = string.Empty;
// Look up the category value, starting at this item and walking up to maxDepth ancestors
// Find the category VariantData up the hierarchy
var v = FindRevitCategoryInHierarchy(item, maxDepth);
if (v == null)
if (v is null)
{
return false;
}
var name = ConvertPropertyValue(v, "")?.ToString();
converter.Reset();
// Convert using per-object model units and current UI units
var nameObj = converter.ConvertPropertyValue(v, item.Model.Units, item.DisplayName);
var name = nameObj?.ToString();
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
name = name?.Trim();
name = name!.Trim();
// Map display name to OST_* built-in category constant
var builtInCategory = DisplayNameToRevitBuiltInCategory(name);
@@ -11,7 +11,8 @@ public class HierarchicalPropertyHandler(
PropertySetsExtractor propertySetsExtractor,
ModelPropertiesExtractor modelPropertiesExtractor,
ClassPropertiesExtractor classPropertiesExtractor,
IConverterSettingsStore<NavisworksConversionSettings> settingsStore
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
IRevitBuiltInCategoryExtractor revitCategoryExtractor
) : BasePropertyHandler(propertySetsExtractor, modelPropertiesExtractor)
{
private static string PseudoClassPropertiesKey => "_pseudoClassProperties";
@@ -22,7 +23,7 @@ public class HierarchicalPropertyHandler(
var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem) ?? [];
// Interop-lite mapping for Revit built-in categories
if (_mapRevit && RevitBuiltInCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
if (_mapRevit && revitCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
{
PropertyHelpers.AddPropertyIfNotNullOrEmpty(
propertyDict,
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Text.RegularExpressions;
using Speckle.Objects.Geometry;
@@ -8,6 +8,9 @@ public static class PropertyHelpers
{
private static readonly HashSet<string> s_excludedCategories = ["Geometry", "Metadata"];
/// <summary>
/// Adds a property to an object (either a Base object or a Dictionary) if the value is not null or empty.
/// </summary>
private static readonly Dictionary<NAV.VariantDataType, Func<NAV.VariantData, string, dynamic?>> s_typeHandlers =
new()
{
@@ -92,3 +95,27 @@ public static class PropertyHelpers
internal static bool ShouldSkipCategory(NAV.PropertyCategory propertyCategory) =>
s_excludedCategories.Contains(propertyCategory.DisplayName);
}
internal static class UnitLabels
{
internal static string Linear(NAV.Units u) =>
u switch
{
NAV.Units.Kilometers => "Kilometers",
NAV.Units.Meters => "Metres",
NAV.Units.Centimeters => "Centimeters",
NAV.Units.Millimeters => "Millimeters",
NAV.Units.Micrometers => "Micrometers",
NAV.Units.Miles => "Miles",
NAV.Units.Yards => "Yards",
NAV.Units.Feet => "Feet",
NAV.Units.Inches => "Inches",
NAV.Units.Mils => "Mils",
NAV.Units.Microinches => "Microinches",
_ => "Metres"
};
internal static string Area(NAV.Units u) => $"Square {Linear(u).ToLower()}";
public static string Volume(NAV.Units u) => $"Cubic {Linear(u).ToLower()}";
}
@@ -103,23 +103,6 @@ public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
// Add instance definition proxy if not exists
if (!InstanceDefinitionProxiesStore.Contains(definitionId))
{
if (geometry.applicationId == null)
{
_logger.LogWarning(
"Cannot create instance definition proxy - geometry.id is null for FragmentId={FragmentId}",
fragmentId
);
var result = geometryAdded || proxyAdded;
_logger.LogDebug(
"AddSharedGeometry completed: FragmentId={FragmentId}, Result={Result}, GeometryAdded={GeometryAdded}, ProxyAdded={ProxyAdded}",
fragmentId,
result,
geometryAdded,
proxyAdded
);
return result;
}
var definitionProxy = new InstanceDefinitionProxy
{
applicationId = definitionId,
@@ -0,0 +1,91 @@
using System.Globalization;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.InterfaceGenerator;
namespace Speckle.Converter.Navisworks.Services;
[GenerateAutoInterface]
public class PropertyConverter(IUiUnitsCache uiUnitsCache) : IPropertyConverter
{
public void Reset() => uiUnitsCache.Reset();
public object? ConvertPropertyValue(NAV.VariantData? value, NAV.Units modelUnits, string propDisplayName) =>
value == null
? null
: _handlers.TryGetValue(value.DataType, out var f)
? f(value, (modelUnits, propDisplayName))
: value.DataType is NAV.VariantDataType.None or NAV.VariantDataType.Point2D
? null
: value.ToString();
private readonly Dictionary<
NAV.VariantDataType,
Func<NAV.VariantData, (NAV.Units model, string name), object?>
> _handlers =
new()
{
{ NAV.VariantDataType.Boolean, (v, _) => v.ToBoolean() },
{ NAV.VariantDataType.DisplayString, (v, _) => v.ToDisplayString() },
{ NAV.VariantDataType.IdentifierString, (v, _) => v.ToIdentifierString() },
{ NAV.VariantDataType.Int32, (v, _) => v.ToInt32() },
{ NAV.VariantDataType.Double, (v, _) => v.ToDouble() },
// Angle as dictionary with units
{ NAV.VariantDataType.DoubleAngle, (v, t) => NumObj(t.name, v.ToDoubleAngle(), "Degrees") },
// Length → dictionary in UI units
{
NAV.VariantDataType.DoubleLength,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
return NumObj(t.name, v.ToDoubleLength() * k, UnitLabels.Linear(uiUnits));
}
},
// Area → dictionary in UI units^2
{
NAV.VariantDataType.DoubleArea,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
k *= k;
return NumObj(t.name, v.ToDoubleArea() * k, UnitLabels.Area(uiUnits));
}
},
// Volume → dictionary in UI units^3
{
NAV.VariantDataType.DoubleVolume,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
k = k * k * k;
return NumObj(t.name, v.ToDoubleVolume() * k, UnitLabels.Volume(uiUnits));
}
},
{ NAV.VariantDataType.DateTime, (v, _) => v.ToDateTime().ToString(CultureInfo.InvariantCulture) },
{ NAV.VariantDataType.NamedConstant, (v, _) => v.ToNamedConstant().DisplayName },
{ NAV.VariantDataType.None, (_, _) => null },
{ NAV.VariantDataType.Point2D, (_, _) => null },
{
NAV.VariantDataType.Point3D,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
var p = v.ToPoint3D();
return new Speckle.Objects.Geometry.Point(p.X * k, p.Y * k, p.Z * k, UnitLabels.Linear(uiUnits));
}
}
};
private static Dictionary<string, object> NumObj(string name, double value, string units) =>
new()
{
["name"] = name,
["value"] = value,
["units"] = units
};
}
@@ -0,0 +1,71 @@
using Autodesk.Navisworks.Api.Interop;
using Speckle.InterfaceGenerator;
using static Autodesk.Navisworks.Api.Interop.LcUOption;
namespace Speckle.Converter.Navisworks.Services;
[GenerateAutoInterface]
public class UiUnitsCache : IUiUnitsCache
{
private NAV.Units? _ui;
public NAV.Units Ensure()
{
if (_ui.HasValue)
{
return _ui.Value;
}
UiUnitsUtil.TryGetUiLinearUnits(out var ui);
_ui = ui;
return _ui.Value;
}
public void Reset() => _ui = null;
}
public static class UiUnitsUtil
{
// disp_units: 0=linear_format
public static bool TryGetUiLinearUnits(out NAV.Units uiUnits)
{
using var opt = new LcUOptionLock();
var root = GetRoot(opt);
var disp = root.GetSubOptions("interface").GetSubOptions("disp_units");
int code = -1;
using var v = new NAV.VariantData();
disp.GetValue(0, v);
var s = v.ToString();
var colon = s.LastIndexOf(':');
var open = s.IndexOf('(', colon + 1);
if (colon >= 0 && open > colon && !int.TryParse(s.Substring(colon + 1, open - colon - 1), out code))
{
code = -1;
}
uiUnits = code switch
{
0 => NAV.Units.Kilometers,
1 => NAV.Units.Meters,
2 => NAV.Units.Centimeters,
3 => NAV.Units.Millimeters,
4 => NAV.Units.Micrometers,
5 => NAV.Units.Miles,
6 => NAV.Units.Miles,
7 => NAV.Units.Yards,
8 => NAV.Units.Yards,
9 => NAV.Units.Feet,
10 => NAV.Units.Feet,
11 => NAV.Units.Feet,
12 => NAV.Units.Inches,
13 => NAV.Units.Inches,
14 => NAV.Units.Mils,
15 => NAV.Units.Microinches,
_ => NAV.Units.Meters
};
return code >= 0;
}
}
@@ -30,6 +30,8 @@
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)PathConstants.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\PropertyConversion.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\UIUnits.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\SharedGeometryStores.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\InstanceStoreManager.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs"/>
@@ -10,7 +10,6 @@ using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects.Geometry;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
@@ -42,7 +41,7 @@ public class GeometryToSpeckleConverter(
private readonly ILogger<GeometryToSpeckleConverter> _logger =
logger ?? throw new ArgumentNullException(nameof(logger));
internal List<Base> Convert(NAV.ModelItem modelItem)
internal List<SSM.Base> Convert(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
@@ -59,6 +58,10 @@ public class GeometryToSpeckleConverter(
{
var fragmentStack = new Stack<InwOaFragment3>();
var paths = comSelection.Paths();
if (paths == null)
{
return [];
}
try
{
if (paths.Count > 0)
@@ -90,10 +93,7 @@ public class GeometryToSpeckleConverter(
}
finally
{
if (paths != null)
{
Marshal.ReleaseComObject(paths);
}
Marshal.ReleaseComObject(paths);
}
}
finally
@@ -127,7 +127,7 @@ public class GeometryToSpeckleConverter(
}
}
private List<Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
private List<SSM.Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
{
var fragmentId = GenerateFragmentId(paths);
@@ -153,20 +153,12 @@ public class GeometryToSpeckleConverter(
var baseGeometry = ExtractUntransformedGeometry(fragmentStack);
if (baseGeometry == null)
{
return ProcessFragments(fragmentStack, paths);
}
if (!_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometry))
{
return ProcessFragments(fragmentStack, paths);
}
return CreateInstanceReference(fragmentId, paths);
return baseGeometry == null || !_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometry)
? ProcessFragments(fragmentStack, paths) // default false flag for isSingleObject
: CreateInstanceReference(fragmentId, paths);
}
private List<Base> ProcessFragments(
private List<SSM.Base> ProcessFragments(
Stack<InwOaFragment3> fragmentStack,
InwSelectionPathsColl paths,
bool isSingleObject = false
@@ -191,7 +183,7 @@ public class GeometryToSpeckleConverter(
int fragmentCount;
try
{
fragmentCount = fragmentsForCount.Count;
fragmentCount = fragmentsForCount?.Count ?? 0;
}
finally
{
@@ -204,14 +196,8 @@ public class GeometryToSpeckleConverter(
double[] makeNoChange = s_identityTransform;
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
if (isSingleObject || fragmentCount == 1)
{
processor.LocalToWorldTransformation = transformMatrix;
}
else
{
processor.LocalToWorldTransformation = makeNoChange;
}
processor.LocalToWorldTransformation =
isSingleObject || fragmentCount == 1 ? transformMatrix : (IEnumerable<double>)makeNoChange;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
@@ -227,9 +213,9 @@ public class GeometryToSpeckleConverter(
&& path.ArrayData is Array pathData
&& AreFragmentPathsEqual(fragmentPathData, pathData);
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
private List<SSM.Base> ProcessGeometries(List<PrimitiveProcessor> processors)
{
var baseGeometries = new List<Base>();
var baseGeometries = new List<SSM.Base>();
foreach (var processor in processors)
{
@@ -373,7 +359,12 @@ public class GeometryToSpeckleConverter(
RankException => "Array rank mismatch",
_ => "Error"
};
_logger.LogDebug(ex, "{ErrorType} processing fragment {FragmentIndex}, trying simple enumeration", errorType, fragmentIndex);
_logger.LogDebug(
ex,
"{ErrorType} processing fragment {FragmentIndex}, trying simple enumeration",
errorType,
fragmentIndex
);
var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHash))
@@ -404,10 +395,8 @@ public class GeometryToSpeckleConverter(
var fragmentId = HashRawData(rawData);
return fragmentId;
}
else
{
return string.Empty;
}
return string.Empty;
}
catch (Exception ex) when (ex is COMException or InvalidCastException or IndexOutOfRangeException)
{
@@ -470,7 +459,7 @@ public class GeometryToSpeckleConverter(
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
private Base? ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
private SSM.Base? ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
{
var processor = new PrimitiveProcessor(_isUpright);
@@ -484,7 +473,7 @@ public class GeometryToSpeckleConverter(
return processor.Triangles.Count > 0 ? CreateMesh(processor.Triangles) : null;
}
private List<Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
private List<SSM.Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
{
var transform = ExtractInstanceTransform(paths);
@@ -191,20 +191,51 @@ public sealed class DisplayValueExtractor
);
}
// add rest of geometry (always without transform)
// 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)
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
if (localToWorld is not null)
{
using var transformedCurve = curve.CreateTransformed(localToWorld);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
}
// 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)
{
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(polyline)));
if (localToWorld is not null)
{
var coords = polyline.GetCoordinates();
var transformedCoords = coords.Select(coord => localToWorld.OfPoint(coord)).ToList();
using var transformedPolyline = DB.PolyLine.Create(transformedCoords);
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(transformedPolyline)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(polyline)));
}
}
foreach (var point in collections.Points)
{
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(point)));
if (localToWorld is not null)
{
using var transformedPoint = DB.Point.Create(localToWorld.OfPoint(point.Coord));
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(transformedPoint)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(point)));
}
}
return displayValue;
@@ -346,7 +377,8 @@ public sealed class DisplayValueExtractor
collections.Meshes.Add(mesh);
break;
//Note, we're not applying transforms to curves/polylines/points because ProcessGeometryCollections expects them in world coordinates
// curves, polylines, and points are transformed to world space in ProcessGeometryCollections,
// not here, because they cannot be proxified like meshes.
case DB.Curve curve:
collections.Curves.Add(curve);
break;
@@ -573,8 +605,9 @@ public sealed class DisplayValueExtractor
/// and reduce the risk of parameter ordering errors.
/// </summary>
/// <remarks>
/// <see cref="Solids"/> and <see cref="Meshes"/> potentially in local coordinate space.
/// For now, <see cref="Curves"/>, <see cref="Polylines"/>, <see cref="Points"/> will always be in world space
/// <see cref="Solids"/> and <see cref="Meshes"/> are transformed to local coordinate space in SortGeometry.
/// <see cref="Curves"/>, <see cref="Polylines"/>, and <see cref="Points"/> remain in their original coordinate space
/// and are transformed to world space during processing in ProcessGeometryCollections.
/// </remarks>
private sealed record GeometryCollections
{
@@ -81,8 +81,9 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\RevitElementTopLevelConverterToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ParameterValueExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\RevitContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\View3DTopLevelConverterToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)\Helpers\SendSelection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\RevitToSpeckleUnitConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RevitRootToSpeckleConverter.cs" />
</ItemGroup>
</Project>
</Project>
@@ -88,6 +88,17 @@ public class ClassPropertiesExtractor
{
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
}
// get toRoom and fromRoom for FamilyInstance elements with those properties (e.g. Doors)
if (familyInstance.ToRoom is not null)
{
elementProperties.Add("toRoomId", familyInstance.ToRoom.Id.ToString());
}
if (familyInstance.FromRoom is not null)
{
elementProperties.Add("fromRoomId", familyInstance.FromRoom.Id.ToString());
}
}
catch (Exception e) when (!e.IsFatal())
{
@@ -0,0 +1,49 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Objects.Other;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
namespace Speckle.Converters.RevitShared.ToSpeckle;
[NameAndRankValue(typeof(DB.View3D), 0)]
public class View3DTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter, ITypedConverter<DB.View3D, Camera>
{
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<DB.XYZ, SOG.Vector> _xyzToVectorConverter;
public View3DTopLevelConverterToSpeckle(
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<DB.XYZ, SOG.Vector> xyzToVectorConverter
)
{
_xyzToPointConverter = xyzToPointConverter;
_xyzToVectorConverter = xyzToVectorConverter;
}
public Base Convert(object target) => Convert((DB.View3D)target);
public Camera Convert(DB.View3D target)
{
if (!target.IsPerspective)
{
throw new ConversionException("Non-Perspective views are not supported");
}
// some views have null origin, not sure why
if (target.Origin == null)
{
throw new ConversionException("Views with no origin are not supported");
}
DB.ViewOrientation3D orientation = target.GetSavedOrientation();
return new()
{
name = target.Title,
position = _xyzToPointConverter.Convert(target.Origin),
forward = _xyzToVectorConverter.Convert(orientation.ForwardDirection),
up = _xyzToVectorConverter.Convert(orientation.UpDirection)
};
}
}
@@ -2,4 +2,5 @@ global using RG = Rhino.Geometry;
global using SA = Speckle.Objects.Annotation;
global using SO = Speckle.Objects.Other;
global using SOG = Speckle.Objects.Geometry;
global using SOO = Speckle.Objects.Other;
global using SOP = Speckle.Objects.Primitive;
@@ -74,12 +74,13 @@ public class DataObjectConverter
{
var resultPairs = new List<(RG.GeometryBase, Base)>();
// check if display value contains InstanceProxies - register for special handling
if (target.displayValue.Count > 0 && target.displayValue[0] is InstanceProxy)
// check if displayValue contains ANY InstanceProxies - register for special handling
var instanceProxies = target.displayValue.OfType<InstanceProxy>().ToList();
if (instanceProxies.Count > 0)
{
var instanceProxies = target.displayValue.Cast<InstanceProxy>().ToList();
_dataObjectInstanceRegistry.Register(target.applicationId ?? target.id.NotNull(), target, instanceProxies);
return resultPairs; // empty - will be handled by instance baker
return resultPairs;
}
// normal display value conversion
@@ -0,0 +1,41 @@
using Rhino.DocObjects;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
[NameAndRankValue(typeof(ViewInfo), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
public class ViewInfoToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter, ITypedConverter<ViewInfo, SOO.Camera>
{
private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
private readonly ITypedConverter<RG.Vector3d, SOG.Vector> _vectorConverter;
public ViewInfoToSpeckleTopLevelConverter(
ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
ITypedConverter<RG.Vector3d, SOG.Vector> vectorConverter
)
{
_pointConverter = pointConverter;
_vectorConverter = vectorConverter;
}
public Base Convert(object target) => Convert((ViewInfo)target);
public SOO.Camera Convert(ViewInfo target)
{
if (target.Viewport.IsParallelProjection)
{
throw new ConversionException("Parallel projection views are not supported.");
}
return new()
{
name = target.Name,
position = _pointConverter.Convert(target.Viewport.CameraLocation),
up = _vectorConverter.Convert(target.Viewport.CameraY),
forward = _vectorConverter.Convert(target.Viewport.CameraZ),
};
}
}
@@ -1,5 +1,7 @@
using Speckle.Connectors.DUI.Bridge;
using Speckle.Newtonsoft.Json;
using Speckle.Sdk.Credentials;
using Speckle.Sdk.SQLite;
namespace Speckle.Connectors.DUI.Bindings;
@@ -9,14 +11,25 @@ public class AccountBinding : IBinding
public IBrowserBridge Parent { get; }
private readonly IAccountManager _accountManager;
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
public AccountBinding(IBrowserBridge bridge, IAccountManager accountManager)
public AccountBinding(
IBrowserBridge bridge,
IAccountManager accountManager,
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory
)
{
Parent = bridge;
_accountManager = accountManager;
_jsonCacheManager = sqLiteJsonCacheManagerFactory.CreateForUser("Accounts");
}
public Account[] GetAccounts() => _accountManager.GetAccounts().ToArray();
public void AddAccount(string accountId, Account account)
{
_jsonCacheManager.SaveObject(accountId, JsonConvert.SerializeObject(account));
}
public void RemoveAccount(string accountId) => _accountManager.RemoveAccount(accountId);
}
@@ -4,4 +4,5 @@ public static class RootKeys
{
public const string ANALYSIS_RESULTS = "analysisResults";
public const string REFERENCE_POINT_TRANSFORM = "referencePointTransform";
public const string VIEW = "views";
}