Compare commits

...

18 Commits

Author SHA1 Message Date
Björn Steinhagen 3090b5f5bb Merge pull request #1207 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
merge dev -> main
2025-12-03 12:31:27 +02:00
Björn Steinhagen 7c609c93ae fix(grasshopper): syncing GeometryBase to Base for re-publish workflows (#1205) 2025-12-01 15:46:23 +03:00
Jedd Morgan 36a6572483 Merge pull request #1204 from specklesystems/main
back merge Main -> dev
2025-12-01 10:56:30 +00:00
Jedd Morgan ce04d2fd55 Merge pull request #1203 from specklesystems/dev
Back merge dev -> main
2025-12-01 10:53:36 +00:00
Jedd Morgan 4dbe4dd9a0 fix(rhino-importer): Ensure non-zero exit code is propagated (#1199)
* Ensure background service exit codes are properly propagated

* detail log

* fail fast

* log

* env exit works just as well

* add comment

* Use central management for importer version
2025-12-01 10:51:29 +00:00
Björn Steinhagen 6f72402b76 fix(revit): rebar and area reinforcement transforms with reference points (#1202)
* fix: rebar transforms

* fix: area reinf.
2025-11-28 17:25:37 +02:00
Björn Steinhagen a7b3ae8780 refactor(grasshopper): appending "with Token" to "Speckle Model URL" component name (#1201) 2025-11-28 11:41:47 +00:00
Björn Steinhagen 5da534aeb7 refactor(grasshopper): renaming component naming for automatic loading (#1200) 2025-11-28 13:37:26 +02:00
Björn Steinhagen 93ede98135 fix(grasshopper): CastFrom with long (#1197)
* fix: cast from with long

* chore: be loud about future casting fails
2025-11-26 12:43:13 +02:00
Jedd Morgan 03cffcdf4c Merge pull request #1196 from specklesystems/dependabot/github_actions/actions/checkout-6
chore(deps): bump actions/checkout from 5 to 6
2025-11-24 22:40:43 +03:00
dependabot[bot] c533d0922a chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 14:54:29 +00:00
Björn Steinhagen c24fb7eaa0 fix(rhino): remove control characters from revit families for rhino layer names (#1195) 2025-11-19 09:44:22 +00:00
Oğuzhan Koral 0b246cf95c Merge pull request #1193 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Update dev into main
2025-11-18 15:50:22 +03:00
Jonathon Broughton 003e310089 Adds room ID properties for family instances (#1192)
Introduces extraction of 'toRoomId' and 'fromRoomId' for family instance elements, enhancing the representation of relationships between doors and associated rooms.

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

* adds rhino views

* updates with new proxy field

* adds views to revit send

* updates views to not include parallel projections

also adds rootkeys to connectors common

* updates to 3.8.0 sdk

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

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

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

* Refactors Revit category extraction

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

Additionally, refactors the extractor to a static class.

* Improves property conversion and handling

Introduces robust property conversion and handling logic.

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

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

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

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

Relates to CNX-2784

* Standardizes numerical property dictionary creation.

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

Relates to CNX-2784

* Refactors property conversion to service

Moves property conversion logic into a dedicated service.

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

* Refactors property conversion for consistency

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

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

Relates to CNX-2784

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

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

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

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

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

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

* Moves UI Units cache to service

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

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

Relates to CNX-2784

* Fixes unit label typos.

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

Relates to CNX-2784

* Ensures correct units are used on send

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

Relates to CNX-2784

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

---------

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

Creates a singleton service for deduplicating geometry during conversion.

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

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

* Adds instance store manager for geometry

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

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

* Adds shared project file for Navisworks converter

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

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

* Implements geometry instancing for Navisworks

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

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

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

* Introduces instance store management

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

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

* Refactors display value extraction

Streamlines display value extraction by injecting the geometry converter.

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

* Adds instance geometry support to Navisworks connector

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

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

Addresses issues with duplicate geometry and improves stream performance.

* Fixes geometry processing for instance handling

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

* LFG!!!

Matrix4x4 Transposed
Adds an application ID to instance references.

* Fixes RenderMaterials

* Refactors dependency injection for settings

* Reduces comment clutter in Navisworks converters

Removes unnecessary comments to enhance code readability
and maintainability.

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

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

* Renames methods for clarity and updates logic

Refactors method names to improve readability and understanding of functionality.

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

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

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

* Refines COM object management and method naming

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

* Refactors constants usage for geometry identification

Introduces new constant definitions to standardise fragment ID prefixing.

* Refactors Navisworks build process for resilience

Adds error checking to ensure the Navisworks version is set before build occurs, and improves error handling to avoid empty output directories.

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

Addresses CNX-2788

* Improves geometry retrieval and checks

Refactors geometry definition retrieval for better readability.

* Refactors property name sanitisation logic

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

* Refactors method signature for clarity

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

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

* Enhances fragment processing logic and error handling

Improves null handling for selection paths to prevent crashes.

* Changes fragment ID method visibility to public

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

* implementing more robust error handling during COM
interop operations.

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

* Refactors parameter list for clarity

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2025-11-17 19:37:02 +03:00
58 changed files with 1663 additions and 389 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
file_version: ${{ steps.set-version.outputs.file_version }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 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>
@@ -15,6 +15,7 @@ using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.DUI.Models.Card.SendFilter;
using Speckle.Connectors.DUI.WebView;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using Speckle.Sdk.Models.GraphTraversal;
@@ -52,6 +53,9 @@ public static class NavisworksConnectorServiceRegistration
serviceCollection.AddScoped<NavisworksMaterialUnpacker>();
serviceCollection.AddScoped<NavisworksColorUnpacker>();
// Register dual shared geometry stores for instancing pattern
serviceCollection.AddScoped<InstanceStoreManager>();
serviceCollection.AddSingleton<IAppIdleManager, NavisworksIdleManager>();
// Sending operations
@@ -16,7 +16,13 @@ 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 +77,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 +130,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);
}
}
}
}
@@ -1,7 +1,11 @@
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;
using Speckle.Converters.Common;
using Speckle.Objects.Other;
using Speckle.Sdk;
@@ -11,12 +15,17 @@ namespace Speckle.Connector.Navisworks.HostApp;
public class NavisworksMaterialUnpacker(
ILogger<NavisworksMaterialUnpacker> logger,
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
IElementSelectionService selectionService
IElementSelectionService selectionService,
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,
@@ -64,26 +73,87 @@ public class NavisworksMaterialUnpacker(
var navisworksObjectId = selectionService.GetModelItemPath(navisworksObject);
var finalId = mergedIds.TryGetValue(navisworksObjectId, out var mergedId) ? mergedId : navisworksObjectId;
string hashId = "";
try
{
var item = selectionService.GetModelItemFromPath(finalId);
var comSelection = ComApiBridge.ToInwOpSelection([item]);
try
{
var paths = comSelection.Paths();
try
{
if (paths.Count > 0)
{
var firstPath = paths.OfType<InwOaPath>().FirstOrDefault();
if (firstPath != null)
{
var fragments = firstPath.Fragments();
try
{
if (fragments.Count > 1)
{
var fragmentId = converter.GenerateFragmentId(paths);
hashId = $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}";
}
}
finally
{
if (fragments != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragments);
}
}
}
}
}
finally
{
if (paths != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(paths);
}
}
}
finally
{
if (comSelection != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection);
}
}
}
catch (Exception ex) when (!ex.IsFatal())
{ // If COM interop fails during hash generation, fall back to using finalId
logger.LogWarning(
ex,
"Failed to generate fragment hash ID for item {ItemId}, using finalId as fallback",
finalId
);
hashId = "";
}
var geometry = navisworksObject.Geometry;
var mode = converterSettings.Current.User.VisualRepresentationMode;
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(),
@@ -92,9 +162,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)
{
@@ -106,7 +175,6 @@ public class NavisworksMaterialUnpacker(
}
}
// Check Material category for material name
var materialPropertyCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Material");
if (materialPropertyCategory != null)
{
@@ -120,19 +188,14 @@ public class NavisworksMaterialUnpacker(
if (renderMaterialProxies.TryGetValue(renderMaterialId.ToString(), out RenderMaterialProxy? value))
{
value.objects.Add(finalId);
value.objects.Add(!string.IsNullOrEmpty(hashId) ? hashId : finalId);
}
else
{
renderMaterialProxies[renderMaterialId.ToString()] = new RenderMaterialProxy()
{
value = ConvertRenderColorAndTransparencyToSpeckle(
materialName,
renderTransparency,
renderColor,
renderMaterialId
),
objects = [finalId]
value = CreateRenderMaterial(materialName, renderTransparency, renderColor, renderMaterialId),
objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId]
};
}
}
@@ -145,7 +208,7 @@ public class NavisworksMaterialUnpacker(
return renderMaterialProxies.Values.ToList();
}
private static RenderMaterial ConvertRenderColorAndTransparencyToSpeckle(
private static RenderMaterial CreateRenderMaterial(
string name,
double transparency,
NAV.Color navisworksColor,
@@ -156,7 +219,9 @@ 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,
@@ -6,6 +6,7 @@ using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Operations;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using Speckle.Objects.Data;
@@ -25,7 +26,9 @@ public class NavisworksRootObjectBuilder(
ISdkActivityFactory activityFactory,
NavisworksMaterialUnpacker materialUnpacker,
NavisworksColorUnpacker colorUnpacker,
IElementSelectionService elementSelectionService
IElementSelectionService elementSelectionService,
IUiUnitsCache uiUnitsCache,
InstanceStoreManager instanceStoreManager
) : IRootObjectBuilder<NAV.ModelItem>
{
private bool SkipNodeMerging { get; set; }
@@ -40,32 +43,45 @@ 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 = false;
SkipNodeMerging = true;
#endif
using var activity = activityFactory.Start("Build");
ValidateInputs(navisworksModelItems, projectId, onOperationProgressed);
// 2. Initialize root collection
var rootCollection = InitializeRootCollection();
// 3. Convert all model items and store results
var (convertedElements, conversionResults) = await ConvertModelItemsAsync(
navisworksModelItems,
projectId,
onOperationProgressed,
cancellationToken
);
(Dictionary<string, Base?> convertedElements, List<SendConversionResult> conversionResults) =
await ConvertModelItemsAsync(navisworksModelItems, projectId, onOperationProgressed, cancellationToken);
ValidateConversionResults(conversionResults);
var groupedNodes = SkipNodeMerging ? [] : GroupSiblingGeometryNodes(navisworksModelItems);
var finalElements = BuildFinalElements(convertedElements, groupedNodes);
List<Base> geometryDefinitions = instanceStoreManager.GetGeometryDefinitions();
await AddProxiesToCollection(rootCollection, navisworksModelItems, groupedNodes);
rootCollection.elements = finalElements;
var geometryDefinitionsCollection = new Collection
{
name = "Geometry Definitions",
["units"] = converterSettings.Current.Derived.SpeckleUnits,
elements = geometryDefinitions
};
var mainElementsCollection = new Collection
{
name = rootCollection.name,
["units"] = converterSettings.Current.Derived.SpeckleUnits,
elements = finalElements
};
rootCollection.elements = [mainElementsCollection];
if (geometryDefinitions.Count > 0)
{
rootCollection.elements.Add(geometryDefinitionsCollection);
}
return new RootObjectBuilderResult(rootCollection, conversionResults);
}
@@ -137,12 +153,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(
@@ -151,12 +165,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;
}
@@ -213,24 +224,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
{
@@ -238,16 +242,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)
@@ -255,14 +254,16 @@ public class NavisworksRootObjectBuilder(
return null;
}
(string name, string path) = GetContext(convertedBase.applicationId);
(string name, string path) = GetElementNameAndPath(convertedBase.applicationId);
var units = uiUnitsCache.Ensure();
return new NavisworksObject
{
name = name,
displayValue = convertedBase["displayValue"] as List<Base> ?? [],
properties = convertedBase["properties"] as Dictionary<string, object?> ?? [],
units = converterSettings.Current.Derived.SpeckleUnits,
units = units.ToString(),
applicationId = convertedBase.applicationId,
["path"] = path
};
@@ -288,18 +289,16 @@ public class NavisworksRootObjectBuilder(
rootCollection[ProxyKeys.COLOR] = colors;
}
var instanceDefinitionProxies = instanceStoreManager.GetInstanceDefinitionProxies();
if (instanceDefinitionProxies.Count > 0)
{
rootCollection[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies.ToList();
}
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,
@@ -58,6 +58,7 @@ public static class ServiceRegistration
serviceCollection.AddScoped<SendOperation<DocumentToConvert>>();
serviceCollection.AddScoped<ElementUnpacker>();
serviceCollection.AddScoped<LevelUnpacker>();
serviceCollection.AddScoped<ViewUnpacker>();
serviceCollection.AddScoped<SendCollectionManager>();
serviceCollection.AddScoped<IRootObjectBuilder<DocumentToConvert>, RevitRootObjectBuilder>();
serviceCollection.AddSingleton<ISendConversionCache, SendConversionCache>();
@@ -0,0 +1,87 @@
using Autodesk.Revit.DB;
using Microsoft.Extensions.Logging;
using Speckle.Objects.Other;
using Speckle.Sdk;
namespace Speckle.Connectors.Revit.HostApp;
/// <summary>
/// Unpacks Revit Views for sending
/// </summary>
public class ViewUnpacker
{
private readonly ILogger<ViewUnpacker> _logger;
private readonly Converters.Common.IRootToSpeckleConverter _rootToSpeckleConverter;
public ViewUnpacker(Converters.Common.IRootToSpeckleConverter rootToSpeckleConverter, ILogger<ViewUnpacker> logger)
{
_rootToSpeckleConverter = rootToSpeckleConverter;
_logger = logger;
}
private Camera? ConvertViewToCamera(View3D view)
{
try
{
var converted = (Camera)_rootToSpeckleConverter.Convert(view);
if (converted is null)
{
_logger.LogError("Failed to create a view from {view}", view.Name);
return null;
}
return converted;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to create a view from {view}", view.Name);
return null;
}
}
/// <summary>
/// Iterates through the 3D views in the provided document to create cameras
/// </summary>
/// <param name="doc">Document to retrieve 3D views from</param>
/// <returns></returns>
public List<Camera> Unpack(Document doc)
{
List<Camera> cameras = new();
using FilteredElementCollector collector = new(doc);
List<View> views = collector
.WhereElementIsNotElementType()
.OfCategory(BuiltInCategory.OST_Views)
.Cast<View>()
.Where(x => x.ViewType == ViewType.ThreeD)
.ToList();
foreach (View view in views)
{
if (view is not View3D view3D)
{
continue;
}
// not supporting parallel project yet, since it is too complex to match in the viewer for now
try
{
if (!view3D.IsPerspective)
{
continue;
}
}
catch (Autodesk.Revit.Exceptions.InvalidOperationException)
{
continue; // some threed views will throw an exception: returns true if view is not a view template
}
if (ConvertViewToCamera(view3D) is Camera camera)
{
cameras.Add(camera);
}
}
return cameras;
}
}
@@ -24,6 +24,7 @@ public class RevitRootObjectBuilder(
ISendConversionCache sendConversionCache,
ElementUnpacker elementUnpacker,
LevelUnpacker levelUnpacker,
ViewUnpacker viewUnpacker,
IThreadContext threadContext,
SendCollectionManager sendCollectionManager,
ILogger<RevitRootObjectBuilder> logger,
@@ -240,6 +241,7 @@ public class RevitRootObjectBuilder(
throw new SpeckleException("Failed to convert all objects.");
}
// STEP 5: Unpack proxies to attach to root collection
var flatElements = atomicObjectsByDocumentAndTransform.SelectMany(t => t.Elements).ToList();
var idsAndSubElementIds = elementUnpacker.GetElementsAndSubelementIdsFromAtomicObjects(flatElements);
@@ -260,6 +262,13 @@ public class RevitRootObjectBuilder(
}
);
// STEP 6: Unpack all other objects to attach to root collection
List<Objects.Other.Camera> views = viewUnpacker.Unpack(converterSettings.Current.Document);
if (views.Count > 0)
{
rootObject[RootKeys.VIEW] = views;
}
// NOTE: these are currently not used anywhere, we'll skip them until someone calls for it back
// rootObject[ProxyKeys.PARAMETER_DEFINITIONS] = _parameterDefinitionHandler.Definitions;
@@ -24,6 +24,7 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LevelUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LinkedModelHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitMaterialBaker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ViewUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\SupportedCategoriesUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitViewManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\HideWarningsFailuresPreprocessor.cs" />
@@ -60,4 +61,4 @@
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
</ItemGroup>
</Project>
</Project>
@@ -29,8 +29,8 @@
<ItemGroup>
<PackageReference Include="GrasshopperAsyncComponent" />
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
<PackageReference Include="System.Resources.Extensions" />
</ItemGroup>
@@ -15,7 +15,7 @@ public class TokenUrlComponent : GH_Component
{
public TokenUrlComponent()
: base(
"Speckle Model URL",
"Speckle Model URL with Token",
"URL",
"Create a Speckle model link using URL and developer token",
ComponentCategories.PRIMARY_RIBBON,
@@ -27,8 +27,7 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
protected override Bitmap Icon => Resources.speckle_properties_expand;
public override GH_Exposure Exposure => GH_Exposure.secondary;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
protected override void RegisterInputParams(GH_InputParamManager pManager) =>
pManager.AddParameter(
new SpecklePropertyGroupParam(),
"Properties",
@@ -36,7 +35,6 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
"Speckle Properties to expand",
GH_ParamAccess.item
);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager) { }
@@ -130,7 +130,7 @@ public class ReceiveAsyncComponent : GH_AsyncComponent<ReceiveAsyncComponent>
{
var autoReceiveMi = Menu_AppendItem(
menu,
"Load automatically",
"Load new versions automatically",
(s, e) =>
{
AutoReceive = !AutoReceive;
@@ -3,7 +3,9 @@ using Rhino.Geometry;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Connectors.GrasshopperShared.HostApp;
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Converters.Common;
using Speckle.Converters.Common.ToHost;
using Speckle.Converters.Rhino;
using Speckle.Sdk;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
@@ -29,6 +31,7 @@ internal sealed class LocalToGlobalMapHandler
// injected via constructor (DI-managed)
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
private readonly ILogger<LocalToGlobalMapHandler> _logger;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
// set via Initialize() method (per-operation data)
private TraversalContextUnpacker _traversalContextUnpacker = null!;
@@ -41,11 +44,13 @@ internal sealed class LocalToGlobalMapHandler
public LocalToGlobalMapHandler(
IDataObjectInstanceRegistry dataObjectInstanceRegistry,
ILogger<LocalToGlobalMapHandler> logger
ILogger<LocalToGlobalMapHandler> logger,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
{
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
_logger = logger;
_settingsStore = settingsStore;
}
/// <summary>
@@ -368,7 +373,17 @@ internal sealed class LocalToGlobalMapHandler
{
// deep copy and transform the geometry
var transformedWrapper = definitionObject.DeepCopy();
// transform the GeometryBase
transformedWrapper.GeometryBase.NotNull().Transform(transform);
// keep Base and GeometryBase in sync (fixed as of CNX-2847)
transformedWrapper.Base = SpeckleConversionContext.Current.ConvertToSpeckle(transformedWrapper.GeometryBase);
// preserve metadata from original Base
transformedWrapper.Base.applicationId = definitionObject.Base.applicationId;
transformedWrapper.Base["units"] = _settingsStore.Current.SpeckleUnits;
resolvedGeometries.Add(transformedWrapper);
}
}
@@ -79,6 +79,9 @@ public class SpecklePropertyGoo : GH_Goo<object>, ISpecklePropertyGoo
case int i:
Value = i;
return true;
case long l:
Value = l;
return true;
case string s:
Value = s;
return true;
@@ -8,6 +8,7 @@
<StartProgram>$(ProgramFiles)\Rhino $(RhinoVersion)\System\Rhino.exe</StartProgram>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UseWpf>true</UseWpf>
<PlatformTarget>x64</PlatformTarget>
<UseWindowsForms>true</UseWindowsForms>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
</PropertyGroup>
@@ -7,14 +7,15 @@
<TargetExt>.rhp</TargetExt>
<StartProgram>$(ProgramFiles)\Rhino $(RhinoVersion)\System\Rhino.exe</StartProgram>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<PlatformTarget>x64</PlatformTarget>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
</ItemGroup>
<ItemGroup>
@@ -19,8 +19,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.21.25188.17001"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.21.25188.17001"/>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
@@ -29,20 +29,20 @@
},
"RhinoCommon": {
"type": "Direct",
"requested": "[8.21.25188.17001, )",
"resolved": "8.21.25188.17001",
"contentHash": "Wo6JXheyDBvilyIwDY6xZCQJC4170jzAlTSlMgh8yokUY+vYyCl4KJVXZofIOynNt/xx5wLqb2On5gZZekXR6w==",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "PDKR9GwqyUXUkTulV4J0dzDIf/aWqSJkL7nkS8ReAx8xhnt/+RQpE8gTjOSCmkSU2tjG6WzclowbTxwMTU7VAA==",
"dependencies": {
"System.Drawing.Common": "7.0.0"
}
},
"RhinoWindows": {
"type": "Direct",
"requested": "[8.21.25188.17001, )",
"resolved": "8.21.25188.17001",
"contentHash": "9zqCorcLRBeiW/j1RTwUS4E7bnZetAdA9WDdtd/AQccjOpxdtw76wdN+ciyQ6qslseWkwZ9qSBeh7QaM800Ntw==",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "I/+++piwtYTue+iAAQqcMF5QlontqwNnC7Leyhiv2FiF8JpAl6K44ZsJqB7ZEUC6ns0LDfa3mbFzQwUfHwYumQ==",
"dependencies": {
"RhinoCommon": "[8.21.25188.17001]"
"RhinoCommon": "[8.25.25328.11001]"
}
},
"Speckle.InterfaceGenerator": {
@@ -123,6 +123,12 @@ public class RhinoLayerBaker : TraversalContextUnpacker
}
var cleanNewLayerName = RhinoUtils.CleanLayerName(collection.name);
if (!ModelComponent.IsValidComponentName(cleanNewLayerName))
{
throw new SpeckleException($"Layer name '{currentLayerName}' is not valid");
}
Layer newLayer = new() { Name = cleanNewLayerName, ParentLayerId = previousLayer?.Id ?? Guid.Empty };
// set material
@@ -150,7 +156,7 @@ public class RhinoLayerBaker : TraversalContextUnpacker
int index = currentDocument.Layers.Add(newLayer);
if (index == -1)
{
throw new SpeckleException($"Could not create layer '{currentLayerName}'.");
throw new SpeckleException($"Could not create layer '{currentLayerName}'");
}
_hostLayerCache.Add(currentLayerName, index);
@@ -1,27 +1,41 @@
using System.Text;
namespace Speckle.Connectors.Rhino.HostApp;
public static class RhinoUtils
{
public static string CleanBlockDefinitionName(string str)
{
return ReplaceChars(str, @"\/", "_");
}
private static readonly HashSet<char> s_skipChars = ['[', ']', '(', ')', '{', '}'];
private static readonly HashSet<char> s_replaceWithHyphen = [':', ';'];
public static string CleanBlockDefinitionName(string str) => str.Replace('/', '_').Replace('\\', '_');
// Cleans up layer names to be "rhino" proof. Note this can be improved, as "()[] and {}" are illegal only at the start.
// https://docs.mcneel.com/rhino/6/help/en-us/index.htm#information/namingconventions.htm?Highlight=naming
public static string CleanLayerName(string str)
{
str = ReplaceChars(str, @"[](){}", "");
return ReplaceChars(str, @":;", "-");
}
var sb = new StringBuilder(str.Length);
private static string ReplaceChars(string str, string invalidChars, string replaceString)
{
foreach (char c in invalidChars)
foreach (char c in str)
{
str = str.Replace(c.ToString(), replaceString);
if (char.IsControl(c))
{
continue; // skip control characters (shoutout cnx-2809)
}
if (s_skipChars.Contains(c))
{
continue; // skip brackets
}
if (s_replaceWithHyphen.Contains(c))
{
sb.Append('-');
continue;
}
sb.Append(c);
}
return str;
return sb.ToString();
}
}
@@ -0,0 +1,65 @@
using Microsoft.Extensions.Logging;
using Rhino.DocObjects;
using Rhino.DocObjects.Tables;
using Speckle.Converters.Common;
using Speckle.Objects.Other;
using Speckle.Sdk;
namespace Speckle.Connectors.Rhino.HostApp;
public class RhinoViewUnpacker
{
private readonly IRootToSpeckleConverter _rootToSpeckleConverter;
private readonly ILogger<RhinoViewUnpacker> _logger;
public RhinoViewUnpacker(IRootToSpeckleConverter rootToSpeckleConverter, ILogger<RhinoViewUnpacker> logger)
{
_rootToSpeckleConverter = rootToSpeckleConverter;
_logger = logger;
}
private Camera? ConvertViewToCamera(ViewInfo view)
{
try
{
var converted = (Speckle.Objects.Other.Camera)_rootToSpeckleConverter.Convert(view);
if (converted is null)
{
return null;
}
return converted;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to create a view from {view}", view.Name);
return null;
}
}
/// <summary>
/// Iterates through a given set of rhino named views to create proxies
/// </summary>
/// <param name="views">current document named views</param>
/// <returns></returns>
public List<Camera> UnpackViews(NamedViewTable views)
{
List<Camera> cameras = new();
foreach (ViewInfo view in views)
{
// skip isometric views for now.
// getting the orthographic match between host apps and the viewer requires too much effort atm.
if (view.Viewport.IsParallelProjection)
{
continue;
}
if (ConvertViewToCamera(view) is Camera camera)
{
cameras.Add(camera);
}
}
return cameras;
}
}
@@ -33,6 +33,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
private readonly RhinoGroupUnpacker _groupUnpacker;
private readonly RhinoMaterialUnpacker _materialUnpacker;
private readonly RhinoColorUnpacker _colorUnpacker;
private readonly RhinoViewUnpacker _viewUnpacker;
private readonly PropertiesExtractor _propertiesExtractor;
private readonly ILogger<RhinoRootObjectBuilder> _logger;
private readonly ISdkActivityFactory _activityFactory;
@@ -46,6 +47,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
RhinoGroupUnpacker groupUnpacker,
RhinoMaterialUnpacker materialUnpacker,
RhinoColorUnpacker colorUnpacker,
RhinoViewUnpacker viewUnpacker,
PropertiesExtractor propertiesExtractor,
ILogger<RhinoRootObjectBuilder> logger,
ISdkActivityFactory activityFactory
@@ -59,6 +61,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
_rootToSpeckleConverter = rootToSpeckleConverter;
_materialUnpacker = materialUnpacker;
_colorUnpacker = colorUnpacker;
_viewUnpacker = viewUnpacker;
_propertiesExtractor = propertiesExtractor;
_logger = logger;
_activityFactory = activityFactory;
@@ -122,20 +125,30 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
throw new SpeckleException("Failed to convert all objects."); // fail fast instead creating empty commit! It will appear as model card error with red color.
}
// 4 - Unpack all proxies for the root
// Get all layers from the created collections on the root object commit for proxy processing
List<Layer> layers = _layerUnpacker.GetUsedLayers().ToList();
using (var _ = _activityFactory.Start("UnpackRenderMaterials"))
{
// 4 - Unpack the render material proxies
rootObjectCollection[ProxyKeys.RENDER_MATERIAL] = _materialUnpacker.UnpackRenderMaterials(atomicObjects, layers);
}
using (var _ = _activityFactory.Start("UnpackColors"))
{
// 5 - Unpack the color proxies
rootObjectCollection[ProxyKeys.COLOR] = _colorUnpacker.UnpackColors(atomicObjects, layers);
}
// 5 - Unpack all other objects for the root
using (var _ = _activityFactory.Start("UnpackViews"))
{
List<Objects.Other.Camera> views = _viewUnpacker.UnpackViews(_converterSettings.Current.Document.NamedViews);
if (views.Count > 0)
{
rootObjectCollection[RootKeys.VIEW] = views;
}
}
return new RootObjectBuilderResult(rootObjectCollection, results);
}
@@ -92,6 +92,8 @@ public static class ServiceRegistration
serviceCollection.AddScoped<RhinoColorBaker>();
serviceCollection.AddScoped<RhinoColorUnpacker>();
serviceCollection.AddScoped<RhinoViewUnpacker>();
serviceCollection.AddScoped<PropertiesExtractor>();
serviceCollection.AddScoped<RevitMappingResolver>();
@@ -25,6 +25,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AttributeExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\DataObjectInstanceGrouper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Properties\PropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoViewUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoLayerHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RhinoObjectHelper.cs" />
@@ -63,4 +64,4 @@
<Compile Include="$(MSBuildThisFileDirectory)Plugin\Speckle.Connectors.RhinoCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\Speckle.Connectors.RhinoPlugin.cs" />
</ItemGroup>
</Project>
</Project>
@@ -17,4 +17,4 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellSectionResolver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\EtabsObjectToSpeckleConverter.cs" />
</ItemGroup>
</Project>
</Project>
@@ -1,38 +1,19 @@
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models;
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public class DisplayValueExtractor
public class DisplayValueExtractor(GeometryToSpeckleConverter geometryConverter)
{
private readonly IConverterSettingsStore<NavisworksConversionSettings> _converterSettings;
private readonly ILogger<DisplayValueExtractor> _logger;
private readonly GeometryToSpeckleConverter _geometryConverter;
public DisplayValueExtractor(
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
ILogger<DisplayValueExtractor> logger
)
{
_converterSettings = converterSettings;
_logger = logger;
_geometryConverter = new GeometryToSpeckleConverter(_converterSettings.Current);
}
internal List<Base> GetDisplayValue(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
throw new ArgumentNullException(nameof(modelItem));
}
if (!modelItem.HasGeometry)
{
return [];
}
return !IsElementVisible(modelItem) ? [] : _geometryConverter.Convert(modelItem);
}
internal List<Base> GetDisplayValue(NAV.ModelItem modelItem) =>
modelItem == null
? throw new ArgumentNullException(nameof(modelItem))
: !modelItem.HasGeometry
? ([])
: !IsElementVisible(modelItem)
? []
:
// this can be meshes or the instance reference objects
// the un transformed objects stored in a separate collection
geometryConverter.Convert(modelItem);
}
@@ -1,10 +1,14 @@
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converters.Common;
using static Speckle.Converter.Navisworks.Helpers.PropertyHelpers;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
public class PropertySetsExtractor(
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
IPropertyConverter propertyConverter
)
{
internal Dictionary<string, object?>? GetPropertySets(NAV.ModelItem modelItem)
{
@@ -18,6 +22,17 @@ public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionS
return propertyDictionary;
}
private static NAV.Units GetModelUnits(NAV.ModelItem modelItem)
{
NAV.ModelItem? ancestor = modelItem;
while (ancestor != null && !ancestor.HasModel)
{
ancestor = ancestor.Parent;
}
return ancestor != null ? ancestor.Model.Units : NAV.Units.Meters;
}
/// <summary>
/// Extracts property sets from a NAV.ModelItem and adds them to a dictionary,
/// PropertySets are specific to the host application source appended to Navisworks and therefore
@@ -28,10 +43,13 @@ public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionS
private Dictionary<string, object?> ExtractPropertySets(NAV.ModelItem modelItem)
{
var propertySetDictionary = new Dictionary<string, object?>();
var modelUnits = GetModelUnits(modelItem);
propertyConverter.Reset();
foreach (var propertyCategory in modelItem.PropertyCategories)
{
if (IsCategoryToBeSkipped(propertyCategory))
if (ShouldSkipCategory(propertyCategory))
{
continue;
}
@@ -40,23 +58,18 @@ public class PropertySetsExtractor(IConverterSettingsStore<NavisworksConversionS
foreach (var property in propertyCategory.Properties)
{
string sanitizedName = SanitizePropertyName(property.DisplayName);
var propertyValue = ConvertPropertyValue(property.Value, settingsStore.Current.Derived.SpeckleUnits);
var sanitizedName = SanitizePropertyName(property.DisplayName);
var propertyValue = propertyConverter.ConvertPropertyValue(property.Value, modelUnits, property.DisplayName);
if (propertyValue != null)
{
propertySet[sanitizedName] = propertyValue;
}
}
if (propertySet.Count <= 0)
if (propertySet.Count > 0)
{
continue;
propertySetDictionary[SanitizePropertyName(propertyCategory.DisplayName)] = propertySet;
}
string categoryName = SanitizePropertyName(propertyCategory.DisplayName);
propertySetDictionary[categoryName] = propertySet;
}
return propertySetDictionary;
@@ -1,8 +1,10 @@
using static Speckle.Converter.Navisworks.Helpers.PropertyHelpers;
using Speckle.Converter.Navisworks.Services;
using Speckle.InterfaceGenerator;
namespace Speckle.Converter.Navisworks.ToSpeckle;
public sealed class RevitBuiltInCategoryExtractor
[GenerateAutoInterface]
public class RevitBuiltInCategoryExtractor(IPropertyConverter converter) : IRevitBuiltInCategoryExtractor
{
private const int ANCESTOR_AND_SELF_COUNT = 4; // It seems like this is the maximum depth found needed in practice
private const string REVIT_CAT_GROUP = "LcRevitData_Element";
@@ -13,28 +15,28 @@ public sealed class RevitBuiltInCategoryExtractor
/// Attempts to map a Navisworks/Revit display category from the given model item or its ancestors
/// to a known Revit built-in category constant (e.g., "OST_Walls").
/// </summary>
internal static bool TryGetBuiltInCategory(
NAV.ModelItem item,
out string mapped,
int maxDepth = ANCESTOR_AND_SELF_COUNT
)
public bool TryGetBuiltInCategory(NAV.ModelItem item, out string mapped, int maxDepth = ANCESTOR_AND_SELF_COUNT)
{
mapped = string.Empty;
// Look up the category value, starting at this item and walking up to maxDepth ancestors
// Find the category VariantData up the hierarchy
var v = FindRevitCategoryInHierarchy(item, maxDepth);
if (v == null)
if (v is null)
{
return false;
}
var name = ConvertPropertyValue(v, "")?.ToString();
converter.Reset();
// Convert using per-object model units and current UI units
var nameObj = converter.ConvertPropertyValue(v, item.Model.Units, item.DisplayName);
var name = nameObj?.ToString();
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
name = name?.Trim();
name = name!.Trim();
// Map display name to OST_* built-in category constant
var builtInCategory = DisplayNameToRevitBuiltInCategory(name);
@@ -11,7 +11,8 @@ public class HierarchicalPropertyHandler(
PropertySetsExtractor propertySetsExtractor,
ModelPropertiesExtractor modelPropertiesExtractor,
ClassPropertiesExtractor classPropertiesExtractor,
IConverterSettingsStore<NavisworksConversionSettings> settingsStore
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
IRevitBuiltInCategoryExtractor revitCategoryExtractor
) : BasePropertyHandler(propertySetsExtractor, modelPropertiesExtractor)
{
private static string PseudoClassPropertiesKey => "_pseudoClassProperties";
@@ -22,7 +23,7 @@ public class HierarchicalPropertyHandler(
var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem) ?? [];
// Interop-lite mapping for Revit built-in categories
if (_mapRevit && RevitBuiltInCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
if (_mapRevit && revitCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
{
PropertyHelpers.AddPropertyIfNotNullOrEmpty(
propertyDict,
@@ -46,6 +46,20 @@ public static class NavisworksConverterServiceRegistration
serviceCollection.AddScoped<DisplayValueExtractor>();
serviceCollection.AddScoped<GeometryToSpeckleConverter>();
// Register dual shared geometry stores for instancing pattern (.NET Framework compatible)
// Store 1: For geometry definitions (Mesh, Curve, etc.) - Store 2: For InstanceDefinitionProxy objects
serviceCollection.AddScoped<InstanceStoreManager>();
// Register ISharedGeometryStore interface using the geometry definitions store for backward compatibility
serviceCollection.AddScoped<ISharedGeometryStore>(provider =>
provider.GetRequiredService<InstanceStoreManager>().GeometryDefinitionsStore
);
// Register settings resolved from factory
serviceCollection.AddScoped<NavisworksConversionSettings>(sp =>
sp.GetRequiredService<INavisworksConversionSettingsFactory>().Current
);
return serviceCollection;
}
}
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Text.RegularExpressions;
using Speckle.Objects.Geometry;
@@ -8,6 +8,9 @@ public static class PropertyHelpers
{
private static readonly HashSet<string> s_excludedCategories = ["Geometry", "Metadata"];
/// <summary>
/// Adds a property to an object (either a Base object or a Dictionary) if the value is not null or empty.
/// </summary>
private static readonly Dictionary<NAV.VariantDataType, Func<NAV.VariantData, string, dynamic?>> s_typeHandlers =
new()
{
@@ -47,24 +50,15 @@ public static class PropertyHelpers
return handler(value, units);
}
// Default case for unsupported types
return value.DataType == NAV.VariantDataType.None || value.DataType == NAV.VariantDataType.Point2D
? null
: value.ToString();
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))
@@ -80,9 +74,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)
@@ -98,16 +89,33 @@ 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);
}
internal static class UnitLabels
{
internal static string Linear(NAV.Units u) =>
u switch
{
NAV.Units.Kilometers => "Kilometers",
NAV.Units.Meters => "Metres",
NAV.Units.Centimeters => "Centimeters",
NAV.Units.Millimeters => "Millimeters",
NAV.Units.Micrometers => "Micrometers",
NAV.Units.Miles => "Miles",
NAV.Units.Yards => "Yards",
NAV.Units.Feet => "Feet",
NAV.Units.Inches => "Inches",
NAV.Units.Mils => "Mils",
NAV.Units.Microinches => "Microinches",
_ => "Metres"
};
internal static string Area(NAV.Units u) => $"Square {Linear(u).ToLower()}";
public static string Volume(NAV.Units u) => $"Cubic {Linear(u).ToLower()}";
}
@@ -1,4 +1,4 @@
namespace Speckle.Converter.Navisworks.Constants;
namespace Speckle.Converter.Navisworks.Constants;
public static class PathConstants
{
@@ -6,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_";
}
@@ -0,0 +1,141 @@
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Constants;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converter.Navisworks.Services;
/// <summary>
/// Simple wrapper class that manages two SharedGeometryStores instances for dual instancing pattern.
/// Provides easy access to both mesh definitions store and instance definition proxies store.
/// </summary>
public class InstanceStoreManager(ILogger<InstanceStoreManager> logger)
{
private readonly ILogger<InstanceStoreManager> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
/// <summary>
/// Store for geometry definitions (geometry data) - untransformed base geometries.
/// </summary>
internal SharedGeometryStore GeometryDefinitionsStore { get; } = new();
/// <summary>
/// Store for InstanceDefinitionProxy objects that reference geometry definitions.
/// </summary>
internal SharedGeometryStore InstanceDefinitionProxiesStore { get; } = new();
/// <summary>
/// Clears both stores for a new conversion session.
/// Should be called at the start of each conversion.
/// </summary>
public void ClearAll()
{
GeometryDefinitionsStore.Clear();
InstanceDefinitionProxiesStore.Clear();
}
/// <summary>
/// Gets all instance definition proxies from the store, cast to their specific type.
/// Useful for adding to root collection at end of conversion.
/// </summary>
public IReadOnlyCollection<InstanceDefinitionProxy> GetInstanceDefinitionProxies()
{
var proxies = InstanceDefinitionProxiesStore.Geometries.OfType<InstanceDefinitionProxy>().ToList().AsReadOnly();
_logger.LogDebug("GetInstanceDefinitionProxies returning {Count} proxies", proxies.Count);
return proxies;
}
/// <summary>
/// Gets all geometry definitions from the geometry definitions store.
/// </summary>
/// <returns></returns>
public List<Base> GetGeometryDefinitions() => [.. GeometryDefinitionsStore.Geometries.ToList().AsReadOnly()];
/// <summary>
/// Gets a geometry definition by its application ID from the geometry definitions store.
/// </summary>
/// <returns>The geometry if found, null otherwise.</returns>
public Base? GetGeometryDefinition(string fragmentId) =>
GeometryDefinitionsStore.Geometries.FirstOrDefault(g =>
g.applicationId == $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}"
);
/// <summary>
/// Gets an instance definition proxy by its application ID.
/// </summary>
/// <returns>The instance definition proxy if found, null otherwise.</returns>
public InstanceDefinitionProxy? GetInstanceDefinitionProxy(string fragmentId) =>
InstanceDefinitionProxiesStore
.Geometries.OfType<InstanceDefinitionProxy>()
.FirstOrDefault(p => p.applicationId == $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}");
/// <summary>
/// Adds a geometry definition and corresponding instance definition proxy for shared geometry.
/// This is a convenience method that handles both stores in one call.
/// </summary>
/// <param name="fragmentId">The fragment-based application ID.</param>
/// <param name="geometry">The untransformed base geometry.</param>
/// <returns>True if both were added (new geometry), false if they already existed.</returns>
public bool AddSharedGeometry(string fragmentId, Base geometry)
{
_logger.LogDebug("AddSharedGeometry called for FragmentId={FragmentId}", fragmentId);
bool geometryAdded = false;
bool proxyAdded = false;
// Create prefixed IDs for 1:1:1 relationship using base fragment hash
var geometryId = $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}";
var definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}";
_logger.LogDebug("Using GeometryId={GeometryId}, DefinitionId={DefinitionId}", geometryId, definitionId);
// Add geometry definition if not exists
if (!GeometryDefinitionsStore.Contains(geometryId))
{
geometry.applicationId = geometryId;
geometryAdded = GeometryDefinitionsStore.Add(geometry);
_logger.LogDebug("Added geometry definition: {GeometryId}, Success={Success}", geometryId, geometryAdded);
}
else
{
_logger.LogDebug("Geometry definition already exists: {GeometryId}", geometryId);
}
// Add instance definition proxy if not exists
if (!InstanceDefinitionProxiesStore.Contains(definitionId))
{
var definitionProxy = new InstanceDefinitionProxy
{
applicationId = definitionId,
name = $"Shared Geometry {fragmentId[..8]}...", // Show first 8 chars for readability
objects = [geometry.applicationId],
maxDepth = 0
};
proxyAdded = InstanceDefinitionProxiesStore.Add(definitionProxy);
_logger.LogDebug("Added instance definition proxy: {DefinitionId}, Success={Success}", definitionId, proxyAdded);
}
else
{
_logger.LogDebug("Instance definition proxy already exists: {DefinitionId}", definitionId);
}
var conversionSucceededResult = geometryAdded || proxyAdded;
_logger.LogDebug(
"AddSharedGeometry completed: FragmentId={FragmentId}, Result={Result}, GeometryAdded={GeometryAdded}, ProxyAdded={ProxyAdded}",
fragmentId,
conversionSucceededResult,
geometryAdded,
proxyAdded
);
return conversionSucceededResult;
}
/// <summary>
/// Checks if shared geometry already exists in both stores.
/// </summary>
/// <param name="fragmentId">The fragment-based application ID.</param>
/// <returns>True if geometry definition exists in both stores.</returns>
public bool ContainsSharedGeometry(string fragmentId) =>
GeometryDefinitionsStore.Contains($"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}")
// && InstanceDefinitionProxiesStore.Contains($"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}")
;
}
@@ -0,0 +1,91 @@
using System.Globalization;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.InterfaceGenerator;
namespace Speckle.Converter.Navisworks.Services;
[GenerateAutoInterface]
public class PropertyConverter(IUiUnitsCache uiUnitsCache) : IPropertyConverter
{
public void Reset() => uiUnitsCache.Reset();
public object? ConvertPropertyValue(NAV.VariantData? value, NAV.Units modelUnits, string propDisplayName) =>
value == null
? null
: _handlers.TryGetValue(value.DataType, out var f)
? f(value, (modelUnits, propDisplayName))
: value.DataType is NAV.VariantDataType.None or NAV.VariantDataType.Point2D
? null
: value.ToString();
private readonly Dictionary<
NAV.VariantDataType,
Func<NAV.VariantData, (NAV.Units model, string name), object?>
> _handlers =
new()
{
{ NAV.VariantDataType.Boolean, (v, _) => v.ToBoolean() },
{ NAV.VariantDataType.DisplayString, (v, _) => v.ToDisplayString() },
{ NAV.VariantDataType.IdentifierString, (v, _) => v.ToIdentifierString() },
{ NAV.VariantDataType.Int32, (v, _) => v.ToInt32() },
{ NAV.VariantDataType.Double, (v, _) => v.ToDouble() },
// Angle as dictionary with units
{ NAV.VariantDataType.DoubleAngle, (v, t) => NumObj(t.name, v.ToDoubleAngle(), "Degrees") },
// Length → dictionary in UI units
{
NAV.VariantDataType.DoubleLength,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
return NumObj(t.name, v.ToDoubleLength() * k, UnitLabels.Linear(uiUnits));
}
},
// Area → dictionary in UI units^2
{
NAV.VariantDataType.DoubleArea,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
k *= k;
return NumObj(t.name, v.ToDoubleArea() * k, UnitLabels.Area(uiUnits));
}
},
// Volume → dictionary in UI units^3
{
NAV.VariantDataType.DoubleVolume,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
k = k * k * k;
return NumObj(t.name, v.ToDoubleVolume() * k, UnitLabels.Volume(uiUnits));
}
},
{ NAV.VariantDataType.DateTime, (v, _) => v.ToDateTime().ToString(CultureInfo.InvariantCulture) },
{ NAV.VariantDataType.NamedConstant, (v, _) => v.ToNamedConstant().DisplayName },
{ NAV.VariantDataType.None, (_, _) => null },
{ NAV.VariantDataType.Point2D, (_, _) => null },
{
NAV.VariantDataType.Point3D,
(v, t) =>
{
var uiUnits = uiUnitsCache.Ensure();
var k = NAV.UnitConversion.ScaleFactor(t.model, uiUnits);
var p = v.ToPoint3D();
return new Speckle.Objects.Geometry.Point(p.X * k, p.Y * k, p.Z * k, UnitLabels.Linear(uiUnits));
}
}
};
private static Dictionary<string, object> NumObj(string name, double value, string units) =>
new()
{
["name"] = name,
["value"] = value,
["units"] = units
};
}
@@ -0,0 +1,92 @@
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Models;
namespace Speckle.Converter.Navisworks.Services;
[GenerateAutoInterface]
public class SharedGeometryStore : ISharedGeometryStore
{
private readonly HashSet<Base> _geometries = new();
private readonly Dictionary<string, Base> _geometriesByApplicationId = new();
private readonly object _lock = new();
/// <summary>
/// Gets a read-only collection of all stored geometries.
/// </summary>
public IReadOnlyCollection<Base> Geometries
{
get
{
lock (_lock)
{
return _geometries.ToList().AsReadOnly();
}
}
}
/// <summary>
/// Adds a geometry to the store if it doesn't already exist.
/// </summary>
/// <param name="geometry">The geometry to add.</param>
/// <returns>True if the geometry was added, false if it already existed.</returns>
public bool Add(Base geometry)
{
if (geometry == null)
{
throw new ArgumentNullException(nameof(geometry));
}
if (string.IsNullOrEmpty(geometry.applicationId))
{
throw new ArgumentException("Geometry must have an applicationId for deduplication", nameof(geometry));
}
lock (_lock)
{
if (geometry.applicationId != null && _geometriesByApplicationId.ContainsKey(geometry.applicationId))
{
return false; // Already exists
}
_geometries.Add(geometry);
if (geometry.applicationId != null)
{
_geometriesByApplicationId[geometry.applicationId] = geometry;
}
return true; // Added successfully
}
}
/// <summary>
/// Checks if a geometry with the specified application ID already exists in the store.
/// </summary>
/// <param name="applicationId">The application ID to check for.</param>
/// <returns>True if a geometry with the application ID exists, false otherwise.</returns>
public bool Contains(string applicationId)
{
if (string.IsNullOrEmpty(applicationId))
{
return false;
}
lock (_lock)
{
var contains = _geometriesByApplicationId.ContainsKey(applicationId);
return contains;
}
}
/// <summary>
/// Clears all stored geometries for a new conversion session.
/// </summary>
public void Clear()
{
lock (_lock)
{
_geometries.Clear();
_geometriesByApplicationId.Clear();
}
}
}
@@ -0,0 +1,71 @@
using Autodesk.Navisworks.Api.Interop;
using Speckle.InterfaceGenerator;
using static Autodesk.Navisworks.Api.Interop.LcUOption;
namespace Speckle.Converter.Navisworks.Services;
[GenerateAutoInterface]
public class UiUnitsCache : IUiUnitsCache
{
private NAV.Units? _ui;
public NAV.Units Ensure()
{
if (_ui.HasValue)
{
return _ui.Value;
}
UiUnitsUtil.TryGetUiLinearUnits(out var ui);
_ui = ui;
return _ui.Value;
}
public void Reset() => _ui = null;
}
public static class UiUnitsUtil
{
// disp_units: 0=linear_format
public static bool TryGetUiLinearUnits(out NAV.Units uiUnits)
{
using var opt = new LcUOptionLock();
var root = GetRoot(opt);
var disp = root.GetSubOptions("interface").GetSubOptions("disp_units");
int code = -1;
using var v = new NAV.VariantData();
disp.GetValue(0, v);
var s = v.ToString();
var colon = s.LastIndexOf(':');
var open = s.IndexOf('(', colon + 1);
if (colon >= 0 && open > colon && !int.TryParse(s.Substring(colon + 1, open - colon - 1), out code))
{
code = -1;
}
uiUnits = code switch
{
0 => NAV.Units.Kilometers,
1 => NAV.Units.Meters,
2 => NAV.Units.Centimeters,
3 => NAV.Units.Millimeters,
4 => NAV.Units.Micrometers,
5 => NAV.Units.Miles,
6 => NAV.Units.Miles,
7 => NAV.Units.Yards,
8 => NAV.Units.Yards,
9 => NAV.Units.Feet,
10 => NAV.Units.Feet,
11 => NAV.Units.Feet,
12 => NAV.Units.Inches,
13 => NAV.Units.Inches,
14 => NAV.Units.Mils,
15 => NAV.Units.Microinches,
_ => NAV.Units.Meters
};
return code >= 0;
}
}
@@ -9,35 +9,39 @@
<Import_RootNamespace>Speckle.Converters.NavisworksShared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PathConstants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs" />
<Folder Include="$(MSBuildThisFileDirectory)Models\" />
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettingsFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\NavisworksRootToSpeckleConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoundingBoxToSpeckleRawConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\ModelItemTopLevelConverterToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)PathConstants.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\PropertyConversion.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\UIUnits.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\SharedGeometryStores.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\InstanceStoreManager.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs"/>
<Folder Include="$(MSBuildThisFileDirectory)Models\"/>
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettingsFactory.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\NavisworksRootToSpeckleConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoundingBoxToSpeckleRawConverter.cs"/>
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\ModelItemTopLevelConverterToSpeckle.cs"/>
</ItemGroup>
</Project>
</Project>
@@ -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,45 +1,47 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Autodesk.Navisworks.Api.Interop.ComApi;
using Speckle.Converter.Navisworks.Extensions;
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Constants;
using Speckle.Converter.Navisworks.Geometry;
using Speckle.Converter.Navisworks.Helpers;
using Speckle.Converter.Navisworks.Services;
using Speckle.Converter.Navisworks.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects.Geometry;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
namespace Speckle.Converter.Navisworks.ToSpeckle;
/// <summary>
/// Converts Navisworks geometry to Speckle displayable geometry.
///
/// Note: This class does not implement ITypedConverter{ModelGeometry, Base} because Navisworks geometry
/// conversion requires COM interop access that isn't available through the public ModelGeometry class.
/// The conversion process requires:
/// 1. Convert ModelItem to InwOaPath3 via ComApiBridge
/// 2. Use that to get InwOaFragmentList
/// 3. Process each InwOaFragment3 to generate primitives
/// 4. Convert those primitives to Speckle geometry with appropriate transforms
/// </summary>
public class GeometryToSpeckleConverter
/// <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,
ILogger<GeometryToSpeckleConverter> logger
)
{
private readonly NavisworksConversionSettings _settings;
private readonly bool _isUpright;
private readonly SafeVector _transformVector;
private const double SCALE = 1.0; // Default scale factor
private readonly NavisworksConversionSettings _settings =
settings ?? throw new ArgumentNullException(nameof(settings));
public GeometryToSpeckleConverter(NavisworksConversionSettings settings)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_isUpright = settings.Derived.IsUpright;
_transformVector = settings.Derived.TransformVector;
}
private readonly bool _isUpright = settings.Derived.IsUpright;
private readonly SafeVector _transformVector = settings.Derived.TransformVector;
private const double SCALE = 1.0;
private static readonly Matrix4x4 s_identityMatrix = new(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
private static readonly double[] s_identityTransform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
/// <summary>
/// Converts a ModelItem's geometry to Speckle display geometry by accessing the underlying COM objects.
/// Applies necessary transformations and unit scaling.
/// </summary>
internal List<Base> Convert(NAV.ModelItem modelItem)
private readonly InstanceStoreManager _instanceStoreManager =
instanceStoreManager ?? throw new ArgumentNullException(nameof(instanceStoreManager));
private readonly ILogger<GeometryToSpeckleConverter> _logger =
logger ?? throw new ArgumentNullException(nameof(logger));
internal List<SSM.Base> Convert(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
@@ -56,22 +58,42 @@ public class GeometryToSpeckleConverter
{
var fragmentStack = new Stack<InwOaFragment3>();
var paths = comSelection.Paths();
if (paths == null)
{
return [];
}
try
{
// Populate fragment stack with all fragments
if (paths.Count > 0)
{
var firstPath = paths.Cast<InwOaPath>().First();
var fragmentsCollection = firstPath.Fragments();
try
{
if (fragmentsCollection.Count > 1)
{
return ProcessSharedGeometry(paths, fragmentStack);
}
}
finally
{
if (fragmentsCollection != null)
{
Marshal.ReleaseComObject(fragmentsCollection);
}
}
}
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths);
return ProcessFragments(fragmentStack, paths, true);
}
finally
{
if (paths != null)
{
Marshal.ReleaseComObject(paths);
}
Marshal.ReleaseComObject(paths);
}
}
finally
@@ -86,24 +108,61 @@ public class GeometryToSpeckleConverter
private static void CollectFragments(InwOaPath path, Stack<InwOaFragment3> fragmentStack)
{
var fragments = path.Fragments();
foreach (var fragment in fragments.OfType<InwOaFragment3>())
try
{
if (fragment.path?.ArrayData is not Array pathData1 || path.ArrayData is not Array pathData2)
foreach (var fragment in fragments.OfType<InwOaFragment3>())
{
continue;
if (AreFragmentPathsEqual(fragment, path))
{
fragmentStack.Push(fragment);
}
}
var pathArray1 = pathData1.ToArray<int>();
var pathArray2 = pathData2.ToArray<int>();
if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2))
}
finally
{
if (fragments != null)
{
fragmentStack.Push(fragment);
Marshal.ReleaseComObject(fragments);
}
}
}
private List<Base> ProcessFragments(Stack<InwOaFragment3> fragmentStack, InwSelectionPathsColl paths)
private List<SSM.Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
{
var fragmentId = GenerateFragmentId(paths);
if (string.IsNullOrEmpty(fragmentId))
{
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths, true);
}
if (_instanceStoreManager.ContainsSharedGeometry(fragmentId))
{
return CreateInstanceReference(fragmentId, paths);
}
foreach (InwOaPath path in paths)
{
CollectFragments(path, fragmentStack);
}
var baseGeometry = ExtractUntransformedGeometry(fragmentStack);
return baseGeometry == null || !_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometry)
? ProcessFragments(fragmentStack, paths) // default false flag for isSingleObject
: CreateInstanceReference(fragmentId, paths);
}
private List<SSM.Base> ProcessFragments(
Stack<InwOaFragment3> fragmentStack,
InwSelectionPathsColl paths,
bool isSingleObject = false
)
{
var callbackListeners = new List<PrimitiveProcessor>();
@@ -113,11 +172,6 @@ public class GeometryToSpeckleConverter
foreach (var fragment in fragmentStack)
{
if (!ValidateFragmentPath(fragment, path))
{
continue;
}
var matrix = fragment.GetLocalToWorldMatrix();
var transform = matrix as InwLTransform3f3;
if (transform?.Matrix is not Array matrixArray)
@@ -125,31 +179,43 @@ public class GeometryToSpeckleConverter
continue;
}
processor.LocalToWorldTransformation = ConvertArrayToDouble(matrixArray);
var fragmentsForCount = path.Fragments();
int fragmentCount;
try
{
fragmentCount = fragmentsForCount?.Count ?? 0;
}
finally
{
if (fragmentsForCount != null)
{
Marshal.ReleaseComObject(fragmentsForCount);
}
}
double[] makeNoChange = s_identityTransform;
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
processor.LocalToWorldTransformation =
isSingleObject || fragmentCount == 1 ? transformMatrix : (IEnumerable<double>)makeNoChange;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
callbackListeners.Add(processor);
}
var baseGeometries = ProcessGeometries(callbackListeners);
return baseGeometries;
return ProcessGeometries(callbackListeners);
}
private static bool ValidateFragmentPath(InwOaFragment3 fragment, InwOaPath path)
{
if (fragment.path?.ArrayData is not Array fragmentPathData || path.ArrayData is not Array pathData)
{
return false;
}
private static bool AreFragmentPathsEqual(InwOaFragment3 fragment, InwOaPath path) =>
fragment.path?.ArrayData is Array fragmentPathData
&& path.ArrayData is Array pathData
&& AreFragmentPathsEqual(fragmentPathData, pathData);
return IsSameFragmentPath(fragmentPathData, pathData);
}
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
private List<SSM.Base> ProcessGeometries(List<PrimitiveProcessor> processors)
{
var baseGeometries = new List<Base>();
var baseGeometries = new List<SSM.Base>();
foreach (var processor in processors)
{
@@ -178,7 +244,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,
@@ -204,9 +269,8 @@ public class GeometryToSpeckleConverter
}
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines) =>
(
from line in lines
select new Line
lines
.Select(line => new Line
{
start = new Point(
(line.Start.X + _transformVector.X) * SCALE,
@@ -221,10 +285,312 @@ public class GeometryToSpeckleConverter
_settings.Derived.SpeckleUnits
),
units = _settings.Derived.SpeckleUnits
}
).ToList();
})
.ToList();
private static double[]? ConvertArrayToDouble(Array arr)
public string GenerateFragmentId(InwSelectionPathsColl paths)
{
try
{
if (paths.Count == 0)
{
return string.Empty;
}
var fragmentHashes = new List<string>();
foreach (var fragments in from InwOaPath path in paths select path.Fragments())
{
try
{
var fragmentIndex = 0;
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
{
if (fragment.path?.ArrayData is not Array pathData || pathData.Length == 0)
{
fragmentIndex++;
continue;
}
try
{
if (pathData.Rank != 1)
{
var fragmentHashFallback = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHashFallback))
{
fragmentHashes.Add(fragmentHashFallback);
}
fragmentIndex++;
continue;
}
var lowerBound = pathData.GetLowerBound(0);
var upperBound = pathData.GetUpperBound(0);
var arrayLength = upperBound - lowerBound + 1;
var pathInts = new int[arrayLength];
for (int i = lowerBound; i <= upperBound; i++)
{
try
{
var value = pathData.GetValue(i);
var arrayIndex = i - lowerBound;
pathInts[arrayIndex] = System.Convert.ToInt32(value);
}
catch (Exception ex) when (ex is COMException or InvalidCastException)
{
var errorType = ex is COMException ? "COM array access failed" : "Type conversion failed";
_logger.LogDebug(ex, "{ErrorType} at index {Index}", errorType, i);
}
}
var fragmentHash = string.Join("_", pathInts);
fragmentHashes.Add(fragmentHash);
}
catch (Exception ex) when (ex is COMException or IndexOutOfRangeException or RankException)
{
var errorType = ex switch
{
COMException => "COM access failed",
IndexOutOfRangeException => "Array bounds exceeded",
RankException => "Array rank mismatch",
_ => "Error"
};
_logger.LogDebug(
ex,
"{ErrorType} processing fragment {FragmentIndex}, trying simple enumeration",
errorType,
fragmentIndex
);
var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex);
if (!string.IsNullOrEmpty(fragmentHash))
{
fragmentHashes.Add(fragmentHash);
}
fragmentIndex++;
continue;
}
fragmentIndex++;
}
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
}
}
if (fragmentHashes.Count > 0)
{
fragmentHashes.Sort();
var rawData = string.Join("__", fragmentHashes);
var fragmentId = HashRawData(rawData);
return fragmentId;
}
return string.Empty;
}
catch (Exception ex) when (ex is COMException or InvalidCastException or IndexOutOfRangeException)
{
var errorType = ex switch
{
COMException => "COM access failed",
InvalidCastException => "Type conversion failed",
IndexOutOfRangeException => "Array bounds exceeded",
_ => "Error"
};
_logger.LogWarning(ex, "{ErrorType} generating fragment ID", errorType);
return string.Empty;
}
}
private string TrySimpleArrayEnumeration(Array pathData, int fragmentIndex)
{
try
{
var values = new List<string>();
var maxAttempts = Math.Min(pathData.Length, 20);
for (int i = 0; i < maxAttempts; i++)
{
try
{
var value = pathData.GetValue(i);
var convertedValue = System.Convert.ToInt32(value);
values.Add(convertedValue.ToString());
}
catch (IndexOutOfRangeException)
{
break;
}
catch (InvalidCastException ex)
{
_logger.LogDebug(ex, "Type conversion failed at index {Index}", i);
}
}
if (values.Count <= 0)
{
return string.Empty;
}
return string.Join("_", values);
}
catch (COMException ex)
{
_logger.LogDebug(ex, "COM enumeration failed for fragment {FragmentIndex}", fragmentIndex);
return string.Empty;
}
}
private static string HashRawData(string rawData)
{
using var sha256 = SHA256.Create();
var inputBytes = Encoding.UTF8.GetBytes(rawData);
var hashBytes = sha256.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
private SSM.Base? ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
{
var processor = new PrimitiveProcessor(_isUpright);
foreach (var fragment in fragmentStack)
{
processor.LocalToWorldTransformation = s_identityTransform;
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
}
return processor.Triangles.Count > 0 ? CreateMesh(processor.Triangles) : null;
}
private List<SSM.Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
{
var transform = ExtractInstanceTransform(paths);
var instanceReference = new InstanceProxy
{
definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}",
transform = transform,
units = _settings.Derived.SpeckleUnits,
maxDepth = 0,
applicationId = Guid.NewGuid().ToString()
};
return [instanceReference];
}
private Matrix4x4 ExtractInstanceTransform(InwSelectionPathsColl paths)
{
try
{
if (paths.Count == 0)
{
return s_identityMatrix;
}
var firstPath = paths.Cast<InwOaPath>().First();
var fragments = firstPath.Fragments();
try
{
if (fragments.Count == 0)
{
return s_identityMatrix;
}
var fragmentStack = new Stack<InwOaFragment3>();
foreach (var frag in fragments.OfType<InwOaFragment3>())
{
if (frag.path?.ArrayData is not Array pathData1 || firstPath.ArrayData is not Array pathData2)
{
continue;
}
var pathArray1 = pathData1.Cast<int>().ToArray();
var pathArray2 = pathData2.Cast<int>().ToArray();
if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2))
{
fragmentStack.Push(frag);
}
}
var fragment = fragmentStack.First();
var matrix = fragment.GetLocalToWorldMatrix();
if (matrix is InwLTransform3f3 { Matrix: Array matrixArray })
{
var transformArray = ConvertArrayToDouble(matrixArray);
var transformedMatrix = ApplyCoordinateTransform(transformArray);
var newMatrix = new Matrix4x4(
transformedMatrix[0],
transformedMatrix[1],
transformedMatrix[2],
transformedMatrix[3],
transformedMatrix[4],
transformedMatrix[5],
transformedMatrix[6],
transformedMatrix[7],
transformedMatrix[8],
transformedMatrix[9],
transformedMatrix[10],
transformedMatrix[11],
transformedMatrix[12],
transformedMatrix[13],
transformedMatrix[14],
transformedMatrix[15]
);
return Matrix4x4.Transpose(newMatrix);
}
}
finally
{
if (fragments != null)
{
Marshal.ReleaseComObject(fragments);
}
}
}
catch (Exception ex) when (ex is COMException or InvalidCastException or NullReferenceException)
{
var errorType = ex switch
{
COMException => "COM access failed",
InvalidCastException => "Transform matrix type conversion failed",
NullReferenceException => "Null reference",
_ => "Error"
};
_logger.LogWarning(ex, "{ErrorType} extracting instance transform", errorType);
}
return s_identityMatrix;
}
private double[] ApplyCoordinateTransform(double[] matrixArray)
{
var result = new double[16];
Array.Copy(matrixArray, result, 16);
result[12] = (result[12] + _transformVector.X) * SCALE;
result[13] = (result[13] + _transformVector.Y) * SCALE;
result[14] = (result[14] + _transformVector.Z) * SCALE;
return result;
}
private static double[] ConvertArrayToDouble(Array arr)
{
if (arr.Rank != 1)
{
@@ -240,6 +606,6 @@ public class GeometryToSpeckleConverter
return doubleArray;
}
private static bool IsSameFragmentPath(Array a1, Array a2) =>
private static bool AreFragmentPathsEqual(Array a1, Array a2) =>
a1.Length == a2.Length && a1.Cast<int>().SequenceEqual(a2.Cast<int>());
}
@@ -1,4 +1,4 @@
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.Settings;
using Speckle.Converter.Navisworks.ToSpeckle.PropertyHandlers;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
@@ -74,15 +74,18 @@ public sealed class DisplayValueExtractor
}
return areaDisplay;
// NOTE: this is only for Rebar and not AreaReinforcement, RebarInSystem
// AreaReinforcement and RebarInSystem pass through GetGeometryDisplayValue which get DisplayValues as per hostApp
// Rebar elements need special handling as get_Geometry() doesn't work properly
// We either represent them as centerlines or as solids based on settings
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView + apply reference point transform
case DB.Structure.Rebar rebar:
return _converterSettings.Current.SendRebarsAsVolumetric
? GetRebarVolumetricDisplayValue(rebar)
: GetRebarCenterlineDisplayValue(rebar);
// AreaReinforcement/PathReinforcement get_Geometry() returns curves in document coordinates
// unlike Rebar which needs reference point transform applied, these are already correct
case DB.Structure.AreaReinforcement:
case DB.Structure.PathReinforcement:
return GetAreaReinforcementDisplayValue(element);
// handle specific types of objects with multiple parts or children
// curtain and stacked walls should have their display values in their children
case DB.Wall wall:
@@ -107,7 +110,7 @@ public sealed class DisplayValueExtractor
using DB.Transform? compoundTransform =
localToDocument is not null && documentToWorld is not null
? documentToWorld.Multiply(localToDocument)
: localToDocument; // don't want to accidentally dispose of the ReferencePointTransform
: localToDocument;
DB.Transform? localToWorld = compoundTransform ?? documentToWorld;
@@ -423,7 +426,7 @@ public sealed class DisplayValueExtractor
return false; // exit fast on a potential hot path
}
DB.GraphicsStyle? bjk = null; // ask ogu why this variable is named like this
DB.GraphicsStyle? bjk; // ask ogu why this variable is named like this
if (!_graphicStyleCache.ContainsKey(geomObj.GraphicsStyleId.ToString().NotNull()))
{
@@ -547,8 +550,9 @@ public sealed class DisplayValueExtractor
if (geometryElements != null)
{
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
SortGeometry(rebar, collections, geometryElements, null);
return ProcessGeometryCollections(rebar, collections, null);
return ProcessGeometryCollections(rebar, collections, documentToWorld);
}
// Return empty list if no geometry is found - imo not critical
@@ -589,16 +593,40 @@ public sealed class DisplayValueExtractor
)
);
}
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
List<DisplayValueResult> displayValue = new();
foreach (var curve in curves)
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
if (documentToWorld is not null)
{
using var transformedCurve = curve.CreateTransformed(documentToWorld);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
}
return displayValue;
}
/// <summary>
/// Gets display value for AreaReinforcement and PathReinforcement.
/// </summary>
/// <remarks>
/// These elements' get_Geometry() returns curves already in document coordinates.
/// Unlike Rebar.GetTransformedCenterlineCurves() which requires reference point transform,
/// these curves should not be transformed - they're already in the correct space.
/// </remarks>
private List<DisplayValueResult> GetAreaReinforcementDisplayValue(DB.Element element)
{
var collections = GetSortedGeometryFromElement(element, null, null);
// pass null for transform - curves are already in correct document coordinates
return ProcessGeometryCollections(element, collections, null);
}
/// <summary>
/// Represents sorted collections of different geometry types extracted from an element.
/// Used to pass multiple geometry collections as a single parameter to improve code readability
@@ -81,8 +81,9 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\RevitElementTopLevelConverterToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ParameterValueExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\RevitContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\View3DTopLevelConverterToSpeckle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)\Helpers\SendSelection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\RevitToSpeckleUnitConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RevitRootToSpeckleConverter.cs" />
</ItemGroup>
</Project>
</Project>
@@ -88,6 +88,17 @@ public class ClassPropertiesExtractor
{
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
}
// get toRoom and fromRoom for FamilyInstance elements with those properties (e.g. Doors)
if (familyInstance.ToRoom is not null)
{
elementProperties.Add("toRoomId", familyInstance.ToRoom.Id.ToString());
}
if (familyInstance.FromRoom is not null)
{
elementProperties.Add("fromRoomId", familyInstance.FromRoom.Id.ToString());
}
}
catch (Exception e) when (!e.IsFatal())
{
@@ -0,0 +1,49 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Objects.Other;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
namespace Speckle.Converters.RevitShared.ToSpeckle;
[NameAndRankValue(typeof(DB.View3D), 0)]
public class View3DTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter, ITypedConverter<DB.View3D, Camera>
{
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
private readonly ITypedConverter<DB.XYZ, SOG.Vector> _xyzToVectorConverter;
public View3DTopLevelConverterToSpeckle(
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
ITypedConverter<DB.XYZ, SOG.Vector> xyzToVectorConverter
)
{
_xyzToPointConverter = xyzToPointConverter;
_xyzToVectorConverter = xyzToVectorConverter;
}
public Base Convert(object target) => Convert((DB.View3D)target);
public Camera Convert(DB.View3D target)
{
if (!target.IsPerspective)
{
throw new ConversionException("Non-Perspective views are not supported");
}
// some views have null origin, not sure why
if (target.Origin == null)
{
throw new ConversionException("Views with no origin are not supported");
}
DB.ViewOrientation3D orientation = target.GetSavedOrientation();
return new()
{
name = target.Title,
position = _xyzToPointConverter.Convert(target.Origin),
forward = _xyzToVectorConverter.Convert(orientation.ForwardDirection),
up = _xyzToVectorConverter.Convert(orientation.UpDirection)
};
}
}
@@ -7,11 +7,11 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" />
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.9.24194.18121"/>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.21.25188.17001"/>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
@@ -346,9 +346,9 @@
},
"RhinoCommon": {
"type": "Direct",
"requested": "[8.21.25188.17001, )",
"resolved": "8.21.25188.17001",
"contentHash": "Wo6JXheyDBvilyIwDY6xZCQJC4170jzAlTSlMgh8yokUY+vYyCl4KJVXZofIOynNt/xx5wLqb2On5gZZekXR6w==",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "PDKR9GwqyUXUkTulV4J0dzDIf/aWqSJkL7nkS8ReAx8xhnt/+RQpE8gTjOSCmkSU2tjG6WzclowbTxwMTU7VAA==",
"dependencies": {
"System.Drawing.Common": "7.0.0"
}
@@ -2,4 +2,5 @@ global using RG = Rhino.Geometry;
global using SA = Speckle.Objects.Annotation;
global using SO = Speckle.Objects.Other;
global using SOG = Speckle.Objects.Geometry;
global using SOO = Speckle.Objects.Other;
global using SOP = Speckle.Objects.Primitive;
@@ -0,0 +1,41 @@
using Rhino.DocObjects;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel;
[NameAndRankValue(typeof(ViewInfo), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
public class ViewInfoToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter, ITypedConverter<ViewInfo, SOO.Camera>
{
private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
private readonly ITypedConverter<RG.Vector3d, SOG.Vector> _vectorConverter;
public ViewInfoToSpeckleTopLevelConverter(
ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
ITypedConverter<RG.Vector3d, SOG.Vector> vectorConverter
)
{
_pointConverter = pointConverter;
_vectorConverter = vectorConverter;
}
public Base Convert(object target) => Convert((ViewInfo)target);
public SOO.Camera Convert(ViewInfo target)
{
if (target.Viewport.IsParallelProjection)
{
throw new ConversionException("Parallel projection views are not supported.");
}
return new()
{
name = target.Name,
position = _pointConverter.Convert(target.Viewport.CameraLocation),
up = _vectorConverter.Convert(target.Viewport.CameraY),
forward = _vectorConverter.Convert(target.Viewport.CameraZ),
};
}
}
@@ -1,5 +1,7 @@
using Speckle.Connectors.DUI.Bridge;
using Speckle.Newtonsoft.Json;
using Speckle.Sdk.Credentials;
using Speckle.Sdk.SQLite;
namespace Speckle.Connectors.DUI.Bindings;
@@ -9,14 +11,25 @@ public class AccountBinding : IBinding
public IBrowserBridge Parent { get; }
private readonly IAccountManager _accountManager;
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
public AccountBinding(IBrowserBridge bridge, IAccountManager accountManager)
public AccountBinding(
IBrowserBridge bridge,
IAccountManager accountManager,
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory
)
{
Parent = bridge;
_accountManager = accountManager;
_jsonCacheManager = sqLiteJsonCacheManagerFactory.CreateForUser("Accounts");
}
public Account[] GetAccounts() => _accountManager.GetAccounts().ToArray();
public void AddAccount(string accountId, Account account)
{
_jsonCacheManager.SaveObject(accountId, JsonConvert.SerializeObject(account));
}
public void RemoveAccount(string accountId) => _accountManager.RemoveAccount(accountId);
}
+2 -2
View File
@@ -28,10 +28,10 @@
<PackageVersion Include="NUnit.Analyzers" Version="4.2.0" />
<PackageVersion Include="NUnit3TestAdapter" version="4.6.0" />
<PackageVersion Include="Revit.Async" Version="2.1.1" />
<PackageVersion Include="RhinoCommon" Version="8.9.24194.18121" />
<PackageVersion Include="RhinoCommon" Version="8.25.25328.11001" />
<PackageVersion Include="Rhino.Inside" Version="8.0.7-beta" />
<PackageVersion Include="Grasshopper" Version="8.9.24194.18121" />
<PackageVersion Include="RhinoWindows" Version="8.9.24194.18121" />
<PackageVersion Include="RhinoWindows" Version="8.25.25328.11001" />
<PackageVersion Include="Semver" Version="3.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageVersion Include="Speckle.CSI.API" Version="2.4.0" />
@@ -8,6 +8,7 @@ using Speckle.Connectors.Logging;
using Speckle.Importers.JobProcessor.Domain;
using Speckle.Importers.JobProcessor.JobHandlers;
using Speckle.Importers.JobProcessor.JobQueue;
using Speckle.Sdk;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Common;
@@ -29,6 +30,24 @@ internal sealed class JobProcessorInstance(
private static readonly TimeSpan s_idleTimeout = TimeSpan.FromSeconds(1);
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
try
{
await RunJobProcessorLoop(cancellationToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "Job Processor crashed");
Environment.Exit(1); //This is the only reliable way I've managed to figure out how to get windows services retry policy to actually kick in (see https://github.com/dotnet/runtime/issues/67146)
throw;
}
}
private async Task RunJobProcessorLoop(CancellationToken cancellationToken)
{
await using var connection = await repository.SetupConnection(cancellationToken).ConfigureAwait(false);
@@ -180,6 +199,11 @@ internal sealed class JobProcessorInstance(
{
logger.LogError(new AggregateException(ex, ex2), "Failed to report failure status");
await repository.ReturnJobToQueued(connection, job.Id, cancellationToken);
if (ex2.IsFatal())
{
throw;
}
}
}
finally
@@ -10,27 +10,14 @@ namespace Speckle.Importers.JobProcessor;
public static class Program
{
public static async Task<int> Main(string[] args)
public static async Task Main(string[] args)
{
// Dapper doesn't understand how to handle JSON deserialization, so we need to tell it what types can be deserialzied
SqlMapper.AddTypeHandler(new JsonHandler<FileimportPayload>());
var host = ConfigureAppHost(args);
var backgroundServiceTasks = host
.Services.GetServices<IHostedService>()
.OfType<BackgroundService>()
.Select(s => s.ExecuteTask);
await host.RunAsync();
if (backgroundServiceTasks.Any(t => t?.IsFaulted == true))
{
//https://github.com/dotnet/runtime/issues/67146
return -1;
}
return 0;
}
private static IHost ConfigureAppHost(string[] args)
@@ -50,15 +37,4 @@ public static class Program
return builder.Build();
}
private static void ConfigureTopLevelLogs(ILogger logger)
{
TaskScheduler.UnobservedTaskException += (_, eventArgs) =>
logger.LogCritical(eventArgs.Exception, "Unobserved Task Exception");
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
logger.LogCritical(eventArgs.ExceptionObject as Exception, "Unhandled exception occurred in the AppDomain");
};
}
}
@@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.24.25281.15001"/>
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.24.25281.15001"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.24.25281.15001"/>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.25.25328.11001"/>
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.25.25328.11001"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.25.25328.11001"/>
<PackageReference Include="Rhino.Inside" />
</ItemGroup>
@@ -4,11 +4,11 @@
"net8.0-windows7.0": {
"Grasshopper": {
"type": "Direct",
"requested": "[8.24.25281.15001, )",
"resolved": "8.24.25281.15001",
"contentHash": "TEtB8nElTvhMJctLhv8UC1v6jscYdTgsoRQIr31ewGZr6cpgGtQTBmUk26/9ZvQxXCgOp7Y4EZdcEZkmqCm1SQ==",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "1uFL9pmgCEbYFd1b3JFGHaLEjQuRgFsiRHmP5u73mdEMdO6+5F/pNV1dQZr3YGLDovUmgrc+jmpsme7wr/J01A==",
"dependencies": {
"RhinoCommon": "[8.24.25281.15001]"
"RhinoCommon": "[8.25.25328.11001]"
}
},
"Microsoft.NETFramework.ReferenceAssemblies": {
@@ -48,20 +48,20 @@
},
"RhinoCommon": {
"type": "Direct",
"requested": "[8.24.25281.15001, )",
"resolved": "8.24.25281.15001",
"contentHash": "K8dd7DJvEUUYHpwkyyxr/ojK3e8swlE0STeyG+ulVWkWNHK6gIRDxMYCwB7kNyHHMgpr/vpQlMgR3SVD1GoMTA==",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "PDKR9GwqyUXUkTulV4J0dzDIf/aWqSJkL7nkS8ReAx8xhnt/+RQpE8gTjOSCmkSU2tjG6WzclowbTxwMTU7VAA==",
"dependencies": {
"System.Drawing.Common": "7.0.0"
}
},
"RhinoWindows": {
"type": "Direct",
"requested": "[8.24.25281.15001, )",
"resolved": "8.24.25281.15001",
"contentHash": "JbG98P80Hskpomzx1xhqFoz2WmVnhjda0noQyZE+dE678ZKmw+O3i/iIjaN5jydXvTu/fZXRzQmEDL7M7Ura8g==",
"requested": "[8.25.25328.11001, )",
"resolved": "8.25.25328.11001",
"contentHash": "I/+++piwtYTue+iAAQqcMF5QlontqwNnC7Leyhiv2FiF8JpAl6K44ZsJqB7ZEUC6ns0LDfa3mbFzQwUfHwYumQ==",
"dependencies": {
"RhinoCommon": "[8.24.25281.15001]"
"RhinoCommon": "[8.25.25328.11001]"
}
},
"Speckle.InterfaceGenerator": {
@@ -4,4 +4,5 @@ public static class RootKeys
{
public const string ANALYSIS_RESULTS = "analysisResults";
public const string REFERENCE_POINT_TRANSFORM = "referencePointTransform";
public const string VIEW = "views";
}