Compare commits

..

15 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 604dc00ef0 Add COM error handling to fragment ID generation
Co-authored-by: jsdbroughton <760691+jsdbroughton@users.noreply.github.com>
2025-11-17 14:02:43 +00:00
copilot-swe-agent[bot] 1c24a81d58 Initial plan 2025-11-17 13:58:25 +00:00
Jonathon Broughton b751dd0756 Merge branch 'dev' into jonathon/cnx-2817-adopt-displayvalue-proxification-pattern-in-navisworks 2025-11-17 13:50:00 +00:00
Jonathon Broughton 39ba1f8066 Refactors method signature for clarity 2025-11-17 13:49:16 +00:00
Jonathon Broughton 31a40931c7 Refactors property name sanitisation logic
Consolidates the logic for sanitising property names into a more concise format.
2025-11-17 13:49:02 +00:00
Björn Steinhagen 60c1811fa6 fix(revit): railing TopRail material proxy cache errors and duplication (#1186)
* fix: first pass

* refactor: cleanup
2025-11-17 15:47:14 +02:00
Jonathon Broughton c68ac2a63d Improves geometry retrieval and checks
Refactors geometry definition retrieval for better readability.
2025-11-17 13:42:51 +00:00
Jonathon Broughton dbd35ac31a Refactors Navisworks build process for resilience
Adds error checking to ensure the Navisworks version is set before build occurs, and improves error handling to avoid empty output directories.

Updates file copy logic to handle resource and ribbon files correctly.
Ensures that the Navisworks plugin is correctly packaged and deployed.

Addresses CNX-2788
2025-11-17 12:39:47 +00:00
Jonathon Broughton 0deea75204 Refactors constants usage for geometry identification
Introduces new constant definitions to standardise fragment ID prefixing.
2025-11-17 12:38:37 +00:00
Jonathon Broughton 44bec42198 Refines COM object management and method naming
Improves memory safety by ensuring all COM objects are explicitly released in try-finally blocks.
2025-11-17 12:21:15 +00:00
Jonathon Broughton 6fd1f05680 Renames methods for clarity and updates logic
Refactors method names to improve readability and understanding of functionality.

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

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

Relates to the adoption of the display-value proxification pattern.
2025-11-17 12:13:59 +00:00
Jonathon Broughton 3861da4347 Reduces comment clutter in Navisworks converters
Removes unnecessary comments to enhance code readability
and maintainability.

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

Relates to issue jonathon/cnx-2817-adopt-displayvalue-proxification-pattern-in-navisworks.
2025-11-17 12:10:23 +00:00
Jonathon Broughton 90c38a28a5 Refactors dependency injection for settings 2025-11-17 11:57:19 +00:00
Jonathon Broughton e4de8c47b5 Merge branch 'dev' into jonathon/cnx-2817-adopt-displayvalue-proxification-pattern-in-navisworks 2025-11-17 11:52:01 +00:00
Björn Steinhagen 5365809172 fix(rhino): dispayValue proxies broke rhino load (#1182)
* refactor: root object unpacker doesn't unpack proxified display values

* fix: removes unnecessary using statement

* refactor: sets appropriate methods as private

* feat: adds ProxifiedDisplayValueManager class

* refactor: manager class to do more

* chore: di

* feat: converter uses manager class

* chore: adds sdk.connectors to converter project (NOT HAPPY)

* chore: init manager

* refactor: manager in Speckle.Converters.Common

* fix: di

* fix: don't need clear

* chore: i don't even know what i did

* fix: rhino materials

* fix: autocad

* fix: revit di

* chore: format

* refactor: meshes to instances pt 1

* refactor: new approach final v2.1

* fix: can't even remember anymore

* fix: autocad

* chore: pr comments from Oguzhan Bey
2025-11-14 07:40:40 +00:00
84 changed files with 907 additions and 749 deletions
@@ -259,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +210,7 @@
"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,6 +210,7 @@
"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,6 +268,7 @@
"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,6 +268,7 @@
"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,6 +268,7 @@
"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,6 +219,7 @@
"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,6 +219,7 @@
"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,6 +259,7 @@
"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,6 +210,7 @@
"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, )"
+47 -17
View File
@@ -2,33 +2,63 @@
<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>$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Navisworks.bundle\Contents\$(NavisworksVersion)</PluginVersionContentTarget>
<PluginVersionContentTarget>$(PluginBundleTarget)\Contents\$(NavisworksVersion)</PluginVersionContentTarget>
<RootNamespace>Speckle.Connector.Navisworks</RootNamespace>
</PropertyGroup>
<!-- 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="'$(OS)' == 'Windows_NT' and '$(NavisworksVersion)' != ''">
<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)')"/>
<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)\" />
<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."/>
</Target>
</Project>
@@ -259,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +265,7 @@
"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,6 +266,7 @@
"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.Settings;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using Speckle.Sdk.Models.GraphTraversal;
@@ -16,7 +16,8 @@ public class NavisworksColorUnpacker(
IElementSelectionService selectionService
)
{
private static T Select<T>(RepresentationMode mode, T active, T permanent, T original, T defaultValue) =>
private static T SelectByRepresentationMode<T>(
RepresentationMode mode, T active, T permanent, T original, T defaultValue) =>
mode switch
{
RepresentationMode.Active => active,
@@ -71,14 +72,14 @@ public class NavisworksColorUnpacker(
using var defaultColor = new NAV.Color(1.0, 1.0, 1.0);
var representationColor = Select(
var representationColor = SelectByRepresentationMode(
mode,
geometry.ActiveColor,
geometry.PermanentColor,
geometry.OriginalColor,
defaultColor
);
var colorId = Select(
var colorId = SelectByRepresentationMode(
mode,
$"{geometry.ActiveColor.GetHashCode()}_{geometry.ActiveTransparency}".GetHashCode(),
$"{geometry.PermanentColor.GetHashCode()}_{geometry.PermanentTransparency}".GetHashCode(),
@@ -124,30 +125,49 @@ public class NavisworksColorUnpacker(
var comSelection = ComBridge.ToInwOpSelection([modelItem]);
try
{
foreach (ComApi.InwOaPath path in comSelection.Paths())
var pathsCollection = comSelection.Paths();
try
{
GC.KeepAlive(path);
foreach (ComApi.InwOaFragment3 fragment in path.Fragments())
foreach (ComApi.InwOaPath path in pathsCollection)
{
GC.KeepAlive(fragment);
fragment.GenerateSimplePrimitives(ComApi.nwEVertexProperty.eNORMAL, primitiveChecker);
// Exit early if triangles are found
if (primitiveChecker.HasTriangles)
var fragmentsCollection = path.Fragments();
try
{
return false;
foreach (ComApi.InwOaFragment3 fragment in fragmentsCollection.OfType<ComApi.InwOaFragment3>())
{
fragment.GenerateSimplePrimitives(ComApi.nwEVertexProperty.eNORMAL, primitiveChecker);
if (primitiveChecker.HasTriangles)
{
return false;
}
}
}
finally
{
if (fragmentsCollection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragmentsCollection);
}
}
}
}
// Return true if any 2D primitives are found
return primitiveChecker.HasLines || primitiveChecker.HasPoints || primitiveChecker.HasSnapPoints;
return primitiveChecker.HasLines || primitiveChecker.HasPoints || primitiveChecker.HasSnapPoints;
}
finally
{
if (pathsCollection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(pathsCollection);
}
}
}
finally
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection);
if (comSelection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection);
}
}
}
}
@@ -2,6 +2,7 @@ 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;
@@ -18,9 +19,8 @@ public class NavisworksMaterialUnpacker(
GeometryToSpeckleConverter converter
)
{
// 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) =>
private static T SelectByRepresentationMode<T>(
RepresentationMode mode, T active, T permanent, T original, T defaultValue) =>
mode switch
{
RepresentationMode.Active => active,
@@ -69,16 +69,61 @@ 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 = "";
var comSelection = ComApiBridge.ToInwOpSelection([item]);
var paths = comSelection.Paths();
var path = paths.OfType<InwOaPath>().First();
var fragments = path.Fragments();
if (fragments.Count > 1)
try
{
var fragmentId = converter.GenerateFragmentId(paths);
hashId = $"geom_{fragmentId}";
var item = selectionService.GetModelItemFromPath(finalId);
var comSelection = ComApiBridge.ToInwOpSelection([item]);
try
{
var paths = comSelection.Paths();
try
{
if (paths.Count > 0)
{
var firstPath = paths.OfType<InwOaPath>().FirstOrDefault();
if (firstPath != null)
{
var fragments = firstPath.Fragments();
try
{
if (fragments.Count > 1)
{
var fragmentId = converter.GenerateFragmentId(paths);
hashId = $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}";
}
}
finally
{
if (fragments != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragments);
}
}
}
}
}
finally
{
if (paths != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(paths);
}
}
}
finally
{
if (comSelection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection);
}
}
}
catch (Exception ex) when (!ex.IsFatal())
{
// If COM interop fails during hash generation, fall back to using finalId
logger.LogWarning(ex, "Failed to generate fragment hash ID for item {ItemId}, using finalId as fallback", finalId);
hashId = "";
}
var geometry = navisworksObject.Geometry;
@@ -86,21 +131,21 @@ public class NavisworksMaterialUnpacker(
using var defaultColor = new NAV.Color(1.0, 1.0, 1.0);
var renderColor = Select(
var renderColor = SelectByRepresentationMode(
mode,
geometry.ActiveColor,
geometry.PermanentColor,
geometry.OriginalColor,
defaultColor
);
var renderTransparency = Select(
var renderTransparency = SelectByRepresentationMode(
mode,
geometry.ActiveTransparency,
geometry.PermanentTransparency,
geometry.OriginalTransparency,
0.0
);
var renderMaterialId = Select(
var renderMaterialId = SelectByRepresentationMode(
mode,
$"{geometry.ActiveColor.GetHashCode()}_{geometry.ActiveTransparency}".GetHashCode(),
$"{geometry.PermanentColor.GetHashCode()}_{geometry.PermanentTransparency}".GetHashCode(),
@@ -109,9 +154,8 @@ public class NavisworksMaterialUnpacker(
);
var materialName =
$"NavisworksMaterial_{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
$"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
// Check Item category for material name
var itemCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Item");
if (itemCategory != null)
{
@@ -123,7 +167,6 @@ public class NavisworksMaterialUnpacker(
}
}
// Check Material category for material name
var materialPropertyCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Material");
if (materialPropertyCategory != null)
{
@@ -143,7 +186,7 @@ public class NavisworksMaterialUnpacker(
{
renderMaterialProxies[renderMaterialId.ToString()] = new RenderMaterialProxy()
{
value = ConvertRenderColorAndTransparencyToSpeckle(
value = CreateRenderMaterial(
materialName,
renderTransparency,
renderColor,
@@ -162,7 +205,7 @@ public class NavisworksMaterialUnpacker(
return renderMaterialProxies.Values.ToList();
}
private static RenderMaterial ConvertRenderColorAndTransparencyToSpeckle(
private static RenderMaterial CreateRenderMaterial(
string name,
double transparency,
NAV.Color navisworksColor,
@@ -173,7 +216,7 @@ public class NavisworksMaterialUnpacker(
var speckleRenderMaterial = new RenderMaterial()
{
name = !string.IsNullOrEmpty(name) ? name : $"NavisworksMaterial_{Math.Abs(color.ToArgb())}",
name = !string.IsNullOrEmpty(name) ? name : $"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}",
opacity = 1 - transparency,
metalness = 0,
roughness = 1,
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Speckle.Connector.Navisworks.HostApp;
using Speckle.Connector.Navisworks.Services;
using Speckle.Connectors.Common.Builders;
@@ -42,19 +42,14 @@ 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);
@@ -66,8 +61,6 @@ 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",
@@ -159,12 +152,10 @@ 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(
@@ -173,12 +164,9 @@ public class NavisworksRootObjectBuilder(
elementSelectionService
);
var hierarchy = hierarchyBuilder.BuildHierarchy();
return hierarchy;
return hierarchyBuilder.BuildHierarchy();
}
// Otherwise continue with flat mode
AddRemainingElements(finalElements, convertedBases, processedPaths);
return finalElements;
}
@@ -235,24 +223,17 @@ public class NavisworksRootObjectBuilder(
}
}
private (string name, string path) GetContext(string applicationId)
private (string name, string path) GetElementNameAndPath(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) = GetContext(cleanParentPath);
(string name, string path) = GetElementNameAndPath(cleanParentPath);
return new NavisworksObject
{
@@ -260,16 +241,11 @@ 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, // Use the full composite key as applicationId to preserve uniqueness
applicationId = groupKey,
["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)
@@ -277,7 +253,7 @@ public class NavisworksRootObjectBuilder(
return null;
}
(string name, string path) = GetContext(convertedBase.applicationId);
(string name, string path) = GetElementNameAndPath(convertedBase.applicationId);
return new NavisworksObject
{
@@ -310,36 +286,16 @@ 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,6 +281,7 @@
"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,6 +281,7 @@
"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,6 +281,7 @@
"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,6 +226,7 @@
"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,6 +219,7 @@
"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)
];
@@ -74,7 +74,6 @@ public static class ServiceRegistration
serviceCollection.AddSingleton<RevitUtils>();
serviceCollection.AddSingleton<IFailuresPreprocessor, HideWarningsFailuresPreprocessor>();
serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc());
serviceCollection.AddScoped<LocalToGlobalConverterUtils>();
// operation progress manager
@@ -1,5 +1,6 @@
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;
using Speckle.Converters.RevitShared.Extensions;
namespace Speckle.Connectors.Revit.HostApp;
@@ -23,11 +24,11 @@ public class ElementUnpacker
// Step 1: unpack groups
var atomicObjects = UnpackElements(selectionElements, 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);
// 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);
}
/// <summary>
@@ -108,7 +109,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> PackCurtainWallElementsAndStackedWalls(List<Element> elements, Document doc)
private List<Element> RemoveKnownChildElementsWhenParentPresent(List<Element> elements, Document doc)
{
//just used for contains so use ToHashSet
var ids = elements.Select(el => el.Id).ToHashSet();
@@ -131,64 +132,37 @@ 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>
/// 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.
/// Returns element IDs and their known child element IDs for cache tracking.
/// Uses <see cref="ElementExtensions.GetKnownChildrenElements"/> to determine which children to include.
/// </summary>
/// <param name="elements"></param>
/// <returns></returns>
/// <param name="elements">Elements to process</param>
/// <returns>Flattened list of parent and child element IDs</returns>
public List<string> GetElementsAndSubelementIdsFromAtomicObjects(List<Element> elements)
{
var ids = new HashSet<string>();
foreach (var element in elements)
{
switch (element)
{
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;
}
// 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())
{
ids.Add(childId.ToString());
}
}
return ids.ToList();
@@ -325,6 +325,7 @@
"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, )"
@@ -325,6 +325,7 @@
"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, )"
@@ -306,6 +306,7 @@
"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, )"
@@ -306,6 +306,7 @@
"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, )"
@@ -235,6 +235,7 @@
"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, )"
@@ -0,0 +1,87 @@
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,6 +6,7 @@ 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;
@@ -22,18 +23,21 @@ 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
ILogger<RhinoInstanceBaker> logger,
IDataObjectInstanceRegistry dataObjectInstanceRegistry
)
{
_layerBaker = layerBaker;
_materialBaker = rhinoMaterialBaker;
_colorBaker = colorBaker;
_logger = logger;
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
}
/// <summary>
@@ -155,6 +159,9 @@ 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())
@@ -10,6 +10,7 @@ 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;
@@ -36,6 +37,8 @@ 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,
@@ -48,7 +51,9 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
RhinoGroupBaker groupBaker,
ISdkActivityFactory activityFactory,
IThreadContext threadContext,
IReceiveConversionHandler conversionHandler
IReceiveConversionHandler conversionHandler,
IDataObjectInstanceRegistry dataObjectInstanceRegistry,
DataObjectInstanceGrouper dataObjectInstanceGrouper
)
{
_converter = converter;
@@ -62,6 +67,8 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
_activityFactory = activityFactory;
_threadContext = threadContext;
_conversionHandler = conversionHandler;
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
_dataObjectInstanceGrouper = dataObjectInstanceGrouper;
}
#pragma warning disable CA1506
@@ -188,8 +195,14 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
if (conversionIds.Count == 0)
{
// TODO: add this condition to report object - same as in autocad
throw new ConversionException("Object did not convert to any native geometry");
// 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;
}
// 4: log
@@ -232,7 +245,10 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
conversionResults.UnionWith(instanceConversionResults); // add instance conversion results to our list
}
// 7 - Create groups
// 7.1 Group DataObject instances and apply metadata
_dataObjectInstanceGrouper.GroupAndApplyProperties();
// 7.2 Normal group creation
if (unpackedRoot.GroupProxies is not null)
{
_groupBaker.BakeGroups(unpackedRoot.GroupProxies, applicationIdMap, baseLayerName);
@@ -244,6 +260,9 @@ 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,
@@ -354,7 +373,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
);
@@ -22,6 +22,7 @@ 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;
@@ -75,7 +76,7 @@ public static class ServiceRegistration
InstanceObjectsManager<RhinoObject, List<string>>
>();
// Register unpackers and bakers
// register unpackers and bakers
serviceCollection.AddScoped<RhinoLayerUnpacker>();
serviceCollection.AddScoped<RhinoLayerBaker>();
@@ -94,6 +95,10 @@ public static class ServiceRegistration
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,6 +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\RhinoIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoLayerHelper.cs" />
@@ -325,6 +325,7 @@
"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,6 +406,7 @@
"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,6 +406,7 @@
"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,6 +259,7 @@
"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,6 +210,7 @@
"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,6 +210,7 @@
"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,6 +4,7 @@ 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;
@@ -42,14 +43,20 @@ 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;
}
public IEnumerable<(ADB.Entity a, Base b)> ConvertDisplayObject(Base displayObject)
private IEnumerable<(ADB.Entity a, Base b)> ConvertDisplayObject(Base displayObject)
{
switch (displayObject)
{
@@ -219,6 +219,7 @@
"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,6 +219,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +259,7 @@
"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,6 +260,7 @@
"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, )"
@@ -18,20 +18,13 @@ public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionS
return propertyDictionary;
}
/// <summary>
/// Extracts property sets from a NAV.ModelItem and adds them to a dictionary,
/// PropertySets are specific to the host application source appended to Navisworks and therefore
/// arbitrary in nature.
/// </summary>
/// <param name="modelItem">The NAV.ModelItem from which property sets are extracted.</param>
/// <returns>A dictionary containing property sets of the modelItem.</returns>
private Dictionary<string, object?> ExtractPropertySets(NAV.ModelItem modelItem)
{
var propertySetDictionary = new Dictionary<string, object?>();
foreach (var propertyCategory in modelItem.PropertyCategories)
{
if (IsCategoryToBeSkipped(propertyCategory))
if (ShouldSkipCategory(propertyCategory))
{
continue;
}
@@ -52,7 +52,8 @@ 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 =>
@@ -1,60 +0,0 @@
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
}
}
}
}
@@ -47,22 +47,15 @@ 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; // Do not add null values
break;
case string stringValue:
{
if (!string.IsNullOrEmpty(stringValue))
@@ -78,9 +71,6 @@ 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)
@@ -96,16 +86,9 @@ public static class PropertyHelpers
}
}
/// <summary>
/// Sanitizes property names by replacing invalid characters with underscores.
/// </summary>
internal static string SanitizePropertyName(string name) =>
// 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]", "_");
name == "Item" ? "Item" : Regex.Replace(name, @"[\.\/\s]", "_");
internal static bool IsCategoryToBeSkipped(NAV.PropertyCategory propertyCategory) =>
internal static bool ShouldSkipCategory(NAV.PropertyCategory propertyCategory) =>
s_excludedCategories.Contains(propertyCategory.DisplayName);
}
@@ -1,4 +1,4 @@
namespace Speckle.Converter.Navisworks.Constants;
namespace Speckle.Converter.Navisworks.Constants;
public static class PathConstants
{
@@ -6,3 +6,14 @@ 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,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Constants;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
@@ -54,7 +55,9 @@ 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 == $"geom_{fragmentId}");
GeometryDefinitionsStore.Geometries.FirstOrDefault(g =>
g.applicationId == $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}"
);
/// <summary>
/// Gets an instance definition proxy by its application ID.
@@ -63,7 +66,7 @@ public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
public InstanceDefinitionProxy? GetInstanceDefinitionProxy(string fragmentId) =>
InstanceDefinitionProxiesStore
.Geometries.OfType<InstanceDefinitionProxy>()
.FirstOrDefault(p => p.applicationId == $"def_{fragmentId}");
.FirstOrDefault(p => p.applicationId == $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}");
/// <summary>
/// Adds a geometry definition and corresponding instance definition proxy for shared geometry.
@@ -80,8 +83,8 @@ public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
bool proxyAdded = false;
// Create prefixed IDs for 1:1:1 relationship using base fragment hash
var geometryId = $"geom_{fragmentId}";
var definitionId = $"def_{fragmentId}";
var geometryId = $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}";
var definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}";
_logger.LogDebug("Using GeometryId={GeometryId}, DefinitionId={DefinitionId}", geometryId, definitionId);
@@ -148,7 +151,8 @@ 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($"geom_{fragmentId}")
// && InstanceDefinitionProxiesStore.Contains($"def_{fragmentId}")
public bool ContainsSharedGeometry(string fragmentId) =>
GeometryDefinitionsStore.Contains($"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}")
// && InstanceDefinitionProxiesStore.Contains($"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}")
;
}
@@ -23,7 +23,6 @@
<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"/>
@@ -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,9 +8,6 @@ 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;
@@ -41,11 +38,8 @@ 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,7 +1,9 @@
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;
@@ -14,17 +16,11 @@ using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
namespace Speckle.Converter.Navisworks.ToSpeckle;
/// <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>
/// <remarks>
/// Memory Safety: All COM objects (InwSelectionPathsColl, InwOaPath, InwOaFragmentList) are explicitly
/// released using Marshal.ReleaseComObject in try-finally blocks to prevent memory leaks.
/// NAV.Color objects are disposed using 'using' statements as they implement IDisposable.
/// </remarks>
public class GeometryToSpeckleConverter(
NavisworksConversionSettings settings,
InstanceStoreManager instanceStoreManager,
@@ -36,7 +32,9 @@ public class GeometryToSpeckleConverter(
private readonly bool _isUpright = settings.Derived.IsUpright;
private readonly SafeVector _transformVector = settings.Derived.TransformVector;
private const double SCALE = 1.0; // Default scale factor
private const double SCALE = 1.0;
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 readonly InstanceStoreManager _instanceStoreManager =
instanceStoreManager ?? throw new ArgumentNullException(nameof(instanceStoreManager));
@@ -44,41 +42,6 @@ public class GeometryToSpeckleConverter(
private readonly ILogger<GeometryToSpeckleConverter> _logger =
logger ?? throw new ArgumentNullException(nameof(logger));
// Fragment ID cache for performance optimization
private readonly ConcurrentDictionary<int, string> _fragmentIdCache = new();
// Geometry cache for repeated items
private readonly ConcurrentDictionary<string, List<Base>> _geometryCache = new();
/// <summary>
/// Clears all internal caches. Should be called when starting a new conversion session.
/// </summary>
public void ClearCaches()
{
_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)
@@ -91,89 +54,85 @@ public class GeometryToSpeckleConverter(
return [];
}
// 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());
var comSelection = ComApiBridge.ToInwOpSelection([modelItem]);
try
{
// Check if this geometry is shared across multiple instances
List<Base> result;
if (paths.Value.Count > 0)
var fragmentStack = new Stack<InwOaFragment3>();
var paths = comSelection.Paths();
try
{
var firstPath = paths.Value.Cast<InwOaPath>().First();
var fragmentsCollection = firstPath.Fragments();
if (fragmentsCollection.Count > 1)
if (paths.Count > 0)
{
// 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)
var firstPath = paths.Cast<InwOaPath>().First();
var fragmentsCollection = firstPath.Fragments();
try
{
CollectFragments(path, fragmentStack);
if (fragmentsCollection.Count > 1)
{
return ProcessSharedGeometry(paths, fragmentStack);
}
}
finally
{
if (fragmentsCollection != null)
{
Marshal.ReleaseComObject(fragmentsCollection);
}
}
}
result = ProcessFragments(fragmentStack, paths.Value, true);
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths, true);
}
finally
{
if (paths != null)
{
Marshal.ReleaseComObject(paths);
}
}
else
{
result = [];
}
// Cache the result for future use
if (result.Count > 0)
{
_geometryCache.TryAdd(itemId, result);
}
return result;
}
catch (COMException ex)
finally
{
_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 [];
if (comSelection != null)
{
Marshal.ReleaseComObject(comSelection);
}
}
}
private static void CollectFragments(InwOaPath path, Stack<InwOaFragment3> fragmentStack)
{
using var fragments = new ComScope<InwNodeFragsColl>(path.Fragments());
foreach (var fragment in fragments.Value.OfType<InwOaFragment3>())
var fragments = path.Fragments();
try
{
if (ValidateFragmentPath(fragment, path))
foreach (var fragment in fragments.OfType<InwOaFragment3>())
{
fragmentStack.Push(fragment);
if (AreFragmentPathsEqual(fragment, path))
{
fragmentStack.Push(fragment);
}
}
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
}
}
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);
@@ -182,14 +141,11 @@ 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);
@@ -202,16 +158,12 @@ public class GeometryToSpeckleConverter(
return ProcessFragments(fragmentStack, paths);
}
// Store both the geometry definition and create the instance definition proxy
if (!_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometry))
{
return ProcessFragments(fragmentStack, paths);
}
// Return instance reference to the newly stored geometry
return CreateInstanceReference(fragmentId, paths);
// Fallback to normal processing if store failed
}
private List<Base> ProcessFragments(
@@ -226,40 +178,42 @@ 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
{
var matrix = fragment.GetLocalToWorldMatrix();
var transform = matrix as InwLTransform3f3;
if (transform?.Matrix is not Array matrixArray)
{
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);
fragmentCount = fragmentsForCount.Count;
}
catch (COMException ex)
finally
{
_logger.LogWarning(ex, "COM exception processing fragment, skipping");
if (fragmentsForCount != null)
{
Marshal.ReleaseComObject(fragmentsForCount);
}
}
double[] makeNoChange = s_identityTransform;
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
if (isSingleObject || fragmentCount == 1)
{
processor.LocalToWorldTransformation = transformMatrix;
}
else
{
processor.LocalToWorldTransformation = makeNoChange;
}
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
callbackListeners.Add(processor);
@@ -268,10 +222,10 @@ public class GeometryToSpeckleConverter(
return ProcessGeometries(callbackListeners);
}
private static bool ValidateFragmentPath(InwOaFragment3 fragment, InwOaPath path) =>
private static bool AreFragmentPathsEqual(InwOaFragment3 fragment, InwOaPath path) =>
fragment.path?.ArrayData is Array fragmentPathData
&& path.ArrayData is Array pathData
&& IsSameFragmentPath(fragmentPathData, pathData);
&& AreFragmentPathsEqual(fragmentPathData, pathData);
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
{
@@ -304,7 +258,6 @@ 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,
@@ -349,10 +302,6 @@ 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
@@ -362,137 +311,124 @@ 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 (InwOaPath path in paths)
foreach (var fragments in from InwOaPath path in paths select path.Fragments())
{
var fragments = path.Fragments();
var fragmentIndex = 0;
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
try
{
if (fragment.path?.ArrayData is not Array pathData)
var fragmentIndex = 0;
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
{
fragmentIndex++;
continue;
}
if (pathData.Length == 0)
{
fragmentIndex++;
continue;
}
try
{
// Check array rank first - COM arrays might be multidimensional
if (pathData.Rank != 1)
if (fragment.path?.ArrayData is not Array pathData || pathData.Length == 0)
{
// Try simple enumeration fallback
var fragmentHashFallback = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHashFallback))
fragmentIndex++;
continue;
}
try
{
if (pathData.Rank != 1)
{
fragmentHashes.Add(fragmentHashFallback);
var fragmentHashFallback = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHashFallback))
{
fragmentHashes.Add(fragmentHashFallback);
}
fragmentIndex++;
continue;
}
var lowerBound = pathData.GetLowerBound(0);
var upperBound = pathData.GetUpperBound(0);
var arrayLength = upperBound - lowerBound + 1;
var pathInts = new int[arrayLength];
for (int i = lowerBound; i <= upperBound; i++)
{
try
{
var value = pathData.GetValue(i);
var arrayIndex = i - lowerBound;
pathInts[arrayIndex] = System.Convert.ToInt32(value);
}
catch (Exception ex) when (ex is COMException or InvalidCastException)
{
var errorType = ex is COMException ? "COM array access failed" : "Type conversion failed";
_logger.LogDebug(ex, "{ErrorType} at index {Index}", errorType, i);
}
}
var fragmentHash = string.Join("_", pathInts);
fragmentHashes.Add(fragmentHash);
}
catch (Exception ex) when (ex is COMException or IndexOutOfRangeException or RankException)
{
var errorType = ex switch
{
COMException => "COM access failed",
IndexOutOfRangeException => "Array bounds exceeded",
RankException => "Array rank mismatch",
_ => "Error"
};
_logger.LogDebug(ex, "{ErrorType} processing fragment {FragmentIndex}, trying simple enumeration", errorType, fragmentIndex);
var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHash))
{
fragmentHashes.Add(fragmentHash);
}
fragmentIndex++;
continue;
}
var lowerBound = pathData.GetLowerBound(0);
var upperBound = pathData.GetUpperBound(0);
var arrayLength = upperBound - lowerBound + 1;
var pathInts = new int[arrayLength];
for (int i = lowerBound; i <= upperBound; i++)
{
try
{
var value = pathData.GetValue(i);
var arrayIndex = i - lowerBound;
pathInts[arrayIndex] = System.Convert.ToInt32(value);
}
catch (Exception ex) when (ex is InvalidCastException or OverflowException or FormatException)
{
// Skip invalid array values
}
}
var fragmentHash = string.Join("_", pathInts);
fragmentHashes.Add(fragmentHash);
}
catch (Exception ex) when (ex is InvalidCastException or IndexOutOfRangeException or RankException)
{
// Try simple enumeration as fallback
var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHash))
{
fragmentHashes.Add(fragmentHash);
}
fragmentIndex++;
continue;
}
fragmentIndex++;
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
}
}
string fragmentId;
if (fragmentHashes.Count > 0)
{
// Sort to ensure consistent ordering
fragmentHashes.Sort();
var rawData = string.Join("__", fragmentHashes);
fragmentId = HashRawData(rawData);
var fragmentId = HashRawData(rawData);
return fragmentId;
}
else
{
fragmentId = string.Empty;
return string.Empty;
}
// Cache the result for future use
if (!string.IsNullOrEmpty(fragmentId))
{
_fragmentIdCache.TryAdd(pathsHashCode, fragmentId);
}
return fragmentId;
}
catch (Exception ex)
when (ex
is InvalidCastException
or IndexOutOfRangeException
or OverflowException
or ArgumentException
or COMException
)
catch (Exception ex) when (ex is COMException or InvalidCastException or IndexOutOfRangeException)
{
_logger.LogWarning(ex, "Failed to generate fragment ID due to {ExceptionType}", ex.GetType().Name);
var errorType = ex switch
{
COMException => "COM access failed",
InvalidCastException => "Type conversion failed",
IndexOutOfRangeException => "Array bounds exceeded",
_ => "Error"
};
_logger.LogWarning(ex, "{ErrorType} generating fragment ID", errorType);
return string.Empty;
}
}
/// <summary>
/// Simple array enumeration fallback when bounds access fails.
/// Tries to enumerate array by simple sequential access.
/// </summary>
private string TrySimpleArrayEnumeration(Array pathData, int fragmentIndex)
{
try
{
var values = new List<string>();
var maxAttempts = Math.Min(pathData.Length, 20); // Limit attempts to avoid infinite loops
var maxAttempts = Math.Min(pathData.Length, 20);
for (int i = 0; i < maxAttempts; i++)
{
@@ -501,18 +437,14 @@ 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 (Exception ex)
when (ex is InvalidCastException or OverflowException or FormatException or ArgumentException)
catch (InvalidCastException ex)
{
_logger.LogWarning(ex, "Fragment {FragmentIndex} failed to convert value at index {Index}", fragmentIndex, i);
_logger.LogDebug(ex, "Type conversion failed at index {Index}", i);
}
}
@@ -521,96 +453,44 @@ public class GeometryToSpeckleConverter(
return string.Empty;
}
var hash = string.Join("_", values);
return hash;
return string.Join("_", values);
}
catch (Exception ex) when (ex is COMException or InvalidCastException or ArgumentException)
catch (COMException ex)
{
_logger.LogWarning(ex, "Simple enumeration failed for fragment {FragmentIndex}", fragmentIndex);
_logger.LogDebug(ex, "COM enumeration failed for fragment {FragmentIndex}", fragmentIndex);
return string.Empty;
}
}
/// <summary>
/// Generates a fast hash code for paths collection for caching purposes
/// </summary>
private static int GenerateFastPathsHashCode(InwSelectionPathsColl paths)
{
unchecked
{
int hash = 17;
hash = hash * 23 + paths.Count;
var processed = 0;
foreach (InwOaPath path in paths)
{
if (path.ArrayData is Array { Length: > 0 } pathData)
{
// Sample first few elements for performance
var sampleSize = Math.Min(pathData.Length, 4);
for (int i = 0; i < sampleSize; i++)
{
hash = hash * 23 + (pathData.GetValue(i)?.GetHashCode() ?? 0);
}
hash = hash * 23 + pathData.Length;
}
// Limit processing for performance
if (++processed >= 8)
{
break;
}
}
return hash;
}
}
/// <summary>
/// Creates a fast hash of the raw fragment data using .NET's HashCode struct.
/// For performance, we use HashCode instead of SHA256 for fragment IDs.
/// </summary>
/// <returns>Hash as hex string</returns>
private static string HashRawData(string rawData)
{
var hashCode = rawData.GetHashCode();
return hashCode.ToString("X8");
using var sha256 = SHA256.Create();
var inputBytes = Encoding.UTF8.GetBytes(rawData);
var hashBytes = sha256.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
/// <summary>
/// Extracts untransformed base geometry from fragments.
/// This geometry will be stored once and referenced by instances.
/// </summary>
private Base? ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
{
var processor = new PrimitiveProcessor(_isUpright);
// Process fragments without transforms to get base geometry
foreach (var fragment in fragmentStack)
{
// Use identity transform to get untransformed geometry
double[] identityTransform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
processor.LocalToWorldTransformation = identityTransform;
processor.LocalToWorldTransformation = s_identityTransform;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
// Create mesh from untransformed geometry
return processor.Triangles.Count > 0 ? CreateMesh(processor.Triangles) : null;
}
/// <summary>
/// Creates an instance reference to shared geometry stored in the InstanceStoreManager.
/// This is returned instead of full geometry for shared instances.
/// </summary>
private List<Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
{
var transform = ExtractInstanceTransform(paths);
var instanceReference = new InstanceProxy
{
definitionId = $"def_{fragmentId}",
definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}",
transform = transform,
units = _settings.Derived.SpeckleUnits,
maxDepth = 0,
@@ -620,113 +500,100 @@ 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 new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
return s_identityMatrix;
}
var firstPath = paths.Cast<InwOaPath>().First();
using var fragments = new ComScope<InwNodeFragsColl>(firstPath.Fragments());
if (fragments.Value.Count == 0)
var fragments = firstPath.Fragments();
try
{
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
}
if (fragments.Count == 0)
{
return s_identityMatrix;
}
var fragmentStack = new Stack<InwOaFragment3>();
// Get the first fragment's transform matrix
foreach (var frag in fragments.Value.OfType<InwOaFragment3>())
{
try
var fragmentStack = new Stack<InwOaFragment3>();
foreach (var frag in fragments.OfType<InwOaFragment3>())
{
if (frag.path?.ArrayData is not Array pathData1 || firstPath.ArrayData is not Array pathData2)
{
continue;
}
// Use IsSameFragmentPath for consistency and performance
if (IsSameFragmentPath(pathData1, pathData2))
var pathArray1 = pathData1.Cast<int>().ToArray();
var pathArray2 = pathData2.Cast<int>().ToArray();
if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2))
{
fragmentStack.Push(frag);
}
}
catch (COMException ex)
var fragment = fragmentStack.First();
var matrix = fragment.GetLocalToWorldMatrix();
if (matrix is InwLTransform3f3 { Matrix: Array matrixArray })
{
_logger.LogWarning(ex, "COM exception accessing fragment path data, skipping fragment");
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);
}
}
if (fragmentStack.Count == 0)
finally
{
_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);
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
}
}
catch (Exception ex)
when (ex
is COMException
or InvalidCastException
or IndexOutOfRangeException
or ArgumentException
or NullReferenceException
)
catch (Exception ex) when (ex is COMException or InvalidCastException or NullReferenceException)
{
_logger.LogWarning(
ex,
"Failed to extract instance transform ({ExceptionType}) - returning identity matrix",
ex.GetType().Name
);
var errorType = ex switch
{
COMException => "COM access failed",
InvalidCastException => "Transform matrix type conversion failed",
NullReferenceException => "Null reference",
_ => "Error"
};
_logger.LogWarning(ex, "{ErrorType} extracting instance transform", errorType);
}
return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
return s_identityMatrix;
}
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;
@@ -750,11 +617,6 @@ public class GeometryToSpeckleConverter(
return doubleArray;
}
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()
);
private static bool AreFragmentPathsEqual(Array a1, Array a2) =>
a1.Length == a2.Length && a1.Cast<int>().SequenceEqual(a2.Cast<int>());
}
@@ -32,4 +32,64 @@ 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;
}
}
}
@@ -87,15 +87,6 @@ public sealed class DisplayValueExtractor
// 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:
@@ -49,11 +49,20 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter<DB.Element, Dicti
switch (target)
{
case DBA.Railing railing:
// railings can have subelements including top rails, hand rails, and balusters.
// railings can have sub-elements including top rails, handrails, and balusters.
// they also do *not* have any materials associated with their category.
List<DB.ElementId> railingElementIds = [railing.GetTypeId(), railing.TopRail, .. railing.GetHandRails()];
// TopRail is now a separate child element with its own material quantities (see CNX-2806)
List<DB.ElementId> railingElementIds = [railing.GetTypeId(), .. 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,61 +137,11 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
private IEnumerable<RevitObject> GetElementChildren(DB.Element element)
{
switch (element)
var childrenIds = element.GetKnownChildrenElements();
foreach (var childrenId in childrenIds)
{
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));
var childElement = _converterSettings.Current.Document.GetElement(childrenId);
yield return Convert(childElement);
}
}
@@ -205,7 +155,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
/// </returns>
/// <remarks>
/// <para>
/// This is a bit of a code smell. This method is doing to much, "this ... AND this...".
/// This is a bit of a code smell. This method is doing too much, "this ... AND this...".
/// </para>
/// <para>
/// But, given a mesh:
@@ -1,9 +1,11 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.Common.ToHost;
using Speckle.Objects.Data;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.Rhino.ToHost.TopLevel;
@@ -27,6 +29,7 @@ public class DataObjectConverter
private readonly ITypedConverter<SOG.Region, RG.Hatch> _regionConverter;
private readonly ITypedConverter<SOG.SubDX, List<RG.GeometryBase>> _subdConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
public DataObjectConverter(
ITypedConverter<SOG.Arc, RG.ArcCurve> arcConverter,
@@ -43,7 +46,8 @@ public class DataObjectConverter
ITypedConverter<SOG.Polycurve, RG.PolyCurve> polycurveConverter,
ITypedConverter<SOG.Region, RG.Hatch> regionConverter,
ITypedConverter<SOG.SubDX, List<RG.GeometryBase>> subdConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
IConverterSettingsStore<RhinoConversionSettings> settingsStore,
IDataObjectInstanceRegistry dataObjectInstanceRegistry
)
{
_arcConverter = arcConverter;
@@ -61,13 +65,39 @@ public class DataObjectConverter
_regionConverter = regionConverter;
_subdConverter = subdConverter;
_settingsStore = settingsStore;
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
}
public object Convert(Base target) => Convert((DataObject)target);
private List<RG.GeometryBase> GetConvertedGeometry(Base b)
public List<(RG.GeometryBase a, Base b)> Convert(DataObject target)
{
return b switch
var resultPairs = new List<(RG.GeometryBase, Base)>();
// check if display value contains InstanceProxies - register for special handling
if (target.displayValue.Count > 0 && target.displayValue[0] is InstanceProxy)
{
var instanceProxies = target.displayValue.Cast<InstanceProxy>().ToList();
_dataObjectInstanceRegistry.Register(target.applicationId ?? target.id.NotNull(), target, instanceProxies);
return resultPairs; // empty - will be handled by instance baker
}
// normal display value conversion
foreach (var item in target.displayValue)
{
var converted = GetConvertedGeometry(item);
foreach (var geom in converted)
{
geom.Transform(GetUnitsTransform(item));
resultPairs.Add((geom, item));
}
}
return resultPairs;
}
private List<RG.GeometryBase> GetConvertedGeometry(Base b) =>
b switch
{
SOG.Arc arc => new() { _arcConverter.Convert(arc) },
SOG.BrepX brep => _brepConverter.Convert(brep),
@@ -85,23 +115,6 @@ public class DataObjectConverter
SOG.SubDX subd => _subdConverter.Convert(subd),
_ => throw new ConversionException($"Found unsupported fallback geometry: {b.GetType()}")
};
}
public List<(RG.GeometryBase a, Base b)> Convert(DataObject target)
{
var result = new List<RG.GeometryBase>();
foreach (var item in target.displayValue)
{
var converted = GetConvertedGeometry(item);
foreach (var x in converted)
{
x.Transform(GetUnitsTransform(item));
result.Add(x);
}
}
return result.Zip(target.displayValue, (a, b) => (a, b)).ToList();
}
private RG.Transform GetUnitsTransform(Base speckleObject)
{
@@ -314,6 +314,7 @@
"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, )"
@@ -329,6 +330,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"speckle.testing": {
"type": "Project",
"dependencies": {
@@ -259,6 +259,7 @@
"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, )"
@@ -274,6 +275,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[2.2.0, )",
@@ -549,6 +557,7 @@
"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, )"
@@ -564,6 +573,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[2.2.0, )",
@@ -259,6 +259,7 @@
"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, )"
@@ -267,6 +268,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[2.2.0, )",
@@ -536,6 +544,7 @@
"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, )"
@@ -544,6 +553,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[2.2.0, )",
@@ -493,6 +493,7 @@
"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, )"
@@ -254,6 +254,7 @@
"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, )"
@@ -308,6 +308,7 @@
"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, )"
@@ -316,6 +317,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"speckle.testing": {
"type": "Project",
"dependencies": {
@@ -22,7 +22,6 @@ public static class ContainerRegistration
serviceCollection.AddSingleton<IAccountService, AccountService>();
serviceCollection.AddSingleton<IMixPanelManager, MixPanelManager>();
serviceCollection.AddSingleton<ISerializationOptions, SerializationOptions>();
serviceCollection.AddTransient(typeof(ILogger<>), typeof(Logger<>));
}
}
@@ -30,22 +30,22 @@ public class RootObjectUnpacker
TryGetLevelProxies(root)
);
public IReadOnlyCollection<TraversalContext> GetObjectsToConvert(Base root) =>
private IReadOnlyCollection<TraversalContext> GetObjectsToConvert(Base root) =>
_traverseFunction.Traverse(root).Where(obj => obj.Current is not Collection).ToArray();
public IReadOnlyCollection<ColorProxy>? TryGetColorProxies(Base root) =>
private IReadOnlyCollection<ColorProxy>? TryGetColorProxies(Base root) =>
TryGetProxies<ColorProxy>(root, ProxyKeys.COLOR);
public IReadOnlyCollection<RenderMaterialProxy>? TryGetRenderMaterialProxies(Base root) =>
private IReadOnlyCollection<RenderMaterialProxy>? TryGetRenderMaterialProxies(Base root) =>
TryGetProxies<RenderMaterialProxy>(root, ProxyKeys.RENDER_MATERIAL);
public IReadOnlyCollection<InstanceDefinitionProxy>? TryGetInstanceDefinitionProxies(Base root) =>
private IReadOnlyCollection<InstanceDefinitionProxy>? TryGetInstanceDefinitionProxies(Base root) =>
TryGetProxies<InstanceDefinitionProxy>(root, ProxyKeys.INSTANCE_DEFINITION);
public IReadOnlyCollection<GroupProxy>? TryGetGroupProxies(Base root) =>
private IReadOnlyCollection<GroupProxy>? TryGetGroupProxies(Base root) =>
TryGetProxies<GroupProxy>(root, ProxyKeys.GROUP);
public IReadOnlyCollection<LevelProxy>? TryGetLevelProxies(Base root) =>
private IReadOnlyCollection<LevelProxy>? TryGetLevelProxies(Base root) =>
TryGetProxies<LevelProxy>(root, ProxyKeys.LEVEL);
public (
@@ -59,11 +59,11 @@ public class RootObjectUnpacker
{
if (tc.Current is IInstanceComponent)
{
instanceComponents.Add(tc);
instanceComponents.Add(tc); // handles actual blocks / instances
}
else
{
atomicObjects.Add(tc);
atomicObjects.Add(tc); // handles DataObject which INCLUDES DataObject with proxified displayValue(s)
}
if (tc.Current is DataObject dataObject)
@@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<ProjectReference Include="..\Speckle.Connectors.Logging\Speckle.Connectors.Logging.csproj" />
<ProjectReference Include="..\Speckle.Converters.Common\Speckle.Converters.Common.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<Reference Include="System.Net.Http" />
@@ -292,6 +292,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"Microsoft.Extensions.Logging": {
"type": "CentralTransitive",
"requested": "[2.2.0, )",
@@ -557,6 +564,13 @@
"speckle.connectors.logging": {
"type": "Project"
},
"speckle.converters.common": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )",
"Speckle.Objects": "[3.9.0, )"
}
},
"Microsoft.Extensions.Logging": {
"type": "CentralTransitive",
"requested": "[2.2.0, )",
@@ -0,0 +1,56 @@
using Speckle.Objects.Data;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.Common.ToHost;
/// <summary>
/// Tracks DataObjects with InstanceProxy display values that need special handling during on load.
/// </summary>
/// <remarks>
/// In Rhino-land: converter registers these instead of returning geometry, and the instance baker uses this to create
/// grouped block instances with proper metadata applied.
/// </remarks>
public sealed class DataObjectInstanceRegistry : IDataObjectInstanceRegistry
{
private readonly Dictionary<string, DataObjectInstanceEntry> _entries = new();
private readonly Dictionary<string, string> _instanceProxyToDataObject = new();
private readonly Dictionary<string, List<string>> _dataObjectToBakedInstances = new();
public void Register(string dataObjectId, DataObject dataObject, List<InstanceProxy> instanceProxies)
{
_entries[dataObjectId] = new DataObjectInstanceEntry(dataObject, instanceProxies);
// track reverse mapping for each proxy
foreach (var proxy in instanceProxies)
{
var proxyId = proxy.applicationId ?? proxy.id.NotNull();
_instanceProxyToDataObject[proxyId] = dataObjectId;
}
_dataObjectToBakedInstances[dataObjectId] = new List<string>();
}
public bool IsRegistered(string dataObjectId) => _entries.ContainsKey(dataObjectId);
/// <inheritdoc />
public IReadOnlyDictionary<string, DataObjectInstanceEntry> GetEntries() => _entries;
public void LinkInstanceToDataObject(string instanceProxyId, string bakedInstanceId)
{
if (_instanceProxyToDataObject.TryGetValue(instanceProxyId, out var dataObjectId))
{
_dataObjectToBakedInstances[dataObjectId].Add(bakedInstanceId);
}
}
public List<string> GetInstanceIdsForDataObject(string dataObjectId) =>
_dataObjectToBakedInstances.TryGetValue(dataObjectId, out var ids) ? ids : new List<string>();
public void Clear()
{
_entries.Clear();
_instanceProxyToDataObject.Clear();
_dataObjectToBakedInstances.Clear();
}
}
@@ -0,0 +1,36 @@
using Speckle.Objects.Data;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.Common.ToHost;
/// <summary>
/// Tracks DataObjects with InstanceProxy display values that need special handling during instance baking.
/// </summary>
public interface IDataObjectInstanceRegistry
{
void Register(string dataObjectId, DataObject dataObject, List<InstanceProxy> instanceProxies);
bool IsRegistered(string dataObjectId);
/// <summary>
/// Dictionary of data object id to DataObjectInstanceEntry (which holds the DataObject and instance proxies
/// that form the display value of that data object).
/// </summary>
IReadOnlyDictionary<string, DataObjectInstanceEntry> GetEntries();
void Clear();
/// <summary>
/// Links a baked instance ID back to its parent DataObject.
/// Called after instance baking to track which instances belong to which DataObject.
/// </summary>
void LinkInstanceToDataObject(string instanceProxyId, string bakedInstanceId);
/// <summary>
/// Gets all baked instance IDs for a given DataObject.
/// </summary>
List<string> GetInstanceIdsForDataObject(string dataObjectId);
}
/// <summary>
/// Represents a DataObject with InstanceProxy display values awaiting instance baking.
/// </summary>
public sealed record DataObjectInstanceEntry(DataObject DataObject, List<InstanceProxy> InstanceProxies);