Compare commits

..

12 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

It uses a dictionary and a hash set to optimize lookup and storage, and includes thread safety mechanisms.
2025-11-13 14:17:50 +03:00
123 changed files with 1175 additions and 2165 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
file_version: ${{ steps.set-version.outputs.file_version }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -210,7 +210,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -210,7 +210,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -268,7 +268,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -268,7 +268,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -268,7 +268,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -219,7 +219,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -219,7 +219,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -210,7 +210,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
+17 -47
View File
@@ -2,63 +2,33 @@
<Project>
<PropertyGroup>
<UseWpf>true</UseWpf>
<Description>NextGen Speckle Connector for Autodesk Navisworks Manage</Description>
<Authors>$(Authors) jonathon@speckle.systems</Authors>
<PackageTags>$(PackageTags) connector nwd nwc nwf navisworks manage</PackageTags>
<PluginBundleTarget>$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Navisworks.bundle</PluginBundleTarget>
<PluginVersionContentTarget>$(PluginBundleTarget)\Contents\$(NavisworksVersion)</PluginVersionContentTarget>
<PluginVersionContentTarget>$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Navisworks.bundle\Contents\$(NavisworksVersion)</PluginVersionContentTarget>
<RootNamespace>Speckle.Connector.Navisworks</RootNamespace>
</PropertyGroup>
<Target Name="PostBuild"
AfterTargets="Build"
Condition="'$(OS)' == 'Windows_NT' and '$(NavisworksVersion)' != ''">
<!-- Post Builds -->
<ItemGroup>
<RibbonFiles Include="$(OutDir)Plugin\NavisworksRibbon.*"/>
<ResourceFiles Include="$(OutDir)Resources\**\*.png"/>
<ResourceFiles Include="$(OutDir)Resources\**\*.ico"/>
<AllFiles Include="$(OutDir)*"/>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="Build" Condition="'$(NavisworksVersion)' != '' And '$(ContinuousIntegrationBuild)' != 'true' And '$(OS)' == 'Windows_NT'">
<Message Text="Navisworks Version $(NavisworksVersion)" Importance="high"/>
<RemoveDir Directories="$(PluginVersionContentTarget)" Condition="Exists('$(PluginVersionContentTarget)')"/>
<MakeDir Directories="
$(PluginBundleTarget);
$(PluginBundleTarget)\Contents;
$(PluginVersionContentTarget);
$(PluginVersionContentTarget)\en-US;
$(PluginVersionContentTarget)\Resources"/>
<!-- Re-evaluate outputs at execution time -->
<ItemGroup>
<PackageXml Include="$(OutDir)Plugin\PackageContents.xml"/>
<RibbonFiles Include="$(OutDir)Plugin\NavisworksRibbon.*"/>
<ResourceFiles Include="$(OutDir)Resources\**\*.png;$(OutDir)Resources\**\*.ico"/>
<AllFiles Include="$(OutDir)**\*.*"/>
<Message Text="AllFiles count: @(AllFiles->Count())" Importance="high"/>
<Warning Condition="'@(AllFiles)' == ''" Text="No files in $(OutDir) at PostBuild time."/>
</ItemGroup>
<Copy SourceFiles="@(PackageXml)"
DestinationFolder="$(PluginBundleTarget)\"
SkipUnchangedFiles="true"/>
<Copy SourceFiles="@(RibbonFiles)"
DestinationFolder="$(PluginVersionContentTarget)\en-US\"
SkipUnchangedFiles="true"/>
<Copy SourceFiles="@(ResourceFiles)"
DestinationFiles="@(ResourceFiles->'$(PluginVersionContentTarget)\Resources\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true"/>
<Copy SourceFiles="@(AllFiles)"
DestinationFiles="@(AllFiles->'$(PluginVersionContentTarget)\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true"/>
<Message Text="Copied build to $(PluginVersionContentTarget)" Importance="high"/>
</Target>
<Target Name="ValidateNavisworksVersion" BeforeTargets="PostBuild"
Condition="'$(NavisworksVersion)' == '' and '$(OS)' == 'Windows_NT'">
<Error Text="NavisworksVersion property is required for PostBuild packaging."/>
<Copy SourceFiles="$(OutDir)Plugin\PackageContents.xml" DestinationFolder="$(PluginBundleTarget)\"/>
<Copy SourceFiles="@(RibbonFiles)" DestinationFolder="$(PluginVersionContentTarget)\en-US\"/>
<Copy SourceFiles="@(ResourceFiles)" DestinationFolder="$(PluginVersionContentTarget)\Resources\"/>
<Copy SourceFiles="@(AllFiles)" DestinationFolder="$(PluginVersionContentTarget)\" />
</Target>
</Project>
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -265,7 +265,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -266,7 +266,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -15,8 +15,8 @@ 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.Settings;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converters.Common;
using Speckle.Sdk.Models.GraphTraversal;
@@ -16,13 +16,7 @@ public class NavisworksColorUnpacker(
IElementSelectionService selectionService
)
{
private static T SelectByRepresentationMode<T>(
RepresentationMode mode,
T active,
T permanent,
T original,
T defaultValue
) =>
private static T Select<T>(RepresentationMode mode, T active, T permanent, T original, T defaultValue) =>
mode switch
{
RepresentationMode.Active => active,
@@ -77,14 +71,14 @@ public class NavisworksColorUnpacker(
using var defaultColor = new NAV.Color(1.0, 1.0, 1.0);
var representationColor = SelectByRepresentationMode(
var representationColor = Select(
mode,
geometry.ActiveColor,
geometry.PermanentColor,
geometry.OriginalColor,
defaultColor
);
var colorId = SelectByRepresentationMode(
var colorId = Select(
mode,
$"{geometry.ActiveColor.GetHashCode()}_{geometry.ActiveTransparency}".GetHashCode(),
$"{geometry.PermanentColor.GetHashCode()}_{geometry.PermanentTransparency}".GetHashCode(),
@@ -130,49 +124,30 @@ public class NavisworksColorUnpacker(
var comSelection = ComBridge.ToInwOpSelection([modelItem]);
try
{
var pathsCollection = comSelection.Paths();
try
foreach (ComApi.InwOaPath path in comSelection.Paths())
{
foreach (ComApi.InwOaPath path in pathsCollection)
{
var fragmentsCollection = path.Fragments();
try
{
foreach (ComApi.InwOaFragment3 fragment in fragmentsCollection.OfType<ComApi.InwOaFragment3>())
{
fragment.GenerateSimplePrimitives(ComApi.nwEVertexProperty.eNORMAL, primitiveChecker);
GC.KeepAlive(path);
if (primitiveChecker.HasTriangles)
{
return false;
}
}
}
finally
foreach (ComApi.InwOaFragment3 fragment in path.Fragments())
{
GC.KeepAlive(fragment);
fragment.GenerateSimplePrimitives(ComApi.nwEVertexProperty.eNORMAL, primitiveChecker);
// Exit early if triangles are found
if (primitiveChecker.HasTriangles)
{
if (fragmentsCollection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragmentsCollection);
}
return false;
}
}
}
return primitiveChecker.HasLines || primitiveChecker.HasPoints || primitiveChecker.HasSnapPoints;
}
finally
{
if (pathsCollection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(pathsCollection);
}
}
// Return true if any 2D primitives are found
return primitiveChecker.HasLines || primitiveChecker.HasPoints || primitiveChecker.HasSnapPoints;
}
finally
{
if (comSelection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection);
}
System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection);
}
}
}
@@ -2,7 +2,6 @@ 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.Settings;
using Speckle.Converter.Navisworks.ToSpeckle;
@@ -19,13 +18,9 @@ public class NavisworksMaterialUnpacker(
GeometryToSpeckleConverter converter
)
{
private static T SelectByRepresentationMode<T>(
RepresentationMode mode,
T active,
T permanent,
T original,
T defaultValue
) =>
// Helper function to select a property based on the representation mode
// Selector method for individual properties
private static T Select<T>(RepresentationMode mode, T active, T permanent, T original, T defaultValue) =>
mode switch
{
RepresentationMode.Active => active,
@@ -74,64 +69,16 @@ public class NavisworksMaterialUnpacker(
var navisworksObjectId = selectionService.GetModelItemPath(navisworksObject);
var finalId = mergedIds.TryGetValue(navisworksObjectId, out var mergedId) ? mergedId : navisworksObjectId;
var item = selectionService.GetModelItemFromPath(finalId);
string hashId = "";
try
var comSelection = ComApiBridge.ToInwOpSelection([item]);
var paths = comSelection.Paths();
var path = paths.OfType<InwOaPath>().First();
var fragments = path.Fragments();
if (fragments.Count > 1)
{
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 fragmentId = converter.GenerateFragmentId(paths);
hashId = $"geom_{fragmentId}";
}
var geometry = navisworksObject.Geometry;
@@ -139,21 +86,21 @@ public class NavisworksMaterialUnpacker(
using var defaultColor = new NAV.Color(1.0, 1.0, 1.0);
var renderColor = SelectByRepresentationMode(
var renderColor = Select(
mode,
geometry.ActiveColor,
geometry.PermanentColor,
geometry.OriginalColor,
defaultColor
);
var renderTransparency = SelectByRepresentationMode(
var renderTransparency = Select(
mode,
geometry.ActiveTransparency,
geometry.PermanentTransparency,
geometry.OriginalTransparency,
0.0
);
var renderMaterialId = SelectByRepresentationMode(
var renderMaterialId = Select(
mode,
$"{geometry.ActiveColor.GetHashCode()}_{geometry.ActiveTransparency}".GetHashCode(),
$"{geometry.PermanentColor.GetHashCode()}_{geometry.PermanentTransparency}".GetHashCode(),
@@ -162,8 +109,9 @@ public class NavisworksMaterialUnpacker(
);
var materialName =
$"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
$"NavisworksMaterial_{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
// Check Item category for material name
var itemCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Item");
if (itemCategory != null)
{
@@ -175,6 +123,7 @@ public class NavisworksMaterialUnpacker(
}
}
// Check Material category for material name
var materialPropertyCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Material");
if (materialPropertyCategory != null)
{
@@ -194,7 +143,12 @@ public class NavisworksMaterialUnpacker(
{
renderMaterialProxies[renderMaterialId.ToString()] = new RenderMaterialProxy()
{
value = CreateRenderMaterial(materialName, renderTransparency, renderColor, renderMaterialId),
value = ConvertRenderColorAndTransparencyToSpeckle(
materialName,
renderTransparency,
renderColor,
renderMaterialId
),
objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId]
};
}
@@ -208,7 +162,7 @@ public class NavisworksMaterialUnpacker(
return renderMaterialProxies.Values.ToList();
}
private static RenderMaterial CreateRenderMaterial(
private static RenderMaterial ConvertRenderColorAndTransparencyToSpeckle(
string name,
double transparency,
NAV.Color navisworksColor,
@@ -219,9 +173,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 : $"NavisworksMaterial_{Math.Abs(color.ToArgb())}",
opacity = 1 - transparency,
metalness = 0,
roughness = 1,
@@ -27,7 +27,6 @@ public class NavisworksRootObjectBuilder(
NavisworksMaterialUnpacker materialUnpacker,
NavisworksColorUnpacker colorUnpacker,
IElementSelectionService elementSelectionService,
IUiUnitsCache uiUnitsCache,
InstanceStoreManager instanceStoreManager
) : IRootObjectBuilder<NAV.ModelItem>
{
@@ -43,14 +42,19 @@ public class NavisworksRootObjectBuilder(
)
{
#if DEBUG
// This is a temporary workaround to disable node merging for debugging purposes - false is default, true is for debugging
SkipNodeMerging = true;
#endif
using var activity = activityFactory.Start("Build");
ValidateInputs(navisworksModelItems, projectId, onOperationProgressed);
// 2. Initialize root collection
var rootCollection = InitializeRootCollection();
// InstanceStoreManager is scoped - starts fresh for each conversion session
// 3. Convert all model items and store results
(Dictionary<string, Base?> convertedElements, List<SendConversionResult> conversionResults) =
await ConvertModelItemsAsync(navisworksModelItems, projectId, onOperationProgressed, cancellationToken);
@@ -62,6 +66,8 @@ public class NavisworksRootObjectBuilder(
await AddProxiesToCollection(rootCollection, navisworksModelItems, groupedNodes);
// rootCollection.elements will contain two Collections: one for geometry definitions and one for the main elements
var geometryDefinitionsCollection = new Collection
{
name = "Geometry Definitions",
@@ -153,10 +159,12 @@ public class NavisworksRootObjectBuilder(
Dictionary<string, List<NAV.ModelItem>> groupedNodes
)
{
// First build the grouped nodes as before
var finalElements = new List<Base>();
var processedPaths = new HashSet<string>();
AddGroupedElements(finalElements, convertedBases, groupedNodes, processedPaths);
// If hierarchy mode is enabled, reorganize into proper nested structure
if (converterSettings.Current.User.PreserveModelHierarchy)
{
var hierarchyBuilder = new NavisworksHierarchyBuilder(
@@ -165,9 +173,12 @@ public class NavisworksRootObjectBuilder(
elementSelectionService
);
return hierarchyBuilder.BuildHierarchy();
var hierarchy = hierarchyBuilder.BuildHierarchy();
return hierarchy;
}
// Otherwise continue with flat mode
AddRemainingElements(finalElements, convertedBases, processedPaths);
return finalElements;
}
@@ -224,17 +235,24 @@ public class NavisworksRootObjectBuilder(
}
}
private (string name, string path) GetElementNameAndPath(string applicationId)
private (string name, string path) GetContext(string applicationId)
{
var modelItem = elementSelectionService.GetModelItemFromPath(applicationId);
var context = HierarchyHelper.ExtractContext(modelItem);
return (context.Name, context.Path);
}
/// <summary>
/// Processes and adds any remaining non-grouped elements.
/// </summary>
/// <remarks>
/// Handles both Collection and Base type elements differently.
/// Only processes elements that weren't handled in grouped processing.
/// </remarks>
private NavisworksObject CreateNavisworksObject(string groupKey, List<Base> siblingBases)
{
string cleanParentPath = ElementSelectionHelper.GetCleanPath(groupKey);
(string name, string path) = GetElementNameAndPath(cleanParentPath);
(string name, string path) = GetContext(cleanParentPath);
return new NavisworksObject
{
@@ -242,11 +260,16 @@ public class NavisworksRootObjectBuilder(
displayValue = siblingBases.SelectMany(b => b["displayValue"] as List<Base> ?? []).ToList(),
properties = siblingBases.First()["properties"] as Dictionary<string, object?> ?? [],
units = converterSettings.Current.Derived.SpeckleUnits,
applicationId = groupKey,
applicationId = groupKey, // Use the full composite key as applicationId to preserve uniqueness
["path"] = path
};
}
/// <summary>
/// Creates a NavisworksObject from a single converted base.
/// </summary>
/// <param name="convertedBase">The converted Speckle Base object.</param>
/// <returns>A new NavisworksObject containing the converted data.</returns>
private NavisworksObject? CreateNavisworksObject(Base convertedBase)
{
if (convertedBase.applicationId == null)
@@ -254,16 +277,14 @@ public class NavisworksRootObjectBuilder(
return null;
}
(string name, string path) = GetElementNameAndPath(convertedBase.applicationId);
var units = uiUnitsCache.Ensure();
(string name, string path) = GetContext(convertedBase.applicationId);
return new NavisworksObject
{
name = name,
displayValue = convertedBase["displayValue"] as List<Base> ?? [],
properties = convertedBase["properties"] as Dictionary<string, object?> ?? [],
units = units.ToString(),
units = converterSettings.Current.Derived.SpeckleUnits,
applicationId = convertedBase.applicationId,
["path"] = path
};
@@ -289,16 +310,36 @@ public class NavisworksRootObjectBuilder(
rootCollection[ProxyKeys.COLOR] = colors;
}
// Add instance definition proxies from dual store
var instanceDefinitionProxies = instanceStoreManager.GetInstanceDefinitionProxies();
logger.LogDebug("Retrieved {Count} instance definition proxies from store", instanceDefinitionProxies.Count);
if (instanceDefinitionProxies.Count > 0)
{
rootCollection[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies.ToList();
logger.LogDebug(
"Added {Count} instance definition proxies to root collection under key '{Key}'",
instanceDefinitionProxies.Count,
ProxyKeys.INSTANCE_DEFINITION
);
}
else
{
logger.LogDebug("No instance definition proxies to add to root collection");
}
return Task.CompletedTask;
}
/// <summary>
/// Converts a single Navisworks item to a Speckle object.
/// </summary>
/// <remarks>
/// Attempts to retrieve from cache first.
/// Falls back to fresh conversion if not cached.
/// Logs errors but doesn't throw exceptions.
/// </remarks>
/// <returns>A SendConversionResult indicating success or failure.</returns>
private SendConversionResult ConvertNavisworksItem(
NAV.ModelItem navisworksItem,
Dictionary<string, Base?> convertedBases,
@@ -281,7 +281,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -281,7 +281,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -281,7 +281,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -226,7 +226,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -219,7 +219,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -94,7 +94,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
public List<ISendFilter> GetSendFilters() =>
[
new RevitSelectionFilter { IsDefault = true },
new RevitSelectionFilter() { IsDefault = true },
new RevitViewsFilter(_revitContext),
new RevitCategoriesFilter(_revitContext)
];
@@ -58,7 +58,6 @@ 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>();
@@ -75,6 +74,7 @@ public static class ServiceRegistration
serviceCollection.AddSingleton<RevitUtils>();
serviceCollection.AddSingleton<IFailuresPreprocessor, HideWarningsFailuresPreprocessor>();
serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc());
serviceCollection.AddScoped<LocalToGlobalConverterUtils>();
// operation progress manager
@@ -1,6 +1,5 @@
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;
using Speckle.Converters.RevitShared.Extensions;
namespace Speckle.Connectors.Revit.HostApp;
@@ -24,11 +23,11 @@ public class ElementUnpacker
// Step 1: unpack groups
var atomicObjects = UnpackElements(selectionElements, doc);
// Step 2: Deduplicate parent-child elements in selection
// Removes child elements (mullions, panels, top rails, stacked wall members) when
// their parent element is also selected, since parents include children in their conversion.
// Children are only converted independently when their parent is NOT in the selection.
return RemoveKnownChildElementsWhenParentPresent(atomicObjects, doc);
// Step 2: pack curtain wall elements, once we know the full extent of our flattened item list.
// The behaviour we're looking for:
// If parent wall is part of selection, does not select individual elements out. Otherwise, selects individual elements (Panels, Mullions) as atomic objects.
// NOTE: this also conditionally "packs" stacked wall elements if their parent is present. See detailed note inside the function.
return PackCurtainWallElementsAndStackedWalls(atomicObjects, doc);
}
/// <summary>
@@ -109,7 +108,7 @@ public class ElementUnpacker
// We use the nullable document (happiness level 5/10) for the sake of linked models - bc we use this function in 2 different places
// 1- RootObjectBuilder with linked model document - otherwise we cannot unpack elements from correct document.
// 2- Evicting the cache while introducing the settings
private List<Element> RemoveKnownChildElementsWhenParentPresent(List<Element> elements, Document doc)
private List<Element> PackCurtainWallElementsAndStackedWalls(List<Element> elements, Document doc)
{
//just used for contains so use ToHashSet
var ids = elements.Select(el => el.Id).ToHashSet();
@@ -132,37 +131,64 @@ public class ElementUnpacker
// If you wonder why revit is driving people to insanity, this is one of those moments.
// See [CNX-851: Stacked Wall Duplicate Geometry or Materials not applied](https://linear.app/speckle/issue/CNX-851/stacked-wall-duplicate-geometry-or-materials-not-applied)
|| (element is Wall { IsStackedWallMember: true } wall && ids.Contains(wall.StackedWallOwnerId))
// Railings: Remove TopRail when parent railing is selected
// Prevents duplication since railing converter includes TopRail as a child element
// TODO: Consider adding HandRail support (also inherits from ContinuousRail)
|| (
element is TopRail topRail
&& doc.GetElement(topRail.HostRailingId) is Railing railing
&& ids.Contains(railing.Id)
)
);
return elements;
}
/// <summary>
/// Returns element IDs and their known child element IDs for cache tracking.
/// Uses <see cref="ElementExtensions.GetKnownChildrenElements"/> to determine which children to include.
/// Given a set of atomic elements, it will return a list of all their ids as well as their subelements. This currently handles <b>curtain walls</b> and <b>stacked walls</b>.
/// This might not be an exhaustive list of valid objects with "subelements" in revit, and will need revisiting.
/// </summary>
/// <param name="elements">Elements to process</param>
/// <returns>Flattened list of parent and child element IDs</returns>
/// <param name="elements"></param>
/// <returns></returns>
public List<string> GetElementsAndSubelementIdsFromAtomicObjects(List<Element> elements)
{
var ids = new HashSet<string>();
foreach (var element in elements)
{
// add the element's own ID
ids.Add(element.Id.ToString());
// add all known children IDs using the extension method. trying to consolidate duplication here with converter
foreach (var childId in element.GetKnownChildrenElements())
switch (element)
{
ids.Add(childId.ToString());
case Wall wall:
if (wall.CurtainGrid is { } grid)
{
foreach (var mullionId in grid.GetMullionIds())
{
ids.Add(mullionId.ToString());
}
foreach (var panelId in grid.GetPanelIds())
{
ids.Add(panelId.ToString());
}
}
else if (wall.IsStackedWall)
{
foreach (var stackedWallId in wall.GetStackedWallMemberIds())
{
ids.Add(stackedWallId.ToString());
}
}
break;
case FootPrintRoof footPrintRoof:
if (footPrintRoof.CurtainGrids is { } gs)
{
foreach (CurtainGrid roofGrid in gs)
{
foreach (var mullionId in roofGrid.GetMullionIds())
{
ids.Add(mullionId.ToString());
}
foreach (var panelId in roofGrid.GetPanelIds())
{
ids.Add(panelId.ToString());
}
}
}
break;
default:
break;
}
ids.Add(element.Id.ToString());
}
return ids.ToList();
@@ -1,87 +0,0 @@
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,7 +24,6 @@ public class RevitRootObjectBuilder(
ISendConversionCache sendConversionCache,
ElementUnpacker elementUnpacker,
LevelUnpacker levelUnpacker,
ViewUnpacker viewUnpacker,
IThreadContext threadContext,
SendCollectionManager sendCollectionManager,
ILogger<RevitRootObjectBuilder> logger,
@@ -241,7 +240,6 @@ 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);
@@ -262,13 +260,6 @@ 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,7 +24,6 @@
<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" />
@@ -61,4 +60,4 @@
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
</ItemGroup>
</Project>
</Project>
@@ -325,7 +325,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -29,8 +29,8 @@
<ItemGroup>
<PackageReference Include="GrasshopperAsyncComponent" />
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="System.Resources.Extensions" />
</ItemGroup>
@@ -325,7 +325,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -15,7 +15,7 @@ public class TokenUrlComponent : GH_Component
{
public TokenUrlComponent()
: base(
"Speckle Model URL with Token",
"Speckle Model URL",
"URL",
"Create a Speckle model link using URL and developer token",
ComponentCategories.PRIMARY_RIBBON,
@@ -27,7 +27,8 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
protected override Bitmap Icon => Resources.speckle_properties_expand;
public override GH_Exposure Exposure => GH_Exposure.secondary;
protected override void RegisterInputParams(GH_InputParamManager pManager) =>
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddParameter(
new SpecklePropertyGroupParam(),
"Properties",
@@ -35,6 +36,7 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
"Speckle Properties to expand",
GH_ParamAccess.item
);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager) { }
@@ -130,7 +130,7 @@ public class ReceiveAsyncComponent : GH_AsyncComponent<ReceiveAsyncComponent>
{
var autoReceiveMi = Menu_AppendItem(
menu,
"Load new versions automatically",
"Load automatically",
(s, e) =>
{
AutoReceive = !AutoReceive;
@@ -447,89 +447,73 @@ public sealed class ReceiveComponentWorker : WorkerInstance<ReceiveAsyncComponen
}
using var scope = PriorityLoader.CreateScopeForActiveDocument();
try
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)
{
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);
rootPropertiesGoo = new SpecklePropertyGroupGoo(rootCollection.properties);
}
finally
// 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)
{
SpeckleConversionContext.EndCurrent();
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);
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,102 +143,95 @@ public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInpu
}
using var scope = PriorityLoader.CreateScopeForActiveDocument();
var clientFactory = scope.ServiceProvider.GetRequiredService<IClientFactory>();
var receiveOperation = scope.ServiceProvider.GetRequiredService<GrasshopperReceiveOperation>();
try
// Do the thing 👇🏼
Account? account = input.Resource.Account.GetAccount(scope);
if (account is null)
{
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 };
throw new SpeckleAccountManagerException("No default account was found");
}
finally
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>(_ =>
{
SpeckleConversionContext.EndCurrent();
// 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);
// 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(IServiceScope? scope = null)
public static void SetupCurrent()
{
if (s_currentContext != null)
{
return;
}
s_scope = scope ?? PriorityLoader.CreateScopeForActiveDocument();
s_scope = PriorityLoader.CreateScopeForActiveDocument();
s_currentContext = s_scope.Get<SpeckleConversionContext>();
}
@@ -60,7 +60,6 @@ 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,20 +1,12 @@
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;
using Speckle.Converters.Common.ToHost;
using Speckle.Converters.Rhino;
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.
@@ -27,401 +19,184 @@ namespace Speckle.Connectors.GrasshopperShared.Operations.Receive;
internal sealed class LocalToGlobalMapHandler
{
public Dictionary<string, SpeckleGeometryWrapper> ConvertedObjectsMap { get; } = new();
public readonly GrasshopperCollectionRebuilder CollectionRebuilder;
// injected via constructor (DI-managed)
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
private readonly ILogger<LocalToGlobalMapHandler> _logger;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
// 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!;
private readonly TraversalContextUnpacker _traversalContextUnpacker;
private readonly GrasshopperColorUnpacker _colorUnpacker;
private readonly GrasshopperMaterialUnpacker _materialUnpacker;
public LocalToGlobalMapHandler(
IDataObjectInstanceRegistry dataObjectInstanceRegistry,
ILogger<LocalToGlobalMapHandler> logger,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
{
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
_logger = logger;
_settingsStore = settingsStore;
}
/// <summary>
/// Initializes the handler with per-operation data.
/// Must be called before using ConvertAtomicObjects or ConvertBlockInstances.
/// </summary>
public LocalToGlobalMapHandler Initialize(
TraversalContextUnpacker traversalContextUnpacker,
GrasshopperColorUnpacker colorUnpacker,
GrasshopperMaterialUnpacker materialUnpacker,
GrasshopperCollectionRebuilder collectionRebuilder,
IReadOnlyCollection<InstanceDefinitionProxy>? definitionProxies
GrasshopperColorUnpacker colorUnpacker,
GrasshopperMaterialUnpacker materialUnpacker
)
{
_traversalContextUnpacker = traversalContextUnpacker;
_colorUnpacker = colorUnpacker;
_materialUnpacker = materialUnpacker;
CollectionRebuilder = collectionRebuilder;
_definitionProxies = definitionProxies;
return this;
}
/// <summary>
/// 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
/// Converts atomic object from TraversalContext to SpeckleObjectWrapper.
/// </summary>
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)
public void ConvertAtomicObject(TraversalContext atomicContext)
{
var obj = atomicContext.Current;
var objId = obj.applicationId ?? obj.id;
if (objId is null || ConvertedObjectsMap.ContainsKey(objId))
if (objId == 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);
// get path and collection
var path = _traversalContextUnpacker.GetCollectionPath(atomicContext).ToList();
var objectCollection = CollectionRebuilder.GetOrCreateSpeckleCollectionFromPath(
path,
_colorUnpacker,
_materialUnpacker
);
// nothing converted - nothing to do
if (converted.Count == 0)
{
return;
}
// handle normal DataObject (has converted geometry)
if (obj is DataObject normalDataObject)
{
var geometries = ConvertToGeometryWrappers(converted);
var dataObjectWrapper = CreateDataObjectWrapper(normalDataObject, geometries, path, objectCollection);
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)
{
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
};
ConvertedObjectsMap[objId] = wrapper;
CollectionRebuilder.AppendSpeckleGrasshopperObject(wrapper, path, _colorUnpacker, _materialUnpacker);
}
}
}
catch (Exception ex) when (!ex.IsFatal())
{
// 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();
// Always create collection - consumed objects will be cleaned up later
var objectCollection = CollectionRebuilder.GetOrCreateSpeckleCollectionFromPath(
path,
_colorUnpacker,
_materialUnpacker
);
var entry = _dataObjectInstanceRegistry.GetEntries()[dataObjectId];
var resolvedGeometries = ResolveInstanceProxiesToGeometries(entry.InstanceProxies);
var dataObjectWrapper = CreateDataObjectWrapper(dataObject, resolvedGeometries, path, objectCollection);
if (obj is Speckle.Objects.Data.DataObject dataObject)
{
// get color and mat on dataobject first
Color? dataObjColor = _colorUnpacker.Cache.TryGetValue(
dataObject.applicationId ?? "",
out var cachedDataObjColor
)
? cachedDataObjColor
: null;
CollectionRebuilder.AppendSpeckleGrasshopperObject(dataObjectWrapper, path, _colorUnpacker, _materialUnpacker);
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);
}
else
{
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)
{
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
};
// Always add to both map and collections
ConvertedObjectsMap[objId] = wrapper;
CollectionRebuilder.AppendSpeckleGrasshopperObject(wrapper, 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);
// TODO: throw?
}
}
/// <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>
public void ConvertBlockInstances(IReadOnlyCollection<TraversalContext> blockInstances)
/// <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
)
{
// 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(
filteredBlockInstances,
_definitionProxies,
blocks,
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();
// transform the GeometryBase
transformedWrapper.GeometryBase.NotNull().Transform(transform);
// keep Base and GeometryBase in sync (fixed as of CNX-2847)
transformedWrapper.Base = SpeckleConversionContext.Current.ConvertToSpeckle(transformedWrapper.GeometryBase);
// preserve metadata from original Base
transformedWrapper.Base.applicationId = definitionObject.Base.applicationId;
transformedWrapper.Base["units"] = _settingsStore.Current.SpeckleUnits;
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;
}
}
@@ -79,9 +79,6 @@ public class SpecklePropertyGoo : GH_Goo<object>, ISpecklePropertyGoo
case int i:
Value = i;
return true;
case long l:
Value = l;
return true;
case string s:
Value = s;
return true;
@@ -15,7 +15,6 @@ 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;
@@ -59,8 +58,6 @@ 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>();
@@ -8,7 +8,6 @@
<StartProgram>$(ProgramFiles)\Rhino $(RhinoVersion)\System\Rhino.exe</StartProgram>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UseWpf>true</UseWpf>
<PlatformTarget>x64</PlatformTarget>
<UseWindowsForms>true</UseWindowsForms>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
</PropertyGroup>
@@ -306,7 +306,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -7,15 +7,14 @@
<TargetExt>.rhp</TargetExt>
<StartProgram>$(ProgramFiles)\Rhino $(RhinoVersion)\System\Rhino.exe</StartProgram>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<PlatformTarget>x64</PlatformTarget>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
@@ -306,7 +306,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -19,8 +19,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all"/>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.21.25188.17001"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.21.25188.17001"/>
</ItemGroup>
<ItemGroup>
@@ -29,20 +29,20 @@
},
"RhinoCommon": {
"type": "Direct",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "PDKR9GwqyUXUkTulV4J0dzDIf/aWqSJkL7nkS8ReAx8xhnt/+RQpE8gTjOSCmkSU2tjG6WzclowbTxwMTU7VAA==",
"requested": "[8.21.25188.17001, )",
"resolved": "8.21.25188.17001",
"contentHash": "Wo6JXheyDBvilyIwDY6xZCQJC4170jzAlTSlMgh8yokUY+vYyCl4KJVXZofIOynNt/xx5wLqb2On5gZZekXR6w==",
"dependencies": {
"System.Drawing.Common": "7.0.0"
}
},
"RhinoWindows": {
"type": "Direct",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "I/+++piwtYTue+iAAQqcMF5QlontqwNnC7Leyhiv2FiF8JpAl6K44ZsJqB7ZEUC6ns0LDfa3mbFzQwUfHwYumQ==",
"requested": "[8.21.25188.17001, )",
"resolved": "8.21.25188.17001",
"contentHash": "9zqCorcLRBeiW/j1RTwUS4E7bnZetAdA9WDdtd/AQccjOpxdtw76wdN+ciyQ6qslseWkwZ9qSBeh7QaM800Ntw==",
"dependencies": {
"RhinoCommon": "[8.25.25328.11001]"
"RhinoCommon": "[8.21.25188.17001]"
}
},
"Speckle.InterfaceGenerator": {
@@ -235,7 +235,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -1,87 +0,0 @@
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Rhino.Extensions;
using Speckle.Converters.Common;
using Speckle.Converters.Common.ToHost;
using Speckle.Converters.Rhino;
using Speckle.Sdk;
namespace Speckle.Connectors.Rhino.HostApp;
/// <summary>
/// Groups block instances created from DataObject with InstanceProxies as display values and applies DataObject metadata.
/// </summary>
public class DataObjectInstanceGrouper
{
private readonly IConverterSettingsStore<RhinoConversionSettings> _converterSettings;
private readonly ILogger<DataObjectInstanceGrouper> _logger;
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
public DataObjectInstanceGrouper(
IConverterSettingsStore<RhinoConversionSettings> converterSettings,
ILogger<DataObjectInstanceGrouper> logger,
IDataObjectInstanceRegistry dataObjectInstanceRegistry
)
{
_converterSettings = converterSettings;
_logger = logger;
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
}
/// <summary>
/// After all instances have been created, we then run through the data object instance registry to see which instances
/// belonged to a data object. The method then groups all instances to "re-assemble" the original data object and
/// applies the properties of the data object on to the instances.
/// </summary>
/// <remarks>
/// This is a deferred action and can only occur once the RhinoInstanceBaker has done its thing.
/// </remarks>
public void GroupAndApplyProperties()
{
var doc = _converterSettings.Current.Document;
var entries = _dataObjectInstanceRegistry.GetEntries(); // see docstring
foreach (var kvp in entries)
{
var dataObjectId = kvp.Key;
var entry = kvp.Value;
try
{
var instanceIds = _dataObjectInstanceRegistry.GetInstanceIdsForDataObject(dataObjectId);
if (instanceIds.Count == 0)
{
continue;
}
// create group, name the group and apply properties
using var dataObjectAtts = entry.DataObject.GetAttributes();
var groupName = dataObjectAtts.Name;
var groupIndex = doc.Groups.Add(groupName, instanceIds.Select(id => new Guid(id)));
if (groupIndex >= 0)
{
// apply properties to each instance (doing this on an instance level because setting to group doesn't work)
foreach (var instanceId in instanceIds)
{
var rhinoObj = doc.Objects.FindId(new Guid(instanceId));
if (rhinoObj != null)
{
// set the name from DataObject
rhinoObj.Attributes.Name = dataObjectAtts.Name;
// copy all user strings
foreach (var key in dataObjectAtts.GetUserStrings().AllKeys)
{
rhinoObj.Attributes.SetUserString(key, dataObjectAtts.GetUserString(key));
}
rhinoObj.CommitChanges();
}
}
}
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to group DataObject instances {dataObjectId}", dataObjectId);
}
}
}
}
@@ -6,7 +6,6 @@ using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Instances;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Rhino.Extensions;
using Speckle.Converters.Common.ToHost;
using Speckle.DoubleNumerics;
using Speckle.Sdk;
using Speckle.Sdk.Common;
@@ -23,21 +22,18 @@ public class RhinoInstanceBaker : IInstanceBaker<IReadOnlyCollection<string>>
private readonly RhinoLayerBaker _layerBaker;
private readonly RhinoColorBaker _colorBaker;
private readonly ILogger<RhinoInstanceBaker> _logger;
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
public RhinoInstanceBaker(
RhinoLayerBaker layerBaker,
RhinoMaterialBaker rhinoMaterialBaker,
RhinoColorBaker colorBaker,
ILogger<RhinoInstanceBaker> logger,
IDataObjectInstanceRegistry dataObjectInstanceRegistry
ILogger<RhinoInstanceBaker> logger
)
{
_layerBaker = layerBaker;
_materialBaker = rhinoMaterialBaker;
_colorBaker = colorBaker;
_logger = logger;
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
}
/// <summary>
@@ -159,9 +155,6 @@ public class RhinoInstanceBaker : IInstanceBaker<IReadOnlyCollection<string>>
applicationIdMap[instanceProxyId] = new List<string>() { id.ToString() };
createdObjectIds.Add(id.ToString());
conversionResults.Add(new(Status.SUCCESS, instanceProxy, id.ToString(), "Instance (Block)"));
// link this baked instance back to its DataObject if it came from one (the method handles the check)
_dataObjectInstanceRegistry.LinkInstanceToDataObject(instanceProxyId, id.ToString());
}
}
catch (Exception ex) when (!ex.IsFatal())
@@ -123,12 +123,6 @@ public class RhinoLayerBaker : TraversalContextUnpacker
}
var cleanNewLayerName = RhinoUtils.CleanLayerName(collection.name);
if (!ModelComponent.IsValidComponentName(cleanNewLayerName))
{
throw new SpeckleException($"Layer name '{currentLayerName}' is not valid");
}
Layer newLayer = new() { Name = cleanNewLayerName, ParentLayerId = previousLayer?.Id ?? Guid.Empty };
// set material
@@ -156,7 +150,7 @@ public class RhinoLayerBaker : TraversalContextUnpacker
int index = currentDocument.Layers.Add(newLayer);
if (index == -1)
{
throw new SpeckleException($"Could not create layer '{currentLayerName}'");
throw new SpeckleException($"Could not create layer '{currentLayerName}'.");
}
_hostLayerCache.Add(currentLayerName, index);
@@ -1,41 +1,27 @@
using System.Text;
namespace Speckle.Connectors.Rhino.HostApp;
public static class RhinoUtils
{
private static readonly HashSet<char> s_skipChars = ['[', ']', '(', ')', '{', '}'];
private static readonly HashSet<char> s_replaceWithHyphen = [':', ';'];
public static string CleanBlockDefinitionName(string str) => str.Replace('/', '_').Replace('\\', '_');
public static string CleanBlockDefinitionName(string str)
{
return ReplaceChars(str, @"\/", "_");
}
// Cleans up layer names to be "rhino" proof. Note this can be improved, as "()[] and {}" are illegal only at the start.
// https://docs.mcneel.com/rhino/6/help/en-us/index.htm#information/namingconventions.htm?Highlight=naming
public static string CleanLayerName(string str)
{
var sb = new StringBuilder(str.Length);
str = ReplaceChars(str, @"[](){}", "");
return ReplaceChars(str, @":;", "-");
}
foreach (char c in str)
private static string ReplaceChars(string str, string invalidChars, string replaceString)
{
foreach (char c in invalidChars)
{
if (char.IsControl(c))
{
continue; // skip control characters (shoutout cnx-2809)
}
if (s_skipChars.Contains(c))
{
continue; // skip brackets
}
if (s_replaceWithHyphen.Contains(c))
{
sb.Append('-');
continue;
}
sb.Append(c);
str = str.Replace(c.ToString(), replaceString);
}
return sb.ToString();
return str;
}
}
@@ -1,65 +0,0 @@
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;
}
}
@@ -10,7 +10,6 @@ using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.Rhino.Extensions;
using Speckle.Connectors.Rhino.HostApp;
using Speckle.Converters.Common;
using Speckle.Converters.Common.ToHost;
using Speckle.Converters.Rhino;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
@@ -37,8 +36,6 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
private readonly ISdkActivityFactory _activityFactory;
private readonly IThreadContext _threadContext;
private readonly IReceiveConversionHandler _conversionHandler;
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
private readonly DataObjectInstanceGrouper _dataObjectInstanceGrouper;
public RhinoHostObjectBuilder(
IRootToHostConverter converter,
@@ -51,9 +48,7 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
RhinoGroupBaker groupBaker,
ISdkActivityFactory activityFactory,
IThreadContext threadContext,
IReceiveConversionHandler conversionHandler,
IDataObjectInstanceRegistry dataObjectInstanceRegistry,
DataObjectInstanceGrouper dataObjectInstanceGrouper
IReceiveConversionHandler conversionHandler
)
{
_converter = converter;
@@ -67,8 +62,6 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
_activityFactory = activityFactory;
_threadContext = threadContext;
_conversionHandler = conversionHandler;
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
_dataObjectInstanceGrouper = dataObjectInstanceGrouper;
}
#pragma warning disable CA1506
@@ -195,14 +188,8 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
if (conversionIds.Count == 0)
{
// Don't throw if this DataObject was registered for instance baking
if (!_dataObjectInstanceRegistry.IsRegistered(obj.applicationId ?? obj.id.NotNull()))
{
throw new ConversionException("Object did not convert to any native geometry");
}
// Skip normal processing - will be handled by DataObjectInstanceGrouper
return;
// TODO: add this condition to report object - same as in autocad
throw new ConversionException("Object did not convert to any native geometry");
}
// 4: log
@@ -245,10 +232,7 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
conversionResults.UnionWith(instanceConversionResults); // add instance conversion results to our list
}
// 7.1 Group DataObject instances and apply metadata
_dataObjectInstanceGrouper.GroupAndApplyProperties();
// 7.2 Normal group creation
// 7 - Create groups
if (unpackedRoot.GroupProxies is not null)
{
_groupBaker.BakeGroups(unpackedRoot.GroupProxies, applicationIdMap, baseLayerName);
@@ -260,9 +244,6 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
private void PreReceiveDeepClean(string baseLayerName)
{
// Clear DataObject instance registry at start of new build
_dataObjectInstanceRegistry.Clear();
// Remove all previously received layers and render materials from the document
int rootLayerIndex = _converterSettings.Current.Document.Layers.Find(
Guid.Empty,
@@ -373,7 +354,7 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
if (objCount > 1)
{
var groupIndex = _converterSettings.Current.Document.Groups.Add(
$"{originatingObject.speckle_type.Split('.').Last()} - {parentId} ({baseLayerName})",
$@"{originatingObject.speckle_type.Split('.').Last()} - {parentId} ({baseLayerName})",
objectIds
);
@@ -33,7 +33,6 @@ 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;
@@ -47,7 +46,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
RhinoGroupUnpacker groupUnpacker,
RhinoMaterialUnpacker materialUnpacker,
RhinoColorUnpacker colorUnpacker,
RhinoViewUnpacker viewUnpacker,
PropertiesExtractor propertiesExtractor,
ILogger<RhinoRootObjectBuilder> logger,
ISdkActivityFactory activityFactory
@@ -61,7 +59,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
_rootToSpeckleConverter = rootToSpeckleConverter;
_materialUnpacker = materialUnpacker;
_colorUnpacker = colorUnpacker;
_viewUnpacker = viewUnpacker;
_propertiesExtractor = propertiesExtractor;
_logger = logger;
_activityFactory = activityFactory;
@@ -125,30 +122,20 @@ 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);
}
@@ -22,7 +22,6 @@ using Speckle.Connectors.Rhino.Operations.Receive;
using Speckle.Connectors.Rhino.Operations.Send;
using Speckle.Connectors.Rhino.Operations.Send.Settings;
using Speckle.Connectors.Rhino.Plugin;
using Speckle.Converters.Common.ToHost;
using Speckle.Sdk.Models.GraphTraversal;
namespace Speckle.Connectors.Rhino.DependencyInjection;
@@ -76,7 +75,7 @@ public static class ServiceRegistration
InstanceObjectsManager<RhinoObject, List<string>>
>();
// register unpackers and bakers
// Register unpackers and bakers
serviceCollection.AddScoped<RhinoLayerUnpacker>();
serviceCollection.AddScoped<RhinoLayerBaker>();
@@ -92,15 +91,9 @@ public static class ServiceRegistration
serviceCollection.AddScoped<RhinoColorBaker>();
serviceCollection.AddScoped<RhinoColorUnpacker>();
serviceCollection.AddScoped<RhinoViewUnpacker>();
serviceCollection.AddScoped<PropertiesExtractor>();
serviceCollection.AddScoped<RevitMappingResolver>();
// handling proxified display values
serviceCollection.AddScoped<IDataObjectInstanceRegistry, DataObjectInstanceRegistry>();
serviceCollection.AddScoped<DataObjectInstanceGrouper>();
// register helpers
serviceCollection.AddScoped<RhinoLayerHelper>();
serviceCollection.AddScoped<RhinoObjectHelper>();
@@ -23,9 +23,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RhinoSendBinding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RhinoSelectionBinding.cs" />
<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" />
@@ -64,4 +62,4 @@
<Compile Include="$(MSBuildThisFileDirectory)Plugin\Speckle.Connectors.RhinoCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\Speckle.Connectors.RhinoPlugin.cs" />
</ItemGroup>
</Project>
</Project>
@@ -325,7 +325,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -406,7 +406,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -406,7 +406,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -210,7 +210,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -210,7 +210,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -4,7 +4,6 @@ using Speckle.Objects;
using Speckle.Objects.Data;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.AutocadShared.ToHost.Geometry;
@@ -43,20 +42,14 @@ public class DataObjectConverter : IToHostTopLevelConverter, ITypedConverter<Dat
public List<(ADB.Entity a, Base b)> Convert(DataObject target)
{
var result = new List<(ADB.Entity a, Base b)>();
if (target.displayValue.Count > 0 && target.displayValue[0] is InstanceProxy)
{
return []; // return empty - defer to instance baker
}
foreach (var item in target.displayValue)
{
result.AddRange(ConvertDisplayObject(item));
}
return result;
}
private IEnumerable<(ADB.Entity a, Base b)> ConvertDisplayObject(Base displayObject)
public IEnumerable<(ADB.Entity a, Base b)> ConvertDisplayObject(Base displayObject)
{
switch (displayObject)
{
@@ -17,4 +17,4 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellSectionResolver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\EtabsObjectToSpeckleConverter.cs" />
</ItemGroup>
</Project>
</Project>
@@ -219,7 +219,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -219,7 +219,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -259,7 +259,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -260,7 +260,6 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "[2.2.0, )",
"Speckle.Connectors.Logging": "[1.0.0, )",
"Speckle.Converters.Common": "[1.0.0, )",
"Speckle.Objects": "[3.9.0, )",
"Speckle.Sdk": "[3.9.0, )",
"Speckle.Sdk.Dependencies": "[3.9.0, )"
@@ -1,14 +1,10 @@
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
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,
IPropertyConverter propertyConverter
)
public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
{
internal Dictionary<string, object?>? GetPropertySets(NAV.ModelItem modelItem)
{
@@ -22,17 +18,6 @@ public class PropertySetsExtractor(
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
@@ -43,13 +28,10 @@ public class PropertySetsExtractor(
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)
{
if (ShouldSkipCategory(propertyCategory))
if (IsCategoryToBeSkipped(propertyCategory))
{
continue;
}
@@ -58,18 +40,23 @@ public class PropertySetsExtractor(
foreach (var property in propertyCategory.Properties)
{
var sanitizedName = SanitizePropertyName(property.DisplayName);
var propertyValue = propertyConverter.ConvertPropertyValue(property.Value, modelUnits, property.DisplayName);
string sanitizedName = SanitizePropertyName(property.DisplayName);
var propertyValue = ConvertPropertyValue(property.Value, settingsStore.Current.Derived.SpeckleUnits);
if (propertyValue != null)
{
propertySet[sanitizedName] = propertyValue;
}
}
if (propertySet.Count > 0)
if (propertySet.Count <= 0)
{
propertySetDictionary[SanitizePropertyName(propertyCategory.DisplayName)] = propertySet;
continue;
}
string categoryName = SanitizePropertyName(propertyCategory.DisplayName);
propertySetDictionary[categoryName] = propertySet;
}
return propertySetDictionary;
@@ -1,10 +1,8 @@
using Speckle.Converter.Navisworks.Services;
using Speckle.InterfaceGenerator;
using static Speckle.Converter.Navisworks.Helpers.PropertyHelpers;
namespace Speckle.Converter.Navisworks.ToSpeckle;
[GenerateAutoInterface]
public class RevitBuiltInCategoryExtractor(IPropertyConverter converter) : IRevitBuiltInCategoryExtractor
public sealed class RevitBuiltInCategoryExtractor
{
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";
@@ -15,28 +13,28 @@ public class RevitBuiltInCategoryExtractor(IPropertyConverter converter) : IRevi
/// 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>
public bool TryGetBuiltInCategory(NAV.ModelItem item, out string mapped, int maxDepth = ANCESTOR_AND_SELF_COUNT)
internal static bool TryGetBuiltInCategory(
NAV.ModelItem item,
out string mapped,
int maxDepth = ANCESTOR_AND_SELF_COUNT
)
{
mapped = string.Empty;
// Find the category VariantData up the hierarchy
// Look up the category value, starting at this item and walking up to maxDepth ancestors
var v = FindRevitCategoryInHierarchy(item, maxDepth);
if (v is null)
if (v == null)
{
return false;
}
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();
var name = ConvertPropertyValue(v, "")?.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,8 +11,7 @@ public class HierarchicalPropertyHandler(
PropertySetsExtractor propertySetsExtractor,
ModelPropertiesExtractor modelPropertiesExtractor,
ClassPropertiesExtractor classPropertiesExtractor,
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
IRevitBuiltInCategoryExtractor revitCategoryExtractor
IConverterSettingsStore<NavisworksConversionSettings> settingsStore
) : BasePropertyHandler(propertySetsExtractor, modelPropertiesExtractor)
{
private static string PseudoClassPropertiesKey => "_pseudoClassProperties";
@@ -23,7 +22,7 @@ public class HierarchicalPropertyHandler(
var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem) ?? [];
// Interop-lite mapping for Revit built-in categories
if (_mapRevit && revitCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
if (_mapRevit && RevitBuiltInCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
{
PropertyHelpers.AddPropertyIfNotNullOrEmpty(
propertyDict,
@@ -52,8 +52,7 @@ public static class NavisworksConverterServiceRegistration
// Register ISharedGeometryStore interface using the geometry definitions store for backward compatibility
serviceCollection.AddScoped<ISharedGeometryStore>(provider =>
provider.GetRequiredService<InstanceStoreManager>().GeometryDefinitionsStore
);
provider.GetRequiredService<InstanceStoreManager>().GeometryDefinitionsStore);
// Register settings resolved from factory
serviceCollection.AddScoped<NavisworksConversionSettings>(sp =>
@@ -0,0 +1,60 @@
using System.Runtime.InteropServices;
namespace Speckle.Converter.Navisworks.Helpers;
/// <summary>
/// ComScope - Because Navisworks COM objects are like vampires that never die unless you tell them to.
///
/// This is a RAII (Resource Acquisition Is Initialization) wrapper for COM objects.
/// Think of it as a babysitter that makes sure COM objects get cleaned up properly
/// when you're done with them, preventing memory leaks that would otherwise
/// slowly consume your machine's RAM like a digital Pac-Man.
///
/// Why do we need this?
/// - Navisworks COM API creates objects that live forever unless explicitly released
/// - Forgetting to call Marshal.ReleaseComObject() = memory leak city
/// - Using statements + IDisposable = automatic cleanup when scope ends
/// - One less thing to remember = fewer bugs = happier developers
///
/// Usage: Wrap it in a 'using' statement and let C# handle the cleanup:
/// using var comThing = new ComScope&lt;SomeComType&gt;(myComObject);
/// // Do stuff with comThing.Value
/// // Automatic cleanup happens here when using block ends
///
/// Pro tip: This prevents the "why is Navisworks eating all my RAM?" conversations
/// that happen way too often with COM interop code.
/// </summary>
/// <typeparam name="T">The COM object type we're babysitting</typeparam>
public readonly struct ComScope<T>(T comObject, bool shouldRelease = true) : IDisposable
where T : class
{
public ComScope(T comObject)
: this(comObject, false) { }
private T ComObject { get; } = comObject;
private bool ShouldRelease { get; } = shouldRelease;
public T Value => ComObject;
/// <summary>
/// The magic cleanup method. This gets called automatically when the 'using' block ends.
/// It tells the COM object "your services are no longer required" in a polite way
/// that doesn't crash the application.
/// </summary>
public void Dispose()
{
// Only release if we're supposed to AND the object actually exists
if (ShouldRelease && ComObject != null)
{
try
{
// This is the important bit - tells COM runtime to decrement reference count
Marshal.ReleaseComObject(ComObject);
}
catch (InvalidComObjectException)
{
// Sometimes the object is already gone (maybe someonereleased, ignore
}
}
}
}
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Text.RegularExpressions;
using Speckle.Objects.Geometry;
@@ -8,9 +8,6 @@ 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()
{
@@ -50,15 +47,22 @@ public static class PropertyHelpers
return handler(value, units);
}
// Default case for unsupported types
return value.DataType is NAV.VariantDataType.None or NAV.VariantDataType.Point2D ? null : value.ToString();
}
/// <summary>
/// Adds a property to an object (either a Base object or a Dictionary) if the value is not null or empty.
/// </summary>
/// <param name="baseObject">The object to which the property is to be added. Can be either a Base object or a Dictionary.</param>
/// <param name="propertyName">The name of the property to add.</param>
/// <param name="value">The value of the property.</param>
internal static void AddPropertyIfNotNullOrEmpty(object baseObject, string propertyName, object? value)
{
switch (value)
{
case null:
break;
break; // Do not add null values
case string stringValue:
{
if (!string.IsNullOrEmpty(stringValue))
@@ -74,6 +78,9 @@ public static class PropertyHelpers
}
}
/// <summary>
/// Helper method to assign the property to the base object or dictionary.
/// </summary>
private static void AssignProperty(object baseObject, string propertyName, object value)
{
switch (baseObject)
@@ -89,33 +96,16 @@ public static class PropertyHelpers
}
}
/// <summary>
/// Sanitizes property names by replacing invalid characters with underscores.
/// </summary>
internal static string SanitizePropertyName(string name) =>
name == "Item" ? "Item" : Regex.Replace(name, @"[\.\/\s]", "_");
// Regex pattern from speckle-sharp/Core/Core/Models/DynamicBase.cs IsPropNameValid
name == "Item"
// Item is a reserved term for Indexed Properties: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/indexers/using-indexers
? "Item"
: Regex.Replace(name, @"[\.\/\s]", "_");
internal static bool ShouldSkipCategory(NAV.PropertyCategory propertyCategory) =>
internal static bool IsCategoryToBeSkipped(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()}";
}
@@ -1,4 +1,4 @@
namespace Speckle.Converter.Navisworks.Constants;
namespace Speckle.Converter.Navisworks.Constants;
public static class PathConstants
{
@@ -6,14 +6,3 @@ public static class PathConstants
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_";
}
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Constants;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
@@ -55,9 +54,7 @@ public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
/// </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}"
);
GeometryDefinitionsStore.Geometries.FirstOrDefault(g => g.applicationId == $"geom_{fragmentId}");
/// <summary>
/// Gets an instance definition proxy by its application ID.
@@ -66,7 +63,7 @@ public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
public InstanceDefinitionProxy? GetInstanceDefinitionProxy(string fragmentId) =>
InstanceDefinitionProxiesStore
.Geometries.OfType<InstanceDefinitionProxy>()
.FirstOrDefault(p => p.applicationId == $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}");
.FirstOrDefault(p => p.applicationId == $"def_{fragmentId}");
/// <summary>
/// Adds a geometry definition and corresponding instance definition proxy for shared geometry.
@@ -83,8 +80,8 @@ public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
bool proxyAdded = false;
// Create prefixed IDs for 1:1:1 relationship using base fragment hash
var geometryId = $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}";
var definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}";
var geometryId = $"geom_{fragmentId}";
var definitionId = $"def_{fragmentId}";
_logger.LogDebug("Using GeometryId={GeometryId}, DefinitionId={DefinitionId}", geometryId, definitionId);
@@ -103,6 +100,23 @@ 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,
@@ -134,8 +148,7 @@ public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
/// </summary>
/// <param name="fragmentId">The fragment-based application ID.</param>
/// <returns>True if geometry definition exists in both stores.</returns>
public bool ContainsSharedGeometry(string fragmentId) =>
GeometryDefinitionsStore.Contains($"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}")
// && InstanceDefinitionProxiesStore.Contains($"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}")
public bool ContainsSharedGeometry(string fragmentId) => GeometryDefinitionsStore.Contains($"geom_{fragmentId}")
// && InstanceDefinitionProxiesStore.Contains($"def_{fragmentId}")
;
}
@@ -1,91 +0,0 @@
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
};
}
@@ -1,71 +0,0 @@
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;
}
}
@@ -23,6 +23,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ComScope.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs"/>
@@ -30,8 +31,6 @@
<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"/>
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
@@ -8,6 +8,9 @@ using Speckle.Sdk.Models;
namespace Speckle.Converter.Navisworks.ToSpeckle;
/// <summary>
/// Converts Navisworks ModelItem objects to Speckle Base objects.
/// </summary>
public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter
{
private readonly IConverterManager<IToSpeckleTopLevelConverter> _toSpeckle;
@@ -38,8 +41,11 @@ public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter
}
Type type = target.GetType();
var objectConverter = _toSpeckle.ResolveConverter(type, true);
Base result = objectConverter.Convert(modelItem);
result.applicationId = ElementSelectionHelper.ResolveModelItemToIndexPath(modelItem);
return result;
@@ -1,25 +1,30 @@
using System.Collections.Concurrent;
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.Geometry;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects.Geometry;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
namespace Speckle.Converter.Navisworks.ToSpeckle;
/// <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>
/// <summary>
/// Converts Navisworks geometry to Speckle displayable geometry.
///
/// Note: This class does not implement ITypedConverter{ModelGeometry, Base} because Navisworks geometry
/// conversion requires COM interop access that isn't available through the public ModelGeometry class.
/// The conversion process requires:
/// 1. Convert ModelItem to InwOaPath3 via ComApiBridge
/// 2. Use that to get InwOaFragmentList
/// 3. Process each InwOaFragment3 to generate primitives
/// 4. Convert those primitives to Speckle geometry with appropriate transforms
/// </summary>
public class GeometryToSpeckleConverter(
NavisworksConversionSettings settings,
InstanceStoreManager instanceStoreManager,
@@ -31,9 +36,7 @@ public class GeometryToSpeckleConverter(
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 double SCALE = 1.0; // Default scale factor
private readonly InstanceStoreManager _instanceStoreManager =
instanceStoreManager ?? throw new ArgumentNullException(nameof(instanceStoreManager));
@@ -41,7 +44,42 @@ public class GeometryToSpeckleConverter(
private readonly ILogger<GeometryToSpeckleConverter> _logger =
logger ?? throw new ArgumentNullException(nameof(logger));
internal List<SSM.Base> Convert(NAV.ModelItem modelItem)
// Fragment ID cache for performance optimization
private readonly ConcurrentDictionary<int, string> _fragmentIdCache = new();
// Geometry cache for repeated items
private readonly ConcurrentDictionary<string, List<Base>> _geometryCache = new();
/// <summary>
/// Clears all internal caches. Should be called when starting a new conversion session.
/// </summary>
public void ClearCaches()
{
_fragmentIdCache.Clear();
_geometryCache.Clear();
}
/// <summary>
/// Gets cache statistics for performance monitoring.
/// </summary>
/// <returns>A record containing cache hit counts and sizes</returns>
public (int FragmentIdCacheSize, int GeometryCacheSize, double CacheMemoryEstimateMB) GetCacheStatistics()
{
var fragmentCacheSize = _fragmentIdCache.Count;
var geometryCacheSize = _geometryCache.Count;
// Rough memory estimate (fragment IDs ~50 bytes, geometry objects ~10KB average)
var estimatedMemoryMb = (fragmentCacheSize * 50 + geometryCacheSize * 10240) / (1024.0 * 1024.0);
return (fragmentCacheSize, geometryCacheSize, Math.Round(estimatedMemoryMb, 2));
}
/// <summary>
/// Converts a ModelItem's geometry to Speckle display geometry by accessing the underlying COM objects.
/// When path.Fragments().Count > 1, extracts untransformed base geometry once, stores in SharedGeometryStore,
/// and returns instance references. Otherwise, returns transformed geometry directly.
/// </summary>
internal List<Base> Convert(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
@@ -53,86 +91,89 @@ public class GeometryToSpeckleConverter(
return [];
}
var comSelection = ComApiBridge.ToInwOpSelection([modelItem]);
// Check geometry cache first
var itemId = modelItem.InstanceGuid.ToString();
if (_geometryCache.TryGetValue(itemId, out var cachedGeometry))
{
return cachedGeometry;
}
using var comSelection = new ComScope<InwOpSelection>(ComApiBridge.ToInwOpSelection([modelItem]));
var fragmentStack = new Stack<InwOaFragment3>();
using var paths = new ComScope<InwSelectionPathsColl>(comSelection.Value.Paths());
try
{
var fragmentStack = new Stack<InwOaFragment3>();
var paths = comSelection.Paths();
if (paths == null)
// Check if this geometry is shared across multiple instances
List<Base> result;
if (paths.Value.Count > 0)
{
return [];
}
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 firstPath = paths.Value.Cast<InwOaPath>().First();
var fragmentsCollection = firstPath.Fragments();
foreach (InwOaPath path in paths)
if (fragmentsCollection.Count > 1)
{
CollectFragments(path, fragmentStack);
// Shared geometry - extract base geometry once and return instance reference
result = ProcessSharedGeometry(paths.Value, fragmentStack);
}
else
{
// Single instance geometry - process normally with transforms
foreach (InwOaPath path in paths.Value)
{
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths, true);
result = ProcessFragments(fragmentStack, paths.Value, true);
}
}
finally
else
{
Marshal.ReleaseComObject(paths);
result = [];
}
// Cache the result for future use
if (result.Count > 0)
{
_geometryCache.TryAdd(itemId, result);
}
return result;
}
finally
catch (COMException ex)
{
if (comSelection != null)
{
Marshal.ReleaseComObject(comSelection);
}
_logger.LogError(ex, "COM exception converting geometry for ModelItem {ItemId}", itemId);
return [];
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "Invalid operation converting geometry for ModelItem {ItemId}", itemId);
return [];
}
}
private static void CollectFragments(InwOaPath path, Stack<InwOaFragment3> fragmentStack)
{
var fragments = path.Fragments();
try
using var fragments = new ComScope<InwNodeFragsColl>(path.Fragments());
foreach (var fragment in fragments.Value.OfType<InwOaFragment3>())
{
foreach (var fragment in fragments.OfType<InwOaFragment3>())
if (ValidateFragmentPath(fragment, path))
{
if (AreFragmentPathsEqual(fragment, path))
{
fragmentStack.Push(fragment);
}
}
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
fragmentStack.Push(fragment);
}
}
}
private List<SSM.Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
private List<Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
{
// Generate ID from fragment data for shared geometry
var fragmentId = GenerateFragmentId(paths);
if (string.IsNullOrEmpty(fragmentId))
{
// Fallback to normal processing if we can't generate ID
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
@@ -141,11 +182,14 @@ public class GeometryToSpeckleConverter(
return ProcessFragments(fragmentStack, paths, true);
}
// Check if shared geometry already exists in store
if (_instanceStoreManager.ContainsSharedGeometry(fragmentId))
{
// Return instance reference to existing geometry
return CreateInstanceReference(fragmentId, paths);
}
// Extract untransformed base geometry
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
@@ -153,12 +197,24 @@ public class GeometryToSpeckleConverter(
var baseGeometry = ExtractUntransformedGeometry(fragmentStack);
return baseGeometry == null || !_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometry)
? ProcessFragments(fragmentStack, paths) // default false flag for isSingleObject
: CreateInstanceReference(fragmentId, paths);
if (baseGeometry == null)
{
return ProcessFragments(fragmentStack, paths);
}
// Store both the geometry definition and create the instance definition proxy
if (!_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometry))
{
return ProcessFragments(fragmentStack, paths);
}
// Return instance reference to the newly stored geometry
return CreateInstanceReference(fragmentId, paths);
// Fallback to normal processing if store failed
}
private List<SSM.Base> ProcessFragments(
private List<Base> ProcessFragments(
Stack<InwOaFragment3> fragmentStack,
InwSelectionPathsColl paths,
bool isSingleObject = false
@@ -170,36 +226,40 @@ public class GeometryToSpeckleConverter(
{
var processor = new PrimitiveProcessor(_isUpright);
using var pathFragments = new ComScope<InwNodeFragsColl>(path.Fragments());
var fragmentCount = pathFragments.Value.Count;
foreach (var fragment in fragmentStack)
{
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)
var matrix = fragment.GetLocalToWorldMatrix();
var transform = matrix as InwLTransform3f3;
if (transform?.Matrix is not Array matrixArray)
{
Marshal.ReleaseComObject(fragmentsForCount);
continue;
}
double[] makeNoChange = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
if (isSingleObject || fragmentCount == 1)
{
// Apply coordinate system transformation
processor.LocalToWorldTransformation = transformMatrix;
}
else
{
// For multiple objects, process geometry without transforms
processor.LocalToWorldTransformation = makeNoChange;
}
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
catch (COMException ex)
{
_logger.LogWarning(ex, "COM exception processing fragment, skipping");
}
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);
@@ -208,14 +268,14 @@ public class GeometryToSpeckleConverter(
return ProcessGeometries(callbackListeners);
}
private static bool AreFragmentPathsEqual(InwOaFragment3 fragment, InwOaPath path) =>
private static bool ValidateFragmentPath(InwOaFragment3 fragment, InwOaPath path) =>
fragment.path?.ArrayData is Array fragmentPathData
&& path.ArrayData is Array pathData
&& AreFragmentPathsEqual(fragmentPathData, pathData);
&& IsSameFragmentPath(fragmentPathData, pathData);
private List<SSM.Base> ProcessGeometries(List<PrimitiveProcessor> processors)
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
{
var baseGeometries = new List<SSM.Base>();
var baseGeometries = new List<Base>();
foreach (var processor in processors)
{
@@ -244,6 +304,7 @@ public class GeometryToSpeckleConverter(
{
var triangle = triangles[t];
// No need to worry about disposal of COM across boundaries - we're working with our safe structs
vertices.AddRange(
[
(triangle.Vertex1.X + _transformVector.X) * SCALE,
@@ -288,6 +349,10 @@ public class GeometryToSpeckleConverter(
})
.ToList();
/// <summary>
/// Generates an idempotent ID from fragment path data for shared geometry.
/// Uses the path.Fragments() collection to create a reproducible hash.
/// </summary>
public string GenerateFragmentId(InwSelectionPathsColl paths)
{
try
@@ -297,127 +362,137 @@ public class GeometryToSpeckleConverter(
return string.Empty;
}
// Generate a fast hash code for cache lookup
var pathsHashCode = GenerateFastPathsHashCode(paths);
// Check cache first
if (_fragmentIdCache.TryGetValue(pathsHashCode, out var cachedId))
{
return cachedId;
}
var fragmentHashes = new List<string>();
foreach (var fragments in from InwOaPath path in paths select path.Fragments())
foreach (InwOaPath path in paths)
{
try
var fragments = path.Fragments();
var fragmentIndex = 0;
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
{
var fragmentIndex = 0;
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
if (fragment.path?.ArrayData is not Array pathData)
{
if (fragment.path?.ArrayData is not Array pathData || pathData.Length == 0)
fragmentIndex++;
continue;
}
if (pathData.Length == 0)
{
fragmentIndex++;
continue;
}
try
{
// Check array rank first - COM arrays might be multidimensional
if (pathData.Rank != 1)
{
// Try simple enumeration fallback
var fragmentHashFallback = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHashFallback))
{
fragmentHashes.Add(fragmentHashFallback);
}
fragmentIndex++;
continue;
}
try
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++)
{
if (pathData.Rank != 1)
try
{
var fragmentHashFallback = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHashFallback))
{
fragmentHashes.Add(fragmentHashFallback);
}
fragmentIndex++;
continue;
var value = pathData.GetValue(i);
var arrayIndex = i - lowerBound;
pathInts[arrayIndex] = System.Convert.ToInt32(value);
}
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++)
catch (Exception ex) when (ex is InvalidCastException or OverflowException or FormatException)
{
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);
}
// Skip invalid array values
}
}
var fragmentHash = string.Join("_", pathInts);
var fragmentHash = string.Join("_", pathInts);
fragmentHashes.Add(fragmentHash);
}
catch (Exception ex) when (ex is InvalidCastException or IndexOutOfRangeException or RankException)
{
// Try simple enumeration as fallback
var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHash))
{
fragmentHashes.Add(fragmentHash);
}
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++;
continue;
}
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
fragmentIndex++;
}
}
string fragmentId;
if (fragmentHashes.Count > 0)
{
// Sort to ensure consistent ordering
fragmentHashes.Sort();
var rawData = string.Join("__", fragmentHashes);
var fragmentId = HashRawData(rawData);
return fragmentId;
fragmentId = HashRawData(rawData);
}
else
{
fragmentId = string.Empty;
}
return string.Empty;
}
catch (Exception ex) when (ex is COMException or InvalidCastException or IndexOutOfRangeException)
{
var errorType = ex switch
// Cache the result for future use
if (!string.IsNullOrEmpty(fragmentId))
{
COMException => "COM access failed",
InvalidCastException => "Type conversion failed",
IndexOutOfRangeException => "Array bounds exceeded",
_ => "Error"
};
_logger.LogWarning(ex, "{ErrorType} generating fragment ID", errorType);
_fragmentIdCache.TryAdd(pathsHashCode, fragmentId);
}
return fragmentId;
}
catch (Exception ex)
when (ex
is InvalidCastException
or IndexOutOfRangeException
or OverflowException
or ArgumentException
or COMException
)
{
_logger.LogWarning(ex, "Failed to generate fragment ID due to {ExceptionType}", ex.GetType().Name);
return string.Empty;
}
}
/// <summary>
/// Simple array enumeration fallback when bounds access fails.
/// Tries to enumerate array by simple sequential access.
/// </summary>
private string TrySimpleArrayEnumeration(Array pathData, int fragmentIndex)
{
try
{
var values = new List<string>();
var maxAttempts = Math.Min(pathData.Length, 20);
var maxAttempts = Math.Min(pathData.Length, 20); // Limit attempts to avoid infinite loops
for (int i = 0; i < maxAttempts; i++)
{
@@ -426,14 +501,18 @@ public class GeometryToSpeckleConverter(
var value = pathData.GetValue(i);
var convertedValue = System.Convert.ToInt32(value);
values.Add(convertedValue.ToString());
_logger.LogDebug("Fragment {FragmentIndex} simple enum[{Index}] = {Value}", fragmentIndex, i, convertedValue);
}
catch (IndexOutOfRangeException)
{
// Hit the end of valid indices
_logger.LogDebug("Fragment {FragmentIndex} reached end of array at index {Index}", fragmentIndex, i);
break;
}
catch (InvalidCastException ex)
catch (Exception ex)
when (ex is InvalidCastException or OverflowException or FormatException or ArgumentException)
{
_logger.LogDebug(ex, "Type conversion failed at index {Index}", i);
_logger.LogWarning(ex, "Fragment {FragmentIndex} failed to convert value at index {Index}", fragmentIndex, i);
}
}
@@ -442,44 +521,96 @@ public class GeometryToSpeckleConverter(
return string.Empty;
}
return string.Join("_", values);
var hash = string.Join("_", values);
return hash;
}
catch (COMException ex)
catch (Exception ex) when (ex is COMException or InvalidCastException or ArgumentException)
{
_logger.LogDebug(ex, "COM enumeration failed for fragment {FragmentIndex}", fragmentIndex);
_logger.LogWarning(ex, "Simple enumeration failed for fragment {FragmentIndex}", fragmentIndex);
return string.Empty;
}
}
private static string HashRawData(string rawData)
/// <summary>
/// Generates a fast hash code for paths collection for caching purposes
/// </summary>
private static int GenerateFastPathsHashCode(InwSelectionPathsColl paths)
{
using var sha256 = SHA256.Create();
var inputBytes = Encoding.UTF8.GetBytes(rawData);
var hashBytes = sha256.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
unchecked
{
int hash = 17;
hash = hash * 23 + paths.Count;
var processed = 0;
foreach (InwOaPath path in paths)
{
if (path.ArrayData is Array { Length: > 0 } pathData)
{
// Sample first few elements for performance
var sampleSize = Math.Min(pathData.Length, 4);
for (int i = 0; i < sampleSize; i++)
{
hash = hash * 23 + (pathData.GetValue(i)?.GetHashCode() ?? 0);
}
hash = hash * 23 + pathData.Length;
}
// Limit processing for performance
if (++processed >= 8)
{
break;
}
}
return hash;
}
}
private SSM.Base? ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
/// <summary>
/// Creates a fast hash of the raw fragment data using .NET's HashCode struct.
/// For performance, we use HashCode instead of SHA256 for fragment IDs.
/// </summary>
/// <returns>Hash as hex string</returns>
private static string HashRawData(string rawData)
{
var hashCode = rawData.GetHashCode();
return hashCode.ToString("X8");
}
/// <summary>
/// Extracts untransformed base geometry from fragments.
/// This geometry will be stored once and referenced by instances.
/// </summary>
private Base? ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
{
var processor = new PrimitiveProcessor(_isUpright);
// Process fragments without transforms to get base geometry
foreach (var fragment in fragmentStack)
{
processor.LocalToWorldTransformation = s_identityTransform;
// Use identity transform to get untransformed geometry
double[] identityTransform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
processor.LocalToWorldTransformation = identityTransform;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
// Create mesh from untransformed geometry
return processor.Triangles.Count > 0 ? CreateMesh(processor.Triangles) : null;
}
private List<SSM.Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
/// <summary>
/// Creates an instance reference to shared geometry stored in the InstanceStoreManager.
/// This is returned instead of full geometry for shared instances.
/// </summary>
private List<Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
{
var transform = ExtractInstanceTransform(paths);
var instanceReference = new InstanceProxy
{
definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}",
definitionId = $"def_{fragmentId}",
transform = transform,
units = _settings.Derived.SpeckleUnits,
maxDepth = 0,
@@ -489,100 +620,113 @@ public class GeometryToSpeckleConverter(
return [instanceReference];
}
/// <summary>
/// Extracts the transform matrix from the first path's fragments for instance placement.
/// </summary>
private Matrix4x4 ExtractInstanceTransform(InwSelectionPathsColl paths)
{
try
{
if (paths.Count == 0)
{
return s_identityMatrix;
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
var firstPath = paths.Cast<InwOaPath>().First();
var fragments = firstPath.Fragments();
try
using var fragments = new ComScope<InwNodeFragsColl>(firstPath.Fragments());
if (fragments.Value.Count == 0)
{
if (fragments.Count == 0)
{
return s_identityMatrix;
}
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
var fragmentStack = new Stack<InwOaFragment3>();
foreach (var frag in fragments.OfType<InwOaFragment3>())
var fragmentStack = new Stack<InwOaFragment3>();
// Get the first fragment's transform matrix
foreach (var frag in fragments.Value.OfType<InwOaFragment3>())
{
try
{
if (frag.path?.ArrayData is not Array pathData1 || firstPath.ArrayData is not Array pathData2)
{
continue;
}
var pathArray1 = pathData1.Cast<int>().ToArray();
var pathArray2 = pathData2.Cast<int>().ToArray();
if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2))
// Use IsSameFragmentPath for consistency and performance
if (IsSameFragmentPath(pathData1, pathData2))
{
fragmentStack.Push(frag);
}
}
var fragment = fragmentStack.First();
var matrix = fragment.GetLocalToWorldMatrix();
if (matrix is InwLTransform3f3 { Matrix: Array matrixArray })
catch (COMException ex)
{
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);
_logger.LogWarning(ex, "COM exception accessing fragment path data, skipping fragment");
}
}
finally
if (fragmentStack.Count == 0)
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
_logger.LogWarning("No valid fragments found for transform extraction");
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
var fragment = fragmentStack.First();
var matrix = fragment.GetLocalToWorldMatrix();
if (matrix is InwLTransform3f3 { Matrix: Array matrixArray })
{
var transformArray = ConvertArrayToDouble(matrixArray);
// Apply coordinate system transformation
var transformedMatrix = ApplyCoordinateTransform(transformArray);
var newMatrix = new Matrix4x4(
transformedMatrix[0],
transformedMatrix[1],
transformedMatrix[2],
transformedMatrix[3],
transformedMatrix[4],
transformedMatrix[5],
transformedMatrix[6],
transformedMatrix[7],
transformedMatrix[8],
transformedMatrix[9],
transformedMatrix[10],
transformedMatrix[11],
transformedMatrix[12],
transformedMatrix[13],
transformedMatrix[14],
transformedMatrix[15]
);
return Matrix4x4.Transpose(newMatrix);
}
}
catch (Exception ex) when (ex is COMException or InvalidCastException or NullReferenceException)
catch (Exception ex)
when (ex
is COMException
or InvalidCastException
or IndexOutOfRangeException
or ArgumentException
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);
_logger.LogWarning(
ex,
"Failed to extract instance transform ({ExceptionType}) - returning identity matrix",
ex.GetType().Name
);
}
return s_identityMatrix;
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
private double[] ApplyCoordinateTransform(double[] matrixArray)
{
// Apply scale and coordinate transformation
var result = new double[16];
Array.Copy(matrixArray, result, 16);
// Apply translation transformation
result[12] = (result[12] + _transformVector.X) * SCALE;
result[13] = (result[13] + _transformVector.Y) * SCALE;
result[14] = (result[14] + _transformVector.Z) * SCALE;
@@ -606,6 +750,11 @@ public class GeometryToSpeckleConverter(
return doubleArray;
}
private static bool AreFragmentPathsEqual(Array a1, Array a2) =>
a1.Length == a2.Length && a1.Cast<int>().SequenceEqual(a2.Cast<int>());
private static bool IsSameFragmentPath(Array a1, Array a2) =>
a1.Length == a2.Length
&& (
a1.Length > 4
? a1.Cast<object>().SequenceEqual(a2.Cast<object>())
: !a1.Cast<object>().Where((_, i) => !Equals(a1.GetValue(i), a2.GetValue(i))).Any()
);
}
@@ -32,64 +32,4 @@ public static class ElementExtensions
return ids;
}
public static IEnumerable<ElementId> GetKnownChildrenElements(this Element element) =>
element switch
{
Wall wall => GetWallChildren(wall),
FootPrintRoof roof => GetFootPrintRoofChildren(roof),
DBA.Railing railing => GetRailingChildren(railing),
_ => []
};
private static IEnumerable<ElementId> GetWallChildren(Wall wall)
{
if (wall.CurtainGrid is CurtainGrid grid)
{
foreach (var id in grid.GetMullionIds())
{
yield return id;
}
foreach (var id in grid.GetPanelIds())
{
yield return id;
}
}
else if (wall.IsStackedWall)
{
foreach (var id in wall.GetStackedWallMemberIds())
{
yield return id;
}
}
}
private static IEnumerable<ElementId> GetFootPrintRoofChildren(FootPrintRoof footPrintRoof)
{
if (footPrintRoof.CurtainGrids is { } grids)
{
foreach (CurtainGrid grid in grids)
{
foreach (var id in grid.GetMullionIds())
{
yield return id;
}
foreach (var id in grid.GetPanelIds())
{
yield return id;
}
}
}
}
private static IEnumerable<ElementId> GetRailingChildren(DBA.Railing railing)
{
// TODO: Consider adding HandRail support (railing.GetHandRails())
if (railing.TopRail != ElementId.InvalidElementId)
{
yield return railing.TopRail;
}
}
}
@@ -74,22 +74,28 @@ public sealed class DisplayValueExtractor
}
return areaDisplay;
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView + apply reference point transform
// NOTE: this is only for Rebar and not AreaReinforcement, RebarInSystem
// AreaReinforcement and RebarInSystem pass through GetGeometryDisplayValue which get DisplayValues as per hostApp
// Rebar elements need special handling as get_Geometry() doesn't work properly
// We either represent them as centerlines or as solids based on settings
case DB.Structure.Rebar rebar:
return _converterSettings.Current.SendRebarsAsVolumetric
? GetRebarVolumetricDisplayValue(rebar)
: GetRebarCenterlineDisplayValue(rebar);
// AreaReinforcement/PathReinforcement get_Geometry() returns curves in document coordinates
// unlike Rebar which needs reference point transform applied, these are already correct
case DB.Structure.AreaReinforcement:
case DB.Structure.PathReinforcement:
return GetAreaReinforcementDisplayValue(element);
// handle specific types of objects with multiple parts or children
// curtain and stacked walls should have their display values in their children
case DB.Wall wall:
return wall.CurtainGrid is not null || wall.IsStackedWall ? new() : GetGeometryDisplayValue(element);
// railings should also include toprail which need to be retrieved separately
case DBA.Railing railing:
List<DisplayValueResult> railingDisplay = GetGeometryDisplayValue(railing);
if (railing.TopRail != DB.ElementId.InvalidElementId)
{
var topRail = _converterSettings.Current.Document.GetElement(railing.TopRail);
railingDisplay.AddRange(GetGeometryDisplayValue(topRail));
}
return railingDisplay;
// POC: footprint roofs can have curtain walls in them. Need to check if they can also have non-curtain wall parts, bc currently not skipping anything.
// case DB.FootPrintRoof footPrintRoof:
@@ -110,7 +116,7 @@ public sealed class DisplayValueExtractor
using DB.Transform? compoundTransform =
localToDocument is not null && documentToWorld is not null
? documentToWorld.Multiply(localToDocument)
: localToDocument;
: localToDocument; // don't want to accidentally dispose of the ReferencePointTransform
DB.Transform? localToWorld = compoundTransform ?? documentToWorld;
@@ -194,51 +200,20 @@ public sealed class DisplayValueExtractor
);
}
// 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.
// add rest of geometry (always without transform)
foreach (var curve in collections.Curves)
{
if (localToWorld is not null)
{
using var transformedCurve = curve.CreateTransformed(localToWorld);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
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)
{
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)));
}
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(polyline)));
}
foreach (var point in collections.Points)
{
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)));
}
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(point)));
}
return displayValue;
@@ -380,8 +355,7 @@ public sealed class DisplayValueExtractor
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.
//Note, we're not applying transforms to curves/polylines/points because ProcessGeometryCollections expects them in world coordinates
case DB.Curve curve:
collections.Curves.Add(curve);
break;
@@ -426,7 +400,7 @@ public sealed class DisplayValueExtractor
return false; // exit fast on a potential hot path
}
DB.GraphicsStyle? bjk; // ask ogu why this variable is named like this
DB.GraphicsStyle? bjk = null; // ask ogu why this variable is named like this
if (!_graphicStyleCache.ContainsKey(geomObj.GraphicsStyleId.ToString().NotNull()))
{
@@ -550,9 +524,8 @@ public sealed class DisplayValueExtractor
if (geometryElements != null)
{
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
SortGeometry(rebar, collections, geometryElements, null);
return ProcessGeometryCollections(rebar, collections, documentToWorld);
return ProcessGeometryCollections(rebar, collections, null);
}
// Return empty list if no geometry is found - imo not critical
@@ -593,49 +566,24 @@ 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;
}
/// <summary>
/// Gets display value for AreaReinforcement and PathReinforcement.
/// </summary>
/// <remarks>
/// These elements' get_Geometry() returns curves already in document coordinates.
/// Unlike Rebar.GetTransformedCenterlineCurves() which requires reference point transform,
/// these curves should not be transformed - they're already in the correct space.
/// </remarks>
private List<DisplayValueResult> GetAreaReinforcementDisplayValue(DB.Element element)
{
var collections = GetSortedGeometryFromElement(element, null, null);
// pass null for transform - curves are already in correct document coordinates
return ProcessGeometryCollections(element, collections, null);
}
/// <summary>
/// Represents sorted collections of different geometry types extracted from an element.
/// Used to pass multiple geometry collections as a single parameter to improve code readability
/// 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="Curves"/>, <see cref="Polylines"/>, and <see cref="Points"/> remain in their original coordinate space
/// and are transformed to world space during processing in ProcessGeometryCollections.
/// <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
/// </remarks>
private sealed record GeometryCollections
{
@@ -81,9 +81,8 @@
<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,17 +88,6 @@ 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())
{
@@ -49,20 +49,11 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter<DB.Element, Dicti
switch (target)
{
case DBA.Railing railing:
// railings can have sub-elements including top rails, handrails, and balusters.
// railings can have subelements including top rails, hand rails, and balusters.
// they also do *not* have any materials associated with their category.
// TopRail is now a separate child element with its own material quantities (see CNX-2806)
List<DB.ElementId> railingElementIds = [railing.GetTypeId(), .. railing.GetHandRails()];
List<DB.ElementId> railingElementIds = [railing.GetTypeId(), railing.TopRail, .. railing.GetHandRails()];
ProcessMaterialsByElementTypes(railingElementIds, quantities);
break;
case DBA.TopRail topRail:
// TopRail/HandRail doesn't expose materials via HasMaterialQuantities
// must extract materials from the type parameters instead
List<DB.ElementId> railElementIds = [topRail.GetTypeId()];
ProcessMaterialsByElementTypes(railElementIds, quantities);
break;
default:
ProcessMaterialsByCategory(target, quantities);
break;
@@ -137,11 +137,61 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
private IEnumerable<RevitObject> GetElementChildren(DB.Element element)
{
var childrenIds = element.GetKnownChildrenElements();
foreach (var childrenId in childrenIds)
switch (element)
{
var childElement = _converterSettings.Current.Document.GetElement(childrenId);
yield return Convert(childElement);
case DB.Wall wall:
var wallChildren = GetWallChildren(wall);
foreach (var child in wallChildren)
{
yield return child;
}
break;
case DB.FootPrintRoof footPrintRoof:
var footPrintRoofChildren = GetFootPrintRoofChildren(footPrintRoof);
foreach (var child in footPrintRoofChildren)
{
yield return child;
}
break;
}
}
private IEnumerable<RevitObject> GetWallChildren(DB.Wall wall)
{
List<DB.ElementId> wallChildrenIds = new();
if (wall.CurtainGrid is DB.CurtainGrid grid)
{
wallChildrenIds.AddRange(grid.GetMullionIds());
wallChildrenIds.AddRange(grid.GetPanelIds());
}
else if (wall.IsStackedWall)
{
wallChildrenIds.AddRange(wall.GetStackedWallMemberIds());
}
foreach (var childId in wallChildrenIds)
{
yield return Convert(_converterSettings.Current.Document.GetElement(childId));
}
}
// Shockingly, roofs can have curtain grids on them. I guess it makes sense: https://en.wikipedia.org/wiki/Louvre_Pyramid
private IEnumerable<RevitObject> GetFootPrintRoofChildren(DB.FootPrintRoof footPrintRoof)
{
List<DB.ElementId> footPrintRoofChildrenIds = new();
if (footPrintRoof.CurtainGrids is { } gs)
{
foreach (DB.CurtainGrid grid in gs)
{
footPrintRoofChildrenIds.AddRange(grid.GetMullionIds());
footPrintRoofChildrenIds.AddRange(grid.GetPanelIds());
}
}
foreach (var childId in footPrintRoofChildrenIds)
{
yield return Convert(_converterSettings.Current.Document.GetElement(childId));
}
}
@@ -155,7 +205,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
/// </returns>
/// <remarks>
/// <para>
/// This is a bit of a code smell. This method is doing too much, "this ... AND this...".
/// This is a bit of a code smell. This method is doing to much, "this ... AND this...".
/// </para>
/// <para>
/// But, given a mesh:
@@ -1,49 +0,0 @@
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)
};
}
}

Some files were not shown because too many files have changed in this diff Show More