Compare commits

...

14 Commits

Author SHA1 Message Date
Mucahit Bilal GOKER d4ee1f2a55 feat(revit): add roomId and spaceId parameters (#1181)
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
* add roomId and spaceId parameters

* clean up
2025-11-11 10:30:27 +03:00
Jedd Morgan 4f960cc670 readme tweaks (#1179) 2025-11-10 13:52:43 +00:00
Jedd Morgan 1f63c1f8b3 Merge pull request #1165 from specklesystems/main
chore: Back merge main -> Dev
2025-11-10 13:48:30 +00:00
Jedd Morgan 2ed9ffbca7 Use Openheadless for unit support (#1178) 2025-11-10 12:53:55 +00:00
Jedd Morgan d87b862e2b add readme (#1173) 2025-11-10 12:49:39 +00:00
Mucahit Bilal GOKER 3ad3ad2f01 feat(rhino): use existing materials when loading (#1177)
* reuse existing materials

* comment out purge material

* fix: unnecessary using directives

* more details in exception message and remove material purge.

---------

Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
2025-11-10 15:43:04 +03:00
Björn Steinhagen 6db7e46401 fix(grasshopper): refresh parameter UI when toggling empty properties mode (#1174) 2025-11-10 15:07:57 +03:00
Björn Steinhagen 13fc24c7c7 fix(rhino): handles null active doc (#1180) 2025-11-10 14:51:06 +03:00
Jedd Morgan cf86158b83 Ignore (#1171) 2025-11-03 12:57:17 +00:00
Björn Steinhagen 25eb955636 feat(etabs): add volume calculation for frames and shells (#1167)
* feat: adds frame volume prop

* feat: adds shell volume prop

* refactor: simplify caching

* fix(etabs): use IsInfinity for etabs 21
2025-10-28 17:39:30 +02:00
dependabot[bot] 7862a858ae chore(deps): bump actions/upload-artifact from 4 to 5 (#1166)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 18:09:02 +00:00
Jedd Morgan bc18d3b494 fix(rhino-importer): Use main thread always for document creation (#1161)
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
* Use main thread

* format

* configure await false

* pass args only once
2025-10-27 11:11:56 +00:00
Björn Steinhagen fd34f22028 fix(grasshopper): applicationId for SpeckleDataObject on creation (#1159)
* chore: formatting

* fix: generate appId on object creation

* feat: validate uniqueness of objects within a collection
2025-10-25 20:46:20 +01:00
Jedd Morgan 958c9e5e94 Merge pull request #1158 from specklesystems/main
Main -> Dev back merge
2025-10-20 15:42:23 +01:00
37 changed files with 577 additions and 328 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
run: ./build.ps1 zip
- name: ⬆️ Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: output-${{ env.SEMVER }}
path: output/*.*
@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging;
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.Utils;
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
using Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
@@ -11,54 +10,55 @@ namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
/// </summary>
public class EtabsShellSectionPropertyExtractor : IApplicationShellSectionPropertyExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly ILogger<EtabsShellSectionPropertyExtractor> _logger;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
private readonly EtabsShellSectionResolver _etabsShellSectionResolver;
public EtabsShellSectionPropertyExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
ILogger<EtabsShellSectionPropertyExtractor> logger,
EtabsShellSectionResolver etabsShellSectionResolver
EtabsShellSectionResolver etabsShellSectionResolver,
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton
)
{
_settingsStore = settingsStore;
_logger = logger;
_etabsShellSectionResolver = etabsShellSectionResolver;
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
}
/// <summary>
/// Extract shell section properties
/// Extract shell section properties from cache.
/// </summary>
/// <remarks>
/// sectionName is unique across all types (Wall, Slab and Deck)
/// There is no general query such as PropArea.GetShell() - rather we have to be specific on the type, for example
/// PropArea.GetWall() or PropArea.GetDeck() BUT we can't get the building type given a SectionName.
/// Hence the introduction of ResolveSection.
/// By the time this method is called during section unpacking, all sections should already be
/// resolved and cached by <see cref="EtabsShellPropertiesExtractor"/> during object conversion.
/// </remarks>
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties)
{
// Step 01: Finding the appropriate api query for the unknown section type (wall, deck or slab)
Dictionary<string, object?> resolvedProperties = _etabsShellSectionResolver.ResolveSection(sectionName);
var sectionProps = GetSectionProperties(sectionName);
// Step 02: Mutate properties dictionary with resolved properties
foreach (var nestedDictionary in resolvedProperties)
// shallow copy nested dictionaries into provided properties dict to mutate it (required by interface contract)
foreach (var kvp in sectionProps)
{
if (nestedDictionary.Value is not Dictionary<string, object?> nestedValues)
{
_logger.LogWarning(
"Unexpected value type for key {Key} in section {SectionName}. Expected Dictionary<string, object?>, got {ActualType}",
nestedDictionary.Key,
sectionName,
nestedDictionary.Value?.GetType().Name ?? "null"
);
continue;
}
var nestedProperties = properties.EnsureNested(nestedDictionary.Key);
foreach (var kvp in nestedValues)
{
nestedProperties[kvp.Key] = kvp.Value;
}
properties[kvp.Key] = kvp.Value;
}
}
private Dictionary<string, object?> GetSectionProperties(string sectionName)
{
// return cached properties directly
if (_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache.TryGetValue(sectionName, out var cachedProperties))
{
return cachedProperties;
}
// fallback - shouldn't happen because cached populated on the fly as sections appear in the extractor
_logger.LogWarning(
"Section {SectionName} not in cache during unpacking - resolving via API (expensive)",
sectionName
);
var resolved = _etabsShellSectionResolver.ResolveSection(sectionName);
_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache[sectionName] = resolved;
return resolved;
}
}
@@ -26,7 +26,6 @@ public static class ServiceRegistration
services.AddScoped<IApplicationShellSectionPropertyExtractor, EtabsShellSectionPropertyExtractor>();
services.AddScoped<EtabsSectionPropertyDefinitionService>();
services.AddScoped<EtabsSectionPropertyExtractor>();
services.AddScoped<EtabsShellSectionResolver>();
return services;
}
@@ -17,7 +17,6 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyDefinitionService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionResolver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\EtabsPluginBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\EtabsSpeckleFormBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ServiceRegistration.cs" />
@@ -64,6 +64,13 @@ public class CreateCollection : VariableParameterComponentBase
}
}
// validate for duplicate application IDs across the entire collection hierarchy
if (HasDuplicateApplicationIds(rootCollection))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
return; // error already added in validation method
}
dataAccess.SetData(0, new SpeckleCollectionWrapperGoo(rootCollection));
}
@@ -182,6 +189,54 @@ public class CreateCollection : VariableParameterComponentBase
}
}
/// <summary>
/// Validates that all application IDs are unique across the entire collection hierarchy.
/// Shows an error if duplicates are found, indicating objects appear in multiple collections.
/// </summary>
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
private bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
{
// args to CheckForDuplicateApplicationIds passed in since the method can recursively check
var seenIds = new HashSet<string>();
var duplicateIds = new HashSet<string>();
// iterate, create hash set and check all application IDs
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
return duplicateIds.Count > 0;
}
/// <summary>
/// Recursively collects application IDs from all in the collection hierarchy.
/// </summary>
/// <remarks>
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
/// </remarks>
private void ProcessAndCheckForDuplicateApplicationIds(
SpeckleCollectionWrapper collection,
HashSet<string> seenIds,
HashSet<string> duplicateIds
)
{
foreach (var element in collection.Elements)
{
switch (element)
{
case SpeckleCollectionWrapper childCollection:
// recurse into child collections
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
break;
case SpeckleWrapper wrapper:
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
{
duplicateIds.Add(wrapper.ApplicationId);
}
break;
}
}
}
// IGH_VariableParameterComponent implementation
public override bool CanInsertParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input;
@@ -162,6 +162,7 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
Params.RegisterInputParam(param);
}
Params.OnParametersChanged();
ExpireSolution(true);
}
@@ -103,7 +103,7 @@ public class SpeckleDataObjectPassthrough()
List<SpeckleGeometryWrapperGoo> inputGeometry = new();
if (!da.GetDataList(1, inputGeometry) && result == null)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Pass in a Speckle DataObject or Geometries.");
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a Speckle DataObject or Geometries");
return;
}
@@ -111,7 +111,7 @@ public class SpeckleDataObjectPassthrough()
{
if (inputGeo.Value is SpeckleBlockInstanceWrapper)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"DataObjects cannot contain Block Instances.");
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "DataObjects cannot contain Block Instances");
return;
}
}
@@ -158,6 +158,10 @@ public class SpeckleDataObjectPassthrough()
result.Properties = inputProperties;
}
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
result.ApplicationId ??= Guid.NewGuid().ToString();
// get the path
string? path =
result.Path.Count > 1 ? string.Join(Constants.LAYER_PATH_DELIMITER, result.Path) : result.Path.FirstOrDefault();
@@ -138,7 +138,7 @@ public class SpeckleGeometryPassthrough()
if (result == null && inputGeometry == null)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Pass in a Speckle Geometry or Geometry.");
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a Speckle Geometry or Geometry");
return;
}
@@ -83,7 +83,7 @@ public sealed class RhinoSendBinding : ISendBinding
_sendOperationManagerFactory = sendOperationManagerFactory;
_rhinoLayerHelper = rhinoLayerHelper;
Commands = new SendBindingUICommands(parent); // POC: Commands are tightly coupled with their bindings, at least for now, saves us injecting a factory.
PreviousUnitSystem = RhinoDoc.ActiveDoc.ModelUnitSystem;
PreviousUnitSystem = RhinoDoc.ActiveDoc?.ModelUnitSystem ?? UnitSystem.None;
SubscribeToRhinoEvents();
}
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;
using Rhino;
using Speckle.Converters.Common;
using Speckle.Converters.Rhino;
using Speckle.Objects.Other;
@@ -44,35 +43,43 @@ public class RhinoMaterialBaker
string materialId = speckleRenderMaterial.applicationId ?? speckleRenderMaterial.id.NotNull();
string matName = $"{speckleRenderMaterial.name}-({materialId})-{baseLayerName}";
matName = matName.Replace("[", "").Replace("]", ""); // "Material" doesn't like square brackets if we create from here. Once they created from Rhino UI, all good..
Color diffuse = Color.FromArgb(speckleRenderMaterial.diffuse);
Color emissive = Color.FromArgb(speckleRenderMaterial.emissive);
double transparency = 1 - speckleRenderMaterial.opacity;
Material rhinoMaterial =
new()
{
Name = matName,
DiffuseColor = diffuse,
EmissionColor = emissive,
Transparency = transparency
};
// Check if material with this name already exists in the document
int matIndex = doc.Materials.Find(matName, ignoreDeletedMaterials: true);
// try to get additional properties
if (speckleRenderMaterial["ior"] is double ior)
{
rhinoMaterial.IndexOfRefraction = ior;
}
if (speckleRenderMaterial["shine"] is double shine)
{
rhinoMaterial.Shine = shine;
}
int matIndex = doc.Materials.Add(rhinoMaterial);
// POC: check on matIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
// If material doesn't exist, create it
if (matIndex == -1)
{
throw new ConversionException("Failed to add a material to the document.");
Color diffuse = Color.FromArgb(speckleRenderMaterial.diffuse);
Color emissive = Color.FromArgb(speckleRenderMaterial.emissive);
double transparency = 1 - speckleRenderMaterial.opacity;
Material rhinoMaterial =
new()
{
Name = matName,
DiffuseColor = diffuse,
EmissionColor = emissive,
Transparency = transparency
};
// try to get additional properties
if (speckleRenderMaterial["ior"] is double ior)
{
rhinoMaterial.IndexOfRefraction = ior;
}
if (speckleRenderMaterial["shine"] is double shine)
{
rhinoMaterial.Shine = shine;
}
matIndex = doc.Materials.Add(rhinoMaterial);
// POC: check on matIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
if (matIndex == -1)
{
throw new ConversionException($"Failed to add a material to the document: '{matName}' (ID: {materialId})");
}
}
// Create the object <> material index map
@@ -87,27 +94,4 @@ public class RhinoMaterialBaker
}
}
}
/// <summary>
/// Removes all materials with a name starting with <paramref name="namePrefix"/> from the active document
/// </summary>
/// <param name="namePrefix"></param>
public void PurgeMaterials(string namePrefix)
{
var currentDoc = RhinoDoc.ActiveDoc; // POC: too much right now to interface around
foreach (Material material in currentDoc.Materials)
{
try
{
if (!material.IsDeleted && material.Name != null && material.Name.Contains(namePrefix))
{
currentDoc.Materials.Delete(material);
}
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to purge a material from the document");
}
}
}
}
@@ -256,7 +256,8 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
.RunOnMain(() =>
{
_instanceBaker.PurgeInstances(baseLayerName);
_materialBaker.PurgeMaterials(baseLayerName);
// Materials are now reused across receives instead of being purged
// _materialBaker.PurgeMaterials(baseLayerName);
var doc = _converterSettings.Current.Document;
// Cleans up any previously received objects
@@ -8,25 +8,11 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers;
/// Extracts properties common to frame elements across CSi products (e.g., Etabs, Sap2000)
/// using the FrameObj API calls.
/// </summary>
/// <remarks>
/// Design Decisions:
/// <list type="bullet">
/// <item>
/// <description>
/// Individual methods preferred over batched calls due to:
/// <list type="bullet">
/// <item><description>Independent API calls with no performance gain from batching (?)</description></item>
/// <item><description>Easier debugging and error tracing</description></item>
/// <item><description>Simpler maintenance as each method maps to one API concept</description></item>
/// </list>
/// </description>
/// </item>
/// </list>
/// </remarks>
public sealed class CsiFramePropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
private readonly DatabaseTableExtractor _databaseTableExtractor;
private static readonly string[] s_releaseKeys =
[
@@ -36,15 +22,17 @@ public sealed class CsiFramePropertiesExtractor
"Torsion",
"Moment 22 (Minor)",
"Moment 33 (Major)"
]; // Note: caching keys for better performance
];
public CsiFramePropertiesExtractor(
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton,
IConverterSettingsStore<CsiConversionSettings> settingsStore
IConverterSettingsStore<CsiConversionSettings> settingsStore,
DatabaseTableExtractor databaseTableExtractor
)
{
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
_settingsStore = settingsStore;
_databaseTableExtractor = databaseTableExtractor;
}
public void ExtractProperties(CsiFrameWrapper frame, PropertyExtractionResult frameData)
@@ -61,13 +49,29 @@ public sealed class CsiFramePropertiesExtractor
assignments[CommonObjectProperty.PROPERTY_MODIFIERS] = GetModifiers(frame);
assignments["End Releases"] = GetReleases(frame);
// NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc.
// NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc. Strings are unique
// Assign sectionId to variable as this will be an argument for the GetMaterialName method
string sectionId = GetSectionName(frame);
string materialId = GetMaterialName(sectionId);
assignments[ObjectPropertyKey.SECTION_ID] = sectionId;
assignments[ObjectPropertyKey.MATERIAL_ID] = materialId;
// CNX-2725 adds more numeric props for dashboard-ing
double length = GetLength(frame);
double area = GetCrossSectionalArea(sectionId);
double volume = double.NaN;
if (!double.IsNaN(length) && !double.IsNaN(area) && length > 0 && area > 0)
{
// I am paranoid about what etabs could throw our way
double computedVolume = length * area;
volume = (!double.IsInfinity(computedVolume) && !double.IsNaN(computedVolume)) ? computedVolume : double.NaN;
}
geometry.AddWithUnits(ObjectPropertyKey.LENGTH, length, _settingsStore.Current.SpeckleUnits);
geometry.AddWithUnits(ObjectPropertyKey.CROSS_SECTIONAL_AREA, area, $"{_settingsStore.Current.SpeckleUnits}²");
geometry.AddWithUnits(ObjectPropertyKey.VOLUME, volume, $"{_settingsStore.Current.SpeckleUnits}³");
// store the object, section, and material id relationships in their corresponding caches to be accessed by the connector
if (!string.IsNullOrEmpty(sectionId))
{
@@ -196,4 +200,56 @@ public sealed class CsiFramePropertiesExtractor
_ = _settingsStore.Current.SapModel.PropFrame.GetMaterial(sectionName, ref materialName);
return materialName;
}
private double GetLength(CsiFrameWrapper frame)
{
// using the DatabaseTableExtractor fetch table with key "Frame Assignments - Summary"
// limit query size to "UniqueName" and "Length" fields
string length = _databaseTableExtractor
.GetTableData("Frame Assignments - Summary", requestedColumns: ["UniqueName", ObjectPropertyKey.LENGTH])
.GetRowValue(frame.Name, ObjectPropertyKey.LENGTH);
// all database data is returned as strings
return double.TryParse(length, out double result) ? result : double.NaN;
}
private double GetCrossSectionalArea(string sectionName)
{
if (_csiToSpeckleCacheSingleton.FrameSectionAreaCache.TryGetValue(sectionName, out double value))
{
return value;
}
double area = 0,
as2 = 0,
as3 = 0,
torsion = 0,
i22 = 0,
i33 = 0,
s22 = 0,
s33 = 0,
z22 = 0,
z33 = 0,
r22 = 0,
r33 = 0;
int result = _settingsStore.Current.SapModel.PropFrame.GetSectProps(
sectionName,
ref area,
ref as2,
ref as3,
ref torsion,
ref i22,
ref i33,
ref s22,
ref s33,
ref z22,
ref z33,
ref r22,
ref r33
);
double validatedArea = result == 0 ? area : double.NaN;
_csiToSpeckleCacheSingleton.FrameSectionAreaCache.Add(sectionName, validatedArea);
return validatedArea;
}
}
@@ -8,28 +8,18 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers;
/// Extracts properties common to shell elements across CSi products (e.g., Etabs, Sap2000)
/// using the AreaObj API calls.
/// </summary>
/// <remarks>
/// Design Decisions:
/// <list type="bullet">
/// <item>
/// <description>
/// Individual methods preferred over batched calls due to:
/// <list type="bullet">
/// <item><description>Independent API calls with no performance gain from batching (?)</description></item>
/// <item><description>Easier debugging and error tracing</description></item>
/// <item><description>Simpler maintenance as each method maps to one API concept</description></item>
/// </list>
/// </description>
/// </item>
/// </list>
/// </remarks>
public sealed class CsiShellPropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
public CsiShellPropertiesExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
public CsiShellPropertiesExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton
)
{
_settingsStore = settingsStore;
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
}
public void ExtractProperties(CsiShellWrapper shell, PropertyExtractionResult shellData)
@@ -37,7 +27,7 @@ public sealed class CsiShellPropertiesExtractor
shellData.ApplicationId = shell.GetSpeckleApplicationId(_settingsStore.Current.SapModel);
var geometry = shellData.Properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
geometry["Joints"] = GetPointNames(shell); // TODO: 🪲 Viewer shows 4 but only displays 3
geometry["Joints"] = GetPointNames(shell);
var assignments = shellData.Properties.EnsureNested(ObjectPropertyCategory.ASSIGNMENTS);
assignments[CommonObjectProperty.GROUPS] = GetGroupAssigns(shell);
@@ -16,4 +16,16 @@ public class CsiToSpeckleCacheSingleton
/// A map of (section id, shell object id). Assumes the section id is the unique name of the section
/// </summary>
public Dictionary<string, List<string>> ShellSectionCache { get; set; } = [];
/// <summary>
/// A cache of cross-sectional areas used
/// </summary>
public Dictionary<string, double> FrameSectionAreaCache { get; set; } = [];
/// <summary>
/// A cache of resolved shell section properties populated by "EtabsShellPropertiesExtractor"
/// and consumed by "EtabsShellSectionPropertyExtractor".
/// This eliminates redundant section resolution API calls.
/// </summary>
public Dictionary<string, Dictionary<string, object?>> ShellSectionPropertiesCache { get; set; } = [];
}
@@ -39,7 +39,7 @@ public abstract class CsiObjectToSpeckleConverterBase : IToSpeckleTopLevelConver
public Base Convert(object target) => Convert((CsiWrapperBase)target);
public Base Convert(CsiWrapperBase wrapper)
private Base Convert(CsiWrapperBase wrapper)
{
var displayValue = _displayValueExtractor.GetDisplayValue(wrapper).ToList();
var objectData = _applicationPropertiesExtractor.ExtractProperties(wrapper);
@@ -21,8 +21,14 @@ public static class ObjectPropertyCategory
/// </summary>
public static class ObjectPropertyKey
{
public const string AREA = "Area";
public const string CROSS_SECTIONAL_AREA = "Cross-Sectional Area";
public const string DESIGN_PROCEDURE = "Design Procedure";
public const string LENGTH = "Length";
public const string MATERIAL_ID = "Material";
public const string SECTION_ID = "Section Property";
public const string THICKNESS = "Thickness";
public const string VOLUME = "Volume";
}
/// <summary>
@@ -20,6 +20,7 @@ public static class ServiceRegistration
serviceCollection.AddScoped<EtabsShellPropertiesExtractor>();
serviceCollection.AddScoped<IApplicationPropertiesExtractor, EtabsPropertiesExtractor>();
serviceCollection.AddScoped<CsiObjectToSpeckleConverterBase, EtabsObjectToSpeckleConverter>();
serviceCollection.AddScoped<EtabsShellSectionResolver>();
serviceCollection.AddMatchingInterfacesAsTransient(converterAssembly);
@@ -14,6 +14,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsFramePropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsJointPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellSectionResolver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\EtabsObjectToSpeckleConverter.cs" />
</ItemGroup>
</Project>
@@ -1,6 +1,5 @@
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
using Speckle.Converters.CSiShared.Utils;
namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
@@ -8,32 +7,13 @@ namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
/// <summary>
/// Extracts ETABS-specific properties from frame elements using the FrameObj API calls.
/// </summary>
/// <remarks>
/// Responsibilities:
/// <list type="bullet">
/// <item><description>Extracts properties only available in ETABS (e.g., Label, Level)</description></item>
/// <item><description>Complements <see cref="CsiFramePropertiesExtractor"/> by adding product-specific data</description></item>
/// <item><description>Follows same pattern of single-purpose methods for clear API mapping</description></item>
/// </list>
///
/// Design Decisions:
/// <list type="bullet">
/// <item><description>Maintains separate methods for each property following CSI API structure</description></item>
/// <item><description>Properties are organized by their functional groups (Object ID, Assignments, Design)</description></item>
/// </list>
/// </remarks>
public sealed class EtabsFramePropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly DatabaseTableExtractor _databaseTableExtractor;
public EtabsFramePropertiesExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
DatabaseTableExtractor databaseTableExtractor
)
public EtabsFramePropertiesExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
{
_settingsStore = settingsStore;
_databaseTableExtractor = databaseTableExtractor;
}
public void ExtractProperties(CsiFrameWrapper frame, Dictionary<string, object?> properties)
@@ -46,11 +26,7 @@ public sealed class EtabsFramePropertiesExtractor
assignments[CommonObjectProperty.SPRING_ASSIGNMENT] = GetSpringAssignmentName(frame);
var design = properties.EnsureNested(ObjectPropertyCategory.DESIGN);
design["Design Procedure"] = GetDesignProcedure(frame);
var geometry = properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
double length = GetLength(frame);
geometry.AddWithUnits("Length", length, _settingsStore.Current.SpeckleUnits);
design[ObjectPropertyKey.DESIGN_PROCEDURE] = GetDesignProcedure(frame);
}
private (string label, string level) GetLabelAndLevel(CsiFrameWrapper frame)
@@ -90,16 +66,4 @@ public sealed class EtabsFramePropertiesExtractor
_ = _settingsStore.Current.SapModel.FrameObj.GetSpringAssignment(frame.Name, ref springPropertyName);
return springPropertyName;
}
private double GetLength(CsiFrameWrapper frame)
{
// using the DatabaseTableExtractor fetch table with key "Frame Assignments - Summary"
// limit query size to "UniqueName" and "Length" fields
string length = _databaseTableExtractor
.GetTableData("Frame Assignments - Summary", requestedColumns: ["UniqueName", "Length"])
.GetRowValue(frame.Name, "Length");
// all database data is returned as strings
return double.TryParse(length, out double result) ? result : double.NaN;
}
}
@@ -9,35 +9,24 @@ namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
/// <summary>
/// Extracts ETABS-specific properties from shell elements using the AreaObj API calls.
/// </summary>
/// <remarks>
/// Responsibilities:
/// <list type="bullet">
/// <item><description>Extracts properties only available in ETABS (e.g., Label, Level)</description></item>
/// <item><description>Complements <see cref="CsiShellPropertiesExtractor"/> by adding product-specific data</description></item>
/// <item><description>Follows same pattern of single-purpose methods for clear API mapping</description></item>
/// </list>
///
/// Design Decisions:
/// <list type="bullet">
/// <item><description>Maintains separate methods for each property following CSI API structure</description></item>
/// <item><description>Properties are organized by their functional groups (Object ID, Assignments, Design)</description></item>
/// </list>
/// </remarks>
public sealed class EtabsShellPropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
private readonly DatabaseTableExtractor _databaseTableExtractor;
private readonly EtabsShellSectionResolver _etabsShellSectionResolver;
public EtabsShellPropertiesExtractor(
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton,
IConverterSettingsStore<CsiConversionSettings> settingsStore,
DatabaseTableExtractor databaseTableExtractor
DatabaseTableExtractor databaseTableExtractor,
EtabsShellSectionResolver etabsShellSectionResolver
)
{
_settingsStore = settingsStore;
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
_databaseTableExtractor = databaseTableExtractor;
_etabsShellSectionResolver = etabsShellSectionResolver;
}
public void ExtractProperties(CsiShellWrapper shell, Dictionary<string, object?> properties)
@@ -62,9 +51,22 @@ public sealed class EtabsShellPropertiesExtractor
assignments[ObjectPropertyKey.SECTION_ID] = sectionId;
assignments[ObjectPropertyKey.MATERIAL_ID] = materialId;
// CNX-2725 adds more numeric props for dashboard-ing
var geometry = properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
double area = GetArea(shell, designOrientation);
geometry.AddWithUnits("Area", area, $"{_settingsStore.Current.SpeckleUnits}²");
double thickness = GetSectionThickness(sectionId);
double volume = double.NaN;
if (!double.IsNaN(area) && !double.IsNaN(thickness) && area > 0 && thickness > 0)
{
// I am paranoid about what etabs could throw our way
double computedVolume = area * thickness;
volume = (!double.IsInfinity(computedVolume) && !double.IsNaN(computedVolume)) ? computedVolume : double.NaN;
}
geometry.AddWithUnits(ObjectPropertyKey.THICKNESS, thickness, _settingsStore.Current.SpeckleUnits);
geometry.AddWithUnits(ObjectPropertyKey.AREA, area, $"{_settingsStore.Current.SpeckleUnits}²");
geometry.AddWithUnits(ObjectPropertyKey.VOLUME, volume, $"{_settingsStore.Current.SpeckleUnits}³");
// store the object, section, and material id relationships in their corresponding caches to be accessed by the connector
if (!string.IsNullOrEmpty(sectionId))
@@ -188,4 +190,66 @@ public sealed class EtabsShellPropertiesExtractor
// all database data is returned as strings
return double.TryParse(area, out var result) ? result : double.NaN;
}
/// <summary>
/// Gets section thickness, resolving and caching section properties on first encounter.
/// </summary>
/// <param name="sectionId">The section name to get thickness for</param>
/// <returns>Thickness value, or NaN if section is invalid or thickness cannot be determined</returns>
private double GetSectionThickness(string sectionId)
{
// Guard against invalid sections
if (string.IsNullOrEmpty(sectionId) || sectionId == "None")
{
return double.NaN;
}
// Check if section already resolved and cached
if (!_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache.TryGetValue(sectionId, out var sectionProperties))
{
// First encounter - resolve section and cache all properties
sectionProperties = _etabsShellSectionResolver.ResolveSection(sectionId);
_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache[sectionId] = sectionProperties;
}
// Extract thickness from cached properties
return ExtractThicknessFromProperties(sectionProperties);
}
/// <summary>
/// Extracts thickness value from resolved section properties dictionary structure.
/// </summary>
/// <remarks>
/// Section properties have nested structure:
/// { "Property Data" -> { "Thickness" -> { "value" -> double, "units" -> string } } }
/// </remarks>
private static double ExtractThicknessFromProperties(Dictionary<string, object?> sectionProperties)
{
if (!sectionProperties.TryGetValue(SectionPropertyCategory.PROPERTY_DATA, out object? propertyDataObj))
{
return double.NaN;
}
if (propertyDataObj is not Dictionary<string, object?> propertyData)
{
return double.NaN;
}
if (!propertyData.TryGetValue(ObjectPropertyKey.THICKNESS, out object? thicknessObj))
{
return double.NaN;
}
if (thicknessObj is not Dictionary<string, object> thicknessDict)
{
return double.NaN;
}
if (!thicknessDict.TryGetValue("value", out object? valueObj))
{
return double.NaN;
}
return valueObj is double thickness ? thickness : double.NaN;
}
}
@@ -2,7 +2,7 @@ using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.Utils;
namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
/// <summary>
/// Attempts to resolve the section type and retrieve its properties by trying different section resolvers.
@@ -149,11 +149,15 @@ public class RevitToSpeckleCacheSingleton(ILogger<RevitToSpeckleCacheSingleton>
// get specific material proxy
if (!proxyMap.TryGetValue(materialId, out var materialProxy))
{
logger.LogError(
"Cache inconsistency: Material proxy not found for material {MaterialId} in element {ElementId}",
materialId,
elementId
);
if (materialId != DB.ElementId.InvalidElementId.ToString())
{
logger.LogError(
"Cache inconsistency: Material proxy not found for material {MaterialId} in element {ElementId}",
materialId,
elementId
);
}
return;
}
@@ -73,6 +73,28 @@ public class ClassPropertiesExtractor
elementProperties.Add("worksetName", worksetName);
}
if (element is DB.FamilyInstance familyInstance)
{
try
{
// get room id if applicable (only for FamilyInstance elements)
if (familyInstance.Room is not null)
{
elementProperties.Add("roomId", familyInstance.Room.Id.ToString());
}
// get space id if applicable (only for FamilyInstance elements)
if (familyInstance.Space is not null)
{
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
}
}
catch (Exception e) when (!e.IsFatal())
{
// silently ignore - not critical
}
}
// get group name if applicable
// TODO: in in group proxies separate issue. Below comments from PR #1081
// We're using group proxies in Rhino etc. Groups should be handled similarly in Revit, unless there's a good
@@ -0,0 +1,77 @@
# Intro
The rhino importer is functionally made from two parts:
- The `Speckle.Importers.JobProcessor`: A windows background service, designed to always be running.
- The `Speckle.Importers.Rhino`: A commandline app triggered by the JobProcessor to handle a single job.
They are deployed as a single product, and share the Innosetup installer deployment system of connectors.
> Note: this is documentation for internal purposes only.
> Despite much of the code being open-source, we do not offer any support or documentation for self hosters.
# First Time Setup and Checking For Updates
On the production/staging Windows servers, you'll find a powershell script `InstallUpdate.ps1` that
does a couple of things:
- Ensures the latest version of the importer is installed
- Defines the Postgres connection string, and how many service instances to stop/start (right now, just the 1 for prod)
`InstallUpdate.ps1` can be run to both check for updates and install updates, and to perform fresh installs and configuring the service(s)
The template for that script can be found [here](https://github.com/specklesystems/gitOps/blob/main/terraform/modules/windows-machine/templates/autoupdater/InstallUpdate.ps1.tpl) but needs to be tweaked per deployment.
There's lot more to do with the windows infra/rhino licence config that's not covered in this readme document.
# The Windows Service
You can view it from the (search "services" in search), It should be running at all times.
and can be stopped/started manually from the "services" menu
It does not display a terminal, the best place to see logs is in our seq-dev instance.
You can view error/crashes either via
1. Event viewer - there is already a "Custom View" called "Speckle Rhino Job Processor"
2. Reliability Monitor - Shows historical crash histogram
Note, the `Speckle.Importers.Rhino` crashes a lot due to Rhino.Inside behaviour, it's handled
crashes of the `Speckle.Importers.JobProcessor` are more serious.
If you need to troubleshoot, it is still possible to manually start the job processor exe as a command line app, rather than as a window service
this will give you back a terminal with all logs. But this is probably better done on the staging server...
Because it's a Windows Service, it will start up with the Windows OS.
If the process crashes (e.g. because it could not connect to the server/db)
it will automatically be restarted, with a backoff policy defined in the `InstallUpdate.ps1` script
If it's in a weird state where you need to manually restart it, this can be done from Services/task manager. Or simply by re-starting the machine.
# Tricking the InstallUpdate script into running even when there's no updates
If you want to make a change to the env vars of the service, or change the setup of the services in some way, it can be useful to run the script
without there actually being an update.
The easiest way to do this is to remove the registry key in `HKEY_CURRENT_USER\Software\Speckle\Services\InstalledConnectors`
That way, the script assumes that there is an update when ran.
# Debugging / Running Against a Local Server
If you have a local Speckle server and want to test processing Rhino jobs.
Firstly, there's a few things on the server you'll need to configure.
I'd recommend using the docker-compose files in the root of the server repo.
On speckle-frontend-2 set the envvar:
```yaml
NUXT_PUBLIC_FF_RHINO_FILE_IMPORTER_ENABLED: 'true'
```
On speckle-server set the envvars:
```yaml
FILEIMPORT_SERVICE_USE_PRIVATE_OBJECTS_SERVER_URL: 'false'
FF_RHINO_FILE_IMPORTER_ENABLED: 'true'
FILE_IMPORT_TIME_LIMIT_MIN: 30
```
Next, you can run the `Speckle.Importers.JobProcessor: Local Docker DB` launch configuration straight from your IDE.
The `launchSettings.json` should already be setup with the correct connection string.
It will run as normal CLI app, not a Windows Service, but aside from minor logging differences, this will work the same.
N.b. you will find you are not able to easily debug the `Speckle.Importers.Rhino` project like this, because it's a spawned sub process.
You have two options.
1. Capture the command line args and call it manually from your IDE, bypassing the JobProcessor.
2. Add a `Thread.Sleep(10000)` near the start of the entry point of the Rhino importer, and during the sleep time, attach your IDE to a running process.
@@ -7,27 +7,19 @@ namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
/// Creates a headless doc and imports the file
/// </summary>
/// <remarks>
/// Note: imported geometry will be converted to the default <c>mm</c> units
/// If we need to preserve the file units, a custom config needs to be created
/// Note: using OpenHeadless should preserve the original file's unit system for file types that have units
/// </remarks>
public sealed class DefaultConfig : IFileTypeConfig
internal sealed class DefaultConfig : IFileTypeConfig
{
public RhinoDoc OpenInHeadlessDocument(string filePath)
{
var doc = RhinoDoc.CreateHeadless(null);
try
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, null);
if (doc is null)
{
if (!doc.Import(filePath, null))
{
throw new SpeckleException("Rhino could not import this file");
}
return doc;
}
catch
{
doc.Dispose();
throw;
throw new SpeckleException("Rhino could not open this file");
}
return doc;
}
public void Dispose() { }
@@ -0,0 +1,23 @@
using Rhino;
using Rhino.FileIO;
using Speckle.Sdk;
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
internal sealed class DgnConfig : IFileTypeConfig
{
private readonly FileDgnReadOptions _readOptions = new() { ImportViews = true };
public RhinoDoc OpenInHeadlessDocument(string filePath)
{
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, _readOptions.ToDictionary());
if (doc is null)
{
throw new SpeckleException("Rhino could not open this file");
}
return doc;
}
public void Dispose() { }
}
@@ -4,7 +4,7 @@ using Speckle.Sdk;
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
public sealed class FbxConfig : IFileTypeConfig
internal sealed class FbxConfig : IFileTypeConfig
{
private readonly FileFbxReadOptions _readOptions =
new()
@@ -16,20 +16,12 @@ public sealed class FbxConfig : IFileTypeConfig
public RhinoDoc OpenInHeadlessDocument(string filePath)
{
var doc = RhinoDoc.CreateHeadless(null);
try
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, _readOptions.ToDictionary());
if (doc is null)
{
if (!doc.Import(filePath, _readOptions.ToDictionary()))
{
throw new SpeckleException("Rhino could not import this file");
}
return doc;
}
catch
{
doc.Dispose();
throw;
throw new SpeckleException("Rhino could not open this file");
}
return doc;
}
public void Dispose() { }
@@ -4,7 +4,7 @@ using Speckle.Sdk;
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
public sealed class ObjConfig : IFileTypeConfig
internal sealed class ObjConfig : IFileTypeConfig
{
private readonly FileObjReadOptions _readOptions =
new(new FileReadOptions() { OpenMode = true })
@@ -1,26 +0,0 @@
using Rhino;
using Speckle.Sdk;
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
/// <summary>
/// <see cref="RhinoDoc.OpenHeadless(string)"/> will preserve the units defined by the file
/// </summary>
/// <remarks>
/// For this config to be safe... we need to make sure we're running Rhino 8.24.25251 or greater due to https://mcneel.myjetbrains.com/youtrack/issue/RH-89162
/// </remarks>
public sealed class Rhino3dmConfig : IFileTypeConfig
{
public RhinoDoc OpenInHeadlessDocument(string filePath)
{
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath);
if (doc is null)
{
throw new SpeckleException("Rhino could not open this file");
}
return doc;
}
public void Dispose() { }
}
@@ -4,7 +4,7 @@ using Speckle.Sdk;
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
public sealed class SketchupConfig : IFileTypeConfig
internal sealed class SketchupConfig : IFileTypeConfig
{
private readonly FileSkpReadOptions _options =
new()
@@ -20,20 +20,12 @@ public sealed class SketchupConfig : IFileTypeConfig
public RhinoDoc OpenInHeadlessDocument(string filePath)
{
var doc = RhinoDoc.CreateHeadless(null);
try
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, _options.ToDictionary());
if (doc is null)
{
if (!doc.Import(filePath, _options.ToDictionary()))
{
throw new SpeckleException("Rhino could not import this file");
}
return doc;
}
catch
{
doc.Dispose();
throw;
throw new SpeckleException("Rhino could not import this file");
}
return doc;
}
public void Dispose() { }
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using Microsoft.Extensions.Logging;
using Rhino;
using Rhino.Runtime.InProcess;
@@ -9,55 +9,34 @@ using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
namespace Speckle.Importers.Rhino.Internal;
internal sealed class ImporterInstance(Sender sender, ILogger<ImporterInstance> logger) : IDisposable
internal sealed class ImporterInstance(ImporterArgs args, Sender sender, ILogger<ImporterInstance> logger) : IDisposable
{
private readonly ILogger _logger = logger;
private readonly RhinoCore _rhinoInstance = new(["/netcore-8"], WindowStyle.NoWindow);
private RhinoDoc? _rhinoDoc;
private readonly RhinoDoc _rhinoDoc = OpenDocument(args, logger);
public async Task<ImporterResponse> Run(ImporterArgs args, CancellationToken cancellationToken)
{
using var scopeJobId = ActivityScope.SetTag("jobId", args.JobId);
// using var scopeJobType = ActivityScope.SetTag("jobType", a.JobType);
using var scopeAttempt = ActivityScope.SetTag("job.attempt", args.Attempt.ToString());
using var scopeServerUrl = ActivityScope.SetTag("serverUrl", args.Account.serverInfo.url);
using var scopeProjectId = ActivityScope.SetTag("projectId", args.Project.id);
using var scopeModelId = ActivityScope.SetTag("modelId", args.ModelId);
using var scopeBlobId = ActivityScope.SetTag("blobId", args.BlobId);
using var scopeFileType = ActivityScope.SetTag("fileType", Path.GetExtension(args.FilePath).TrimStart('.'));
UserActivityScope.AddUserScope(args.Account);
private readonly IReadOnlyList<IDisposable> _scopes =
[
ActivityScope.SetTag("jobId", args.JobId),
ActivityScope.SetTag("job.attempt", args.Attempt.ToString()),
// ActivityScope.SetTag("jobType", args.JobType),
ActivityScope.SetTag("serverUrl", args.Account.serverInfo.url),
ActivityScope.SetTag("projectId", args.Project.id),
ActivityScope.SetTag("modelId", args.ModelId),
ActivityScope.SetTag("blobId", args.BlobId),
ActivityScope.SetTag("fileType", Path.GetExtension(args.FilePath).TrimStart('.')),
UserActivityScope.AddUserScope(args.Account),
];
var result = await TryImport(args, cancellationToken);
return result;
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "IPC")]
private async Task<ImporterResponse> TryImport(ImporterArgs args, CancellationToken cancellationToken)
public async Task<Version> RunRhinoImport(CancellationToken cancellationToken)
{
try
{
var version = await RunRhinoImport(args, cancellationToken);
return new ImporterResponse { Version = version, ErrorMessage = null };
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Import attempt failed with exception");
return new ImporterResponse { ErrorMessage = ex.Message, Version = null };
}
}
private async Task<Version> RunRhinoImport(ImporterArgs args, CancellationToken cancellationToken)
{
try
{
using var config = GetConfig(Path.GetExtension(args.FilePath));
logger.LogInformation("Opening file {FilePath}", args.FilePath);
_rhinoDoc = config.OpenInHeadlessDocument(args.FilePath);
RhinoDoc.ActiveDoc = _rhinoDoc;
var version = await sender.Send(args.Project, args.ModelId, args.Account, cancellationToken);
var version = await sender
.Send(args.Project, args.ModelId, args.Account, cancellationToken)
.ConfigureAwait(false);
return version;
}
finally
@@ -66,12 +45,20 @@ internal sealed class ImporterInstance(Sender sender, ILogger<ImporterInstance>
}
}
private static RhinoDoc OpenDocument(ImporterArgs args, ILogger logger)
{
using var config = GetConfig(Path.GetExtension(args.FilePath));
logger.LogInformation("Opening file {FilePath}", args.FilePath);
return config.OpenInHeadlessDocument(args.FilePath);
}
[Pure]
private static IFileTypeConfig GetConfig(string extension) =>
extension.ToLowerInvariant() switch
{
".skp" => new SketchupConfig(),
".obj" => new ObjConfig(),
".3dm" => new Rhino3dmConfig(),
".dgn" => new DgnConfig(),
".fbx" => new FbxConfig(),
_ => new DefaultConfig(),
};
@@ -83,5 +70,9 @@ internal sealed class ImporterInstance(Sender sender, ILogger<ImporterInstance>
// https://discourse.mcneel.com/t/rhino-inside-fatal-app-crashes-when-disposing-headless-documents/208673
_rhinoDoc?.Dispose();
_rhinoInstance.Dispose();
foreach (var scope in _scopes)
{
scope.Dispose();
}
}
}
@@ -0,0 +1,8 @@
using Microsoft.Extensions.Logging;
namespace Speckle.Importers.Rhino.Internal;
internal sealed class ImporterInstanceFactory(Sender sender, ILogger<ImporterInstance> logger)
{
public ImporterInstance Create(ImporterArgs args) => new(args, sender, logger);
}
@@ -20,6 +20,7 @@ internal static class ServiceRegistration
services.AddTransient<Progress>();
services.AddTransient<Sender>();
services.AddTransient<ImporterInstance>();
services.AddTransient<ImporterInstanceFactory>();
// override default thread context
services.AddSingleton<IThreadContext>(new ImporterThreadContext());
@@ -1,9 +1,11 @@
using System.Text.Json;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RhinoInside;
using Speckle.Importers.Rhino.Internal;
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
namespace Speckle.Importers.Rhino;
@@ -18,9 +20,12 @@ public static class Program
Console.WriteLine($"Loading Rhino @ {Resolver.RhinoSystemDirectory}");
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "IPC")]
public static async Task Main(string[] args)
{
ILogger? logger = null;
ImporterInstance? importer = null;
try
{
var importerArgs = JsonSerializer.Deserialize<ImporterArgs>(args[0], s_serializerOptions);
@@ -32,13 +37,34 @@ public static class Program
TaskScheduler.UnobservedTaskException += (_, eventArgs) =>
logger.LogCritical(eventArgs.Exception, "Unobserved Task Exception");
var importer = serviceProvider.GetRequiredService<ImporterInstance>();
var factory = serviceProvider.GetRequiredService<ImporterInstanceFactory>();
// Error handling flow below here looks a bit of a mess, but we're having to navigate threading issues with rhino inside
// https://discourse.mcneel.com/t/rhino-inside-fatal-app-crashes-when-disposing-headless-documents/208673/7
try
{
// This needs to be called on the main thread
importer = factory.Create(importerArgs);
}
catch (Exception ex)
{
WriteResult(new() { ErrorMessage = ex.Message }, importerArgs.ResultsPath);
return;
}
// As soon as the main thread is yielded, it will be hogged by Rhino
// Task.Run ensures we run everything on a thread pool thread.
await Task.Run(async () =>
{
var result = await importer.Run(importerArgs, CancellationToken.None);
var serializedResult = JsonSerializer.Serialize(result, s_serializerOptions);
File.WriteAllLines(importerArgs.ResultsPath, [serializedResult]);
try
{
Version result = await importer.RunRhinoImport(CancellationToken.None).ConfigureAwait(false);
WriteResult(new() { Version = result }, importerArgs.ResultsPath);
}
catch (Exception ex)
{
WriteResult(new() { ErrorMessage = ex.Message }, importerArgs.ResultsPath);
}
})
.ConfigureAwait(false);
}
@@ -53,7 +79,18 @@ public static class Program
{
Console.WriteLine(MESSAGE);
}
throw;
}
finally
{
importer?.Dispose();
}
}
private static void WriteResult(ImporterResponse result, string resultsPath)
{
var serializedResult = JsonSerializer.Serialize(result, s_serializerOptions);
File.WriteAllLines(resultsPath, [serializedResult]);
}
}
@@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.23.25251.13001"/>
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.23.25251.13001"/>
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.23.25251.13001"/>
<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="Rhino.Inside" />
</ItemGroup>
@@ -4,11 +4,11 @@
"net8.0-windows7.0": {
"Grasshopper": {
"type": "Direct",
"requested": "[8.23.25251.13001, )",
"resolved": "8.23.25251.13001",
"contentHash": "AoswT4QQlD21t/ywsUy2cZRuSQDg39WGIVoyc0jOPv7973WwMrGO891ZIQ0vRZ23s4NGa5lqPU53ZgoGZqSpGA==",
"requested": "[8.24.25281.15001, )",
"resolved": "8.24.25281.15001",
"contentHash": "TEtB8nElTvhMJctLhv8UC1v6jscYdTgsoRQIr31ewGZr6cpgGtQTBmUk26/9ZvQxXCgOp7Y4EZdcEZkmqCm1SQ==",
"dependencies": {
"RhinoCommon": "[8.23.25251.13001]"
"RhinoCommon": "[8.24.25281.15001]"
}
},
"Microsoft.NETFramework.ReferenceAssemblies": {
@@ -48,20 +48,20 @@
},
"RhinoCommon": {
"type": "Direct",
"requested": "[8.23.25251.13001, )",
"resolved": "8.23.25251.13001",
"contentHash": "FKkGghsK0lG7nrjweP3W009lbqMn9tAPF27gw8wognZfZ4hYkTZygOpF99p2Vl7gn+faGKEmh11LgDj6PU/YNw==",
"requested": "[8.24.25281.15001, )",
"resolved": "8.24.25281.15001",
"contentHash": "K8dd7DJvEUUYHpwkyyxr/ojK3e8swlE0STeyG+ulVWkWNHK6gIRDxMYCwB7kNyHHMgpr/vpQlMgR3SVD1GoMTA==",
"dependencies": {
"System.Drawing.Common": "7.0.0"
}
},
"RhinoWindows": {
"type": "Direct",
"requested": "[8.23.25251.13001, )",
"resolved": "8.23.25251.13001",
"contentHash": "xq30IuvvjlensoVROEIenPotvV9o7ynXlROWuZsLfWJp4NwSHc1F/cLn2Rqd0xHCZGhxlwhIYjrdU5WMtDPlxQ==",
"requested": "[8.24.25281.15001, )",
"resolved": "8.24.25281.15001",
"contentHash": "JbG98P80Hskpomzx1xhqFoz2WmVnhjda0noQyZE+dE678ZKmw+O3i/iIjaN5jydXvTu/fZXRzQmEDL7M7Ura8g==",
"dependencies": {
"RhinoCommon": "[8.23.25251.13001]"
"RhinoCommon": "[8.24.25281.15001]"
}
},
"Speckle.InterfaceGenerator": {
+14 -15
View File
@@ -45,24 +45,15 @@ Make sure to also check and ⭐️ these other Speckle next generation repositor
# Developing and Debugging
Clone this repo. **Each section has its own readme**, so follow each readme for specific build and debug instructions.
## Developing
Issues or questions? We encourage everyone interested to debug / hack / contribute / give feedback to this project.
It is recommended that you use Jetbrains Rider (version 2024.3 or greater) or Visual Studio 2022 (version 17.13 or greater)
The project requires version 8.0.4xx of the .NET SDK.
You can download the latest version from https://dotnet.microsoft.com/en-us/download/dotnet/8.0
> **A note on Accounts:**
> The connectors themselves don't have features to manage your Speckle accounts; this functionality is delegated to the Speckle Manager desktop app. You can install it [from here](https://speckle-releases.ams3.digitaloceanspaces.com/manager/SpeckleManager%20Setup.exe).
From there you can open the main `Speckle.Connectors.sln` solution and build the project
## Local Builds
For good development experience and environment setup, run the commands below as needed.
### Switching to SLNX
SLNX was introduced with .NET 9 (in May 2024), Visual Studio 17.13 and Rider 2024.3. The older SLNs being used remain for now but will be removed when .NET 10 is introduced to the repo. SLNXs specific to certain host apps are being generated from the main SLN to allow for faster developmenet.
[https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/](https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/)
[https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/](https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/)
For good development experience and environment setup, you the commands are avaible needed.
### Formatting
We're using [CSharpier](https://github.com/belav/csharpier) to format our code. You can install Csharpier in a few ways:
@@ -107,6 +98,14 @@ This solution includes the Core and Objects projects from the speckle-sharp-sdk
> [!WARNING]
> Using `Local.sln` will modify all your package locks. **Don't check these in!** Revert with the `clean-locks` command or use the regular solution to revert once your changes are made.
## Switching to SLNX
SLNX was introduced with .NET 9 (in May 2024), Visual Studio 17.13 and Rider 2024.3. The older SLNs being used remain for now but will be removed when .NET 10 is introduced to the repo. SLNXs specific to certain host apps are being generated from the main SLN to allow for faster developmenet.
[https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/](https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/)
[https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/](https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/)
# Security and Licensing
### Security