From de662e4a2b4a9798fd67736db1466fd3fc0bbb5d Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Wed, 30 Jul 2025 21:00:17 +0100 Subject: [PATCH 01/26] adds material quantities for pipes --- .../Raw/MaterialQuantitiesToSpeckle.cs | 194 ++++++++++++++---- 1 file changed, 152 insertions(+), 42 deletions(-) diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Raw/MaterialQuantitiesToSpeckle.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Raw/MaterialQuantitiesToSpeckle.cs index 91474209e..0dc0ff869 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Raw/MaterialQuantitiesToSpeckle.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Raw/MaterialQuantitiesToSpeckle.cs @@ -44,9 +44,27 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter Convert(DB.Element target) { Dictionary quantities = new(); - if (target.Category?.HasMaterialQuantities ?? false) //category can be null + switch (target) { - foreach (DB.ElementId? matId in target.GetMaterialIds(false)) + case DBA.Railing railing: + // railings can have subelements including top rails, hand rails, and balusters. + // they also do *not* have any materials associated with their category. + List railingElementIds = [railing.GetTypeId(), railing.TopRail, .. railing.GetHandRails()]; + ProcessMaterialsByElementTypes(railingElementIds, quantities); + break; + default: + ProcessMaterialsByCategory(target, quantities); + break; + } + + return quantities; + } + + private void ProcessMaterialsByCategory(DB.Element element, Dictionary quantities) + { + if (element.Category?.HasMaterialQuantities ?? false) //category can be null + { + foreach (DB.ElementId? matId in element.GetMaterialIds(false)) { if (matId is null) { @@ -56,11 +74,18 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter(); var unitSettings = _converterSettings.Current.Document.GetUnits(); + // add material props + if (TryAddMaterialPropertiesToQuantitiesDict(matId, materialQuantity, out string matName)) + { + quantities[matName] = materialQuantity; + } + + // add area and volume props var areaUnitType = unitSettings.GetFormatOptions(DB.SpecTypeId.Area).GetUnitTypeId(); AddMaterialProperty( materialQuantity, "area", - _scalingService.Scale(target.GetMaterialArea(matId, false), areaUnitType), + _scalingService.Scale(element.GetMaterialArea(matId, false), areaUnitType), areaUnitType ); @@ -68,57 +93,142 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter elementIds, Dictionary quantities) + { + Dictionary matLengths = new(); // stores mat id to total length found for mat + + foreach (DB.ElementId elementId in elementIds) + { + if ( + _converterSettings.Current.Document.GetElement(elementId) is DB.Element element + && _converterSettings.Current.Document.GetElement(element.GetTypeId()) is DB.ElementType elementType + ) + { + DB.ElementId elementMatId = DB.ElementId.InvalidElementId; + + foreach (DB.Parameter param in elementType.Parameters) { - materialQuantity["materialName"] = material.Name; - materialQuantity["materialCategory"] = material.MaterialCategory; - materialQuantity["materialClass"] = material.MaterialClass; - - // get StructuralAssetId (or try to) - DB.ElementId structuralAssetId = material.StructuralAssetId; - if (structuralAssetId != DB.ElementId.InvalidElementId) + DB.Definition def = param.Definition; + if (param.StorageType == DB.StorageType.ElementId && def.GetDataType() == DB.SpecTypeId.Reference.Material) { - StructuralAssetProperties structuralAssetProperties = _structuralAssetExtractor.TryGetProperties( - structuralAssetId - ); + elementMatId = param.AsElementId(); + break; + } + } - materialQuantity["structuralAsset"] = structuralAssetProperties.Name; - AddMaterialProperty( - materialQuantity, - "density", - structuralAssetProperties.Density, - structuralAssetProperties.DensityUnitId - ); - - // more reliable way of determining material type (wood/concrete/type) as it uses Revit enum - // materialClass, materialCategory etc. are user string inputs - materialQuantity["materialType"] = structuralAssetProperties.MaterialType; - - // Only add compressive strength for concrete materials (used by F+E for Automate) - if ( - structuralAssetProperties.MaterialType == "Concrete" - && structuralAssetProperties.CompressiveStrength.HasValue - ) + if (elementMatId != DB.ElementId.InvalidElementId) + { + // try get the length from the element + foreach (DB.Parameter eParam in element.Parameters) + { + DB.Definition eParamDef = eParam.Definition; + var forgeTypeId = eParamDef.GetDataType(); + if (forgeTypeId == DB.SpecTypeId.Length) { - AddMaterialProperty( - materialQuantity, - "compressiveStrength", - structuralAssetProperties.CompressiveStrength.Value, - structuralAssetProperties.CompressiveStrengthUnitId! - ); + double length = eParam.AsDouble(); + if (matLengths.TryGetValue(elementMatId, out double _)) + { + matLengths[elementMatId] += length; + } + else + { + matLengths.Add(elementMatId, length); + } } } - - quantities[material.Name] = materialQuantity; } } } - return quantities; + foreach (var entry in matLengths) + { + var materialQuantity = new Dictionary(); + var unitSettings = _converterSettings.Current.Document.GetUnits(); + + // add material props + if (TryAddMaterialPropertiesToQuantitiesDict(entry.Key, materialQuantity, out string matName)) + { + quantities[matName] = materialQuantity; + + // add length prop + var lengthUnitType = unitSettings.GetFormatOptions(DB.SpecTypeId.Length).GetUnitTypeId(); + AddMaterialProperty( + materialQuantity, + "length", + _scalingService.Scale(entry.Value, lengthUnitType), + lengthUnitType + ); + } + } + } + + /// + /// Adds the material properties (like name, category, and class) to the material quantity dictionary + /// + /// the material id + /// + /// + /// true if material is found, false if not + private bool TryAddMaterialPropertiesToQuantitiesDict( + DB.ElementId matId, + Dictionary materialQuantity, + out string matName + ) + { + matName = ""; + if (_converterSettings.Current.Document.GetElement(matId) is DB.Material material) + { + materialQuantity["materialName"] = material.Name; + materialQuantity["materialCategory"] = material.MaterialCategory; + materialQuantity["materialClass"] = material.MaterialClass; + + // get StructuralAssetId (or try to) + DB.ElementId structuralAssetId = material.StructuralAssetId; + if (structuralAssetId != DB.ElementId.InvalidElementId) + { + StructuralAssetProperties structuralAssetProperties = _structuralAssetExtractor.TryGetProperties( + structuralAssetId + ); + + materialQuantity["structuralAsset"] = structuralAssetProperties.Name; + AddMaterialProperty( + materialQuantity, + "density", + structuralAssetProperties.Density, + structuralAssetProperties.DensityUnitId + ); + + // more reliable way of determining material type (wood/concrete/type) as it uses Revit enum + // materialClass, materialCategory etc. are user string inputs + materialQuantity["materialType"] = structuralAssetProperties.MaterialType; + + // Only add compressive strength for concrete materials (used by F+E for Automate) + if ( + structuralAssetProperties.MaterialType == "Concrete" + && structuralAssetProperties.CompressiveStrength.HasValue + ) + { + AddMaterialProperty( + materialQuantity, + "compressiveStrength", + structuralAssetProperties.CompressiveStrength.Value, + structuralAssetProperties.CompressiveStrengthUnitId! + ); + } + } + + matName = material.Name; + return true; + } + + return false; } /// @@ -129,7 +239,7 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverterThe numeric value of the property /// The Forge type ID representing the units of the property /// - /// Saves code when used repeatedbly. Etabs implements an extension method to dicts (see utils folder). May be worth exploring. + /// Saves code when used repeatedly. Etabs implements an extension method to dicts (see utils folder). May be worth exploring. /// private void AddMaterialProperty( Dictionary materialQuantity, From 876d5c1bfe8fd6d14b59e1c99c6206ea59e1da0e Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:50:03 +0100 Subject: [PATCH 02/26] fix(rhino-importer): Do not save to objects sqlite cache (#1033) * First pass * ifc importer to not save objects to sqlite --- .../JobHandlers/RhinoJobHandler.cs | 23 +++++++++++-- .../JobProcessor.cs | 6 ++-- .../Internal/DummySqliteJsonCacheManager.cs | 34 +++++++++++++++++++ .../Internal/ImporterInstance.cs | 5 ++- .../Internal/ServiceRegistration.cs | 4 +++ 5 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 Importers/Rhino/Speckle.Importers.Rhino/Internal/DummySqliteJsonCacheManager.cs diff --git a/Importers/Rhino/Speckle.Importers.JobProcessor/JobHandlers/RhinoJobHandler.cs b/Importers/Rhino/Speckle.Importers.JobProcessor/JobHandlers/RhinoJobHandler.cs index 6384323fc..4a978f29b 100644 --- a/Importers/Rhino/Speckle.Importers.JobProcessor/JobHandlers/RhinoJobHandler.cs +++ b/Importers/Rhino/Speckle.Importers.JobProcessor/JobHandlers/RhinoJobHandler.cs @@ -1,11 +1,12 @@ -using Speckle.Importers.JobProcessor.Domain; +using Microsoft.Extensions.Logging; +using Speckle.Importers.JobProcessor.Domain; using Speckle.Importers.Rhino; using Speckle.Sdk.Api; using Version = Speckle.Sdk.Api.GraphQL.Models.Version; namespace Speckle.Importers.JobProcessor.JobHandlers; -internal sealed class RhinoJobHandler : IJobHandler +internal sealed class RhinoJobHandler(ILogger logger) : IJobHandler { public async Task ProcessJob(FileimportJob job, IClient client, CancellationToken cancellationToken) { @@ -31,7 +32,23 @@ internal sealed class RhinoJobHandler : IJobHandler } finally { - Directory.Delete(directory.FullName, true); + try + { + await Cleanup(directory.FullName); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + logger.LogError(ex, "Failed to cleanup file"); + } } } + + private static async Task Cleanup(string path) + { + //Some weird cases where *something* is keeping a lock on the file, this *may* fix things... + await Task.Delay(100); + GC.Collect(); + GC.WaitForPendingFinalizers(); + Directory.Delete(path, true); + } } diff --git a/Importers/Rhino/Speckle.Importers.JobProcessor/JobProcessor.cs b/Importers/Rhino/Speckle.Importers.JobProcessor/JobProcessor.cs index cd2c638f7..6f7c06a5d 100644 --- a/Importers/Rhino/Speckle.Importers.JobProcessor/JobProcessor.cs +++ b/Importers/Rhino/Speckle.Importers.JobProcessor/JobProcessor.cs @@ -28,10 +28,10 @@ internal sealed class JobProcessorInstance( { await using var connection = await repository.SetupConnection(cancellationToken).ConfigureAwait(false); + logger.LogInformation("Listening for jobs..."); + while (true) { - logger.LogInformation("Listening for jobs..."); - FileimportJob? job = await repository.GetNextJob(connection, cancellationToken); if (job == null) { @@ -94,7 +94,7 @@ internal sealed class JobProcessorInstance( projectId = job.Payload.ProjectId, jobId = job.Payload.BlobId, warnings = [], - reason = ex.ToString(), + reason = string.IsNullOrEmpty(ex.Message) ? ex.GetType().ToString() : ex.Message, result = new FileImportResult(0, 0, 0, "Rhino Importer", versionId: null) }; await client.FileImport.FinishFileImportJob(input, cancellationToken); diff --git a/Importers/Rhino/Speckle.Importers.Rhino/Internal/DummySqliteJsonCacheManager.cs b/Importers/Rhino/Speckle.Importers.Rhino/Internal/DummySqliteJsonCacheManager.cs new file mode 100644 index 000000000..19dc29105 --- /dev/null +++ b/Importers/Rhino/Speckle.Importers.Rhino/Internal/DummySqliteJsonCacheManager.cs @@ -0,0 +1,34 @@ +using Speckle.Sdk.SQLite; + +namespace Speckle.Importers.Rhino.Internal; + +/// +/// Dummy implementation of to avoid +/// +public sealed class DummySqliteJsonCacheManager : ISqLiteJsonCacheManager +{ + public void Dispose() { } + + public IReadOnlyCollection<(string Id, string Json)> GetAllObjects() => []; + + public void DeleteObject(string id) { } + + public string? GetObject(string id) => null; + + public void SaveObject(string id, string json) { } + + public void UpdateObject(string id, string json) { } + + public void SaveObjects(IEnumerable<(string id, string json)> items) { } + + public bool HasObject(string objectId) => false; +} + +public sealed class DummySqliteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory +{ + private static readonly ISqLiteJsonCacheManager s_instance = new DummySqliteJsonCacheManager(); + + public ISqLiteJsonCacheManager CreateForUser(string scope) => s_instance; + + public ISqLiteJsonCacheManager CreateFromStream(string streamId) => s_instance; +} diff --git a/Importers/Rhino/Speckle.Importers.Rhino/Internal/ImporterInstance.cs b/Importers/Rhino/Speckle.Importers.Rhino/Internal/ImporterInstance.cs index 3ec008add..80e90cb08 100644 --- a/Importers/Rhino/Speckle.Importers.Rhino/Internal/ImporterInstance.cs +++ b/Importers/Rhino/Speckle.Importers.Rhino/Internal/ImporterInstance.cs @@ -1,4 +1,4 @@ -using Rhino; +using Rhino; using Speckle.Importers.Rhino.Internal.FileTypeConfig; using Speckle.Sdk; using Speckle.Sdk.Credentials; @@ -34,10 +34,9 @@ internal sealed class ImporterInstance(Sender sender) } finally { - //Being a bit extra defensive that we're cleaning up the old doc + //Being a bit extra defensive to ensure we're cleaning up the old doc RhinoDoc.ActiveDoc?.Dispose(); RhinoDoc.ActiveDoc = null; - GC.Collect(); } } diff --git a/Importers/Rhino/Speckle.Importers.Rhino/Internal/ServiceRegistration.cs b/Importers/Rhino/Speckle.Importers.Rhino/Internal/ServiceRegistration.cs index 8d8bfd75f..395cb755b 100644 --- a/Importers/Rhino/Speckle.Importers.Rhino/Internal/ServiceRegistration.cs +++ b/Importers/Rhino/Speckle.Importers.Rhino/Internal/ServiceRegistration.cs @@ -3,6 +3,7 @@ using Speckle.Connectors.Common; using Speckle.Connectors.Common.Threading; using Speckle.Connectors.Rhino.DependencyInjection; using Speckle.Converters.Rhino; +using Speckle.Sdk.SQLite; namespace Speckle.Importers.Rhino.Internal; @@ -21,6 +22,9 @@ internal static class ServiceRegistration // override default thread context services.AddSingleton(new ImporterThreadContext()); + // override sqlite cache, since we don't want to persist to disk any object data + services.AddTransient(); + return services; } } From 3e596cac2989f5962d8e970466343ab882cacf54 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Tue, 19 Aug 2025 10:00:10 +0100 Subject: [PATCH 03/26] Update MeshToSpeckleConverter.cs --- .../ToSpeckle/Raw/MeshToSpeckleConverter.cs | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs index 2bbe4f676..15248dd25 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs @@ -1,7 +1,5 @@ using Speckle.Converters.Common; using Speckle.Converters.Common.Objects; -using Speckle.Converters.Rhino.Extensions; -using Speckle.Converters.Rhino.ToSpeckle.Meshing; using Speckle.Sdk.Common.Exceptions; namespace Speckle.Converters.Rhino.ToSpeckle.Raw; @@ -34,39 +32,36 @@ public class MeshToSpeckleConverter : ITypedConverter throw new ValidationException("Cannot convert a mesh with 0 vertices/faces"); } - // Extracting Rhino Mesh and converting to Speckle with the most suitable settings (e.g. moving to origin first, if needed) - // This is needed because of Rhino using single precision numbers for Mesh vertices: https://wiki.mcneel.com/rhino/farfromorigin - RG.Mesh meshToConvert = target; - RG.Vector3d? vector = null; - - // 1. If needed, move geometry to origin - if (_settingsStore.Current.ModelFarFromOrigin && target.IsFarFromOrigin(out RG.Vector3d vectorToGeometry)) - { - meshToConvert = (RG.Mesh)target.Duplicate(); - meshToConvert.Transform(RG.Transform.Translation(-vectorToGeometry)); - vector = vectorToGeometry; - } - // 2. Convert extracted Mesh to Speckle. We don't move geometry back yet, because 'far from origin' geometry is causing Speckle conversion issues too - SOG.Mesh convertedMesh = ConvertMesh(meshToConvert); - - // 3. Move Speckle geometry back from origin, if translation was applied - DisplayMeshExtractor.MoveSpeckleMeshes([convertedMesh], vector, _settingsStore.Current.SpeckleUnits); + SOG.Mesh convertedMesh = ConvertMesh(target); return convertedMesh; } - private SOG.Mesh ConvertMesh(RG.Mesh target) + // By default, Rhino meshes store vertices with single precision. + // For geometry very far away from the origin, this will result in jagged meshes: https://wiki.mcneel.com/rhino/farfromorigin + // Although the .ToPoint3dArray creates extra memory alloc, it's more important to prevent bad geometry for eg infrastructure models. + // An alternative would be to move the geometry back to the origin before conversion and move it back after (since transforms are double precision), but this creates complications for grasshopper since this would have to be evaluated per geometry and not once per model (therefore also expensive). + private double[] ConvertDoublePrecisionVertices(RG.Mesh target) { var vertexCoordinates = new double[target.Vertices.Count * 3]; + target.Vertices.UseDoublePrecisionVertices = true; + RG.Point3d[] vertices = target.Vertices.ToPoint3dArray(); var x = 0; - for (int i = 0; i < target.Vertices.Count; i++) + for (int i = 0; i < vertices.Length; i++) { - var v = target.Vertices[i]; + var v = vertices[i]; vertexCoordinates[x++] = v.X; vertexCoordinates[x++] = v.Y; vertexCoordinates[x++] = v.Z; } + return vertexCoordinates; + } + + private SOG.Mesh ConvertMesh(RG.Mesh target) + { + var vertexCoordinates = ConvertDoublePrecisionVertices(target); + List faces = new(); foreach (RG.MeshNgon polygon in target.GetNgonAndFacesEnumerable()) @@ -81,7 +76,7 @@ public class MeshToSpeckleConverter : ITypedConverter } var colors = new int[target.VertexColors.Count]; - x = 0; + int x = 0; foreach (var c in target.VertexColors) { colors[x++] = c.ToArgb(); From f434cde7b36635281210ba66c829b7edc32ab632 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Tue, 19 Aug 2025 14:23:12 +0100 Subject: [PATCH 04/26] removes model far from origin logic from rhino --- .../Extensions/GeometryBaseExtensions.cs | 24 ------------ .../RhinoConversionSettings.cs | 9 +---- .../RhinoConversionSettingsFactory.cs | 23 +---------- .../ToSpeckle/Meshing/DisplayMeshExtractor.cs | 38 ++----------------- .../ToSpeckle/Raw/BrepToSpeckleConverter.cs | 7 +--- .../Raw/ExtrusionToSpeckleConverter.cs | 7 +--- .../ToSpeckle/Raw/HatchToSpeckleConverter.cs | 7 +--- .../ToSpeckle/Raw/MeshToSpeckleConverter.cs | 9 ++--- .../ToSpeckle/Raw/SubDToSpeckleConverter.cs | 7 +--- Speckle.Connectors.sln | 1 + 10 files changed, 16 insertions(+), 116 deletions(-) delete mode 100644 Converters/Rhino/Speckle.Converters.RhinoShared/Extensions/GeometryBaseExtensions.cs diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/Extensions/GeometryBaseExtensions.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/Extensions/GeometryBaseExtensions.cs deleted file mode 100644 index bb870fce1..000000000 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/Extensions/GeometryBaseExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Speckle.Converters.Rhino.Extensions; - -public static class GeometryBaseExtensions -{ - /// - /// Getting translation vector from origin to the Geometry bbox Center (if geometry is far from origin and translation needed) - /// This is needed for some objects, because of Rhino using single precision numbers for Mesh vertices: https://wiki.mcneel.com/rhino/farfromorigin - /// - /// - /// Vector from origin to Geometry bbox center (if translation needed), otherwise zero-length vector. - /// - public static bool IsFarFromOrigin(this RG.GeometryBase geometry, out RG.Vector3d vectorToGeometry) - { - var geometryBbox = geometry.GetBoundingBox(false); // 'false' for 'accurate' parameter to accelerate bbox calculation - if (geometryBbox.Min.DistanceTo(RG.Point3d.Origin) > 1e5 || geometryBbox.Max.DistanceTo(RG.Point3d.Origin) > 1e5) - { - vectorToGeometry = new RG.Vector3d(geometryBbox.Center); - return true; - } - - vectorToGeometry = new RG.Vector3d(); - return false; - } -} diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettings.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettings.cs index 455555cb4..5b53e888c 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettings.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettings.cs @@ -1,13 +1,8 @@ -using Rhino; +using Rhino; namespace Speckle.Converters.Rhino; /// /// Represents the settings used for Rhino and Grasshopper conversions. /// -public record RhinoConversionSettings( - RhinoDoc Document, - string SpeckleUnits, - bool ModelFarFromOrigin, - bool AddVisualizationProperties -); +public record RhinoConversionSettings(RhinoDoc Document, string SpeckleUnits, bool AddVisualizationProperties); diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettingsFactory.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettingsFactory.cs index 8b46a7091..002664dd9 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettingsFactory.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/RhinoConversionSettingsFactory.cs @@ -1,4 +1,4 @@ -using Rhino; +using Rhino; using Speckle.Converters.Common; using Speckle.InterfaceGenerator; @@ -13,24 +13,5 @@ public class RhinoConversionSettingsFactory( public RhinoConversionSettings Current => settingsStore.Current; public RhinoConversionSettings Create(RhinoDoc document, bool addVisualizationProperties) => - new( - document, - unitsConverter.ConvertOrThrow(RhinoDoc.ActiveDoc.ModelUnitSystem), - ModelFarFromOrigin(), - addVisualizationProperties - ); - - /// - /// Quick check whether any of the objects in the scene might be located too far from origin and cause precision issues during meshing. - /// It prevents 'normal' Rhino models (not too far from origin) from unnecessary Bbox calculations on every object on Send. - /// - private bool ModelFarFromOrigin() - { - RG.BoundingBox bbox = RhinoDoc.ActiveDoc.Objects.BoundingBox; - if (bbox.Min.DistanceTo(RG.Point3d.Origin) > 1e5 || bbox.Max.DistanceTo(RG.Point3d.Origin) > 1e5) - { - return true; - } - return false; - } + new(document, unitsConverter.ConvertOrThrow(RhinoDoc.ActiveDoc.ModelUnitSystem), addVisualizationProperties); } diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs index 5a72761c8..c7d5586d0 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs @@ -1,7 +1,5 @@ using Rhino.DocObjects; using Speckle.Converters.Common.Objects; -using Speckle.Converters.Rhino.Extensions; -using Speckle.DoubleNumerics; using Speckle.Sdk.Common.Exceptions; namespace Speckle.Converters.Rhino.ToSpeckle.Meshing; @@ -108,46 +106,16 @@ public static class DisplayMeshExtractor } /// - /// Extracting Rhino Mesh and converting to Speckle with the most suitable settings (e.g. moving to origin first, if needed) - /// This is needed because of Rhino using single precision numbers for Mesh vertices: https://wiki.mcneel.com/rhino/farfromorigin + /// Extracting Rhino Mesh and converting to Speckle with the most suitable settings /// /// List of converted Speckle meshes public static List GetSpeckleMeshes( RG.GeometryBase geometry, - bool modelFarFromOrigin, - string units, ITypedConverter meshConverter ) { - RG.GeometryBase geometryToMesh = geometry; - RG.Vector3d? vector = null; - - // 1.1. If needed, move geometry to origin - if (modelFarFromOrigin && geometry.IsFarFromOrigin(out RG.Vector3d vectorToGeometry)) - { - geometryToMesh = geometry.Duplicate(); - geometryToMesh.Transform(RG.Transform.Translation(-vectorToGeometry)); - vector = vectorToGeometry; - } - // 1.2. Extract Rhino Mesh - RG.Mesh movedDisplayMesh = GetGeometryDisplayMesh(geometryToMesh, true); - - // 2. Convert extracted Mesh to Speckle. We don't move geometry back yet, because 'far from origin' geometry is causing Speckle conversion issues too - List displayValue = new() { meshConverter.Convert(movedDisplayMesh) }; - - // 3. Move Speckle geometry back from origin, if translation was applied - MoveSpeckleMeshes(displayValue, vector, units); - + RG.Mesh displayMesh = GetGeometryDisplayMesh(geometry, true); + List displayValue = new() { meshConverter.Convert(displayMesh) }; return displayValue; } - - public static void MoveSpeckleMeshes(List displayValue, RG.Vector3d? vectorToGeometry, string units) - { - if (vectorToGeometry is RG.Vector3d vector) - { - Matrix4x4 matrix = new(1, 0, 0, vector.X, 0, 1, 0, vector.Y, 0, 0, 1, vector.Z, 0, 0, 0, 1); - SO.Transform transform = new() { matrix = matrix, units = units }; - displayValue.ForEach(x => x.Transform(transform)); - } - } } diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs index 88b32cca0..8b7678c43 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs @@ -28,12 +28,7 @@ public class BrepToSpeckleConverter : ITypedConverter { var brepEncoding = RawEncodingCreator.Encode(target, _settingsStore.Current.Document); - List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( - target, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter - ); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes(target, _meshConverter); var bx = new SOG.BrepX() { diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs index 1c086fc23..df9b7729c 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs @@ -28,12 +28,7 @@ public class ExtrusionToSpeckleConverter : ITypedConverter displayValue = DisplayMeshExtractor.GetSpeckleMeshes( - target, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter - ); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes(target, _meshConverter); var bx = new SOG.ExtrusionX() { diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs index 94c7bbdd6..9503218e7 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs @@ -39,12 +39,7 @@ public class HatchToSpeckleConverter : ITypedConverter // create display mesh from region by converting to brep first var brep = RG.Brep.TryConvertBrep(target); - List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( - brep, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter - ); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes(brep, _meshConverter); return new SOG.Region { diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs index 15248dd25..49d0f7f4f 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs @@ -37,14 +37,13 @@ public class MeshToSpeckleConverter : ITypedConverter return convertedMesh; } - // By default, Rhino meshes store vertices with single precision. - // For geometry very far away from the origin, this will result in jagged meshes: https://wiki.mcneel.com/rhino/farfromorigin - // Although the .ToPoint3dArray creates extra memory alloc, it's more important to prevent bad geometry for eg infrastructure models. - // An alternative would be to move the geometry back to the origin before conversion and move it back after (since transforms are double precision), but this creates complications for grasshopper since this would have to be evaluated per geometry and not once per model (therefore also expensive). + // Rhino common is casting mesh vertex coords from doubles to float: by default the api returns `Vertices` as float instead of double precision + // https://github.com/mcneel/rhino3dm/blob/71c63a8c1c87782a13a1b76c825e4b792b36fd09/src/dotnet/opennurbs/opennurbs_mesh.cs#L6990-L7000 + // We need to use double precision or else meshes far from origin will come out distorted: do *not* access `Vertices` directly - use `ToPoint3dArray` private double[] ConvertDoublePrecisionVertices(RG.Mesh target) { var vertexCoordinates = new double[target.Vertices.Count * 3]; - target.Vertices.UseDoublePrecisionVertices = true; + // target.Vertices.UseDoublePrecisionVertices = true; RG.Point3d[] vertices = target.Vertices.ToPoint3dArray(); var x = 0; for (int i = 0; i < vertices.Length; i++) diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs index 98091f519..7a4df1dcd 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs @@ -28,12 +28,7 @@ public class SubDToSpeckleConverter : ITypedConverter { var subdEncoding = RawEncodingCreator.Encode(target, _settingsStore.Current.Document); - List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( - target, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter - ); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes(target, _meshConverter); var bx = new SOG.SubDX() { diff --git a/Speckle.Connectors.sln b/Speckle.Connectors.sln index 56d1e3337..47c880b01 100644 --- a/Speckle.Connectors.sln +++ b/Speckle.Connectors.sln @@ -968,6 +968,7 @@ Global Connectors\Autocad\Speckle.Connectors.Civil3dShared\Speckle.Connectors.Civil3dShared.projitems*{4459f2b1-a340-488e-a856-eb2ae9c72ad4}*SharedItemsImports = 5 Converters\Revit\Speckle.Converters.RevitShared\Speckle.Converters.RevitShared.projitems*{4d40a101-07e6-4ff2-8934-83434932591d}*SharedItemsImports = 5 Converters\Tekla\Speckle.Converters.TeklaShared\Speckle.Converters.TeklaShared.projitems*{52666479-5401-47d6-b7ba-d554784439ea}*SharedItemsImports = 13 + Connectors\Rhino\Speckle.Connectors.RhinoShared\Speckle.Connectors.RhinoShared.projitems*{5422f2c8-1e00-4dae-bb01-65a17be8cd68}*SharedItemsImports = 5 Converters\Autocad\Speckle.Converters.AutocadShared\Speckle.Converters.AutocadShared.projitems*{5505b953-d434-49ce-8ebd-efd7b3c378f7}*SharedItemsImports = 5 Converters\Navisworks\Speckle.Converters.NavisworksShared\Speckle.Converters.NavisworksShared.projitems*{56680ea7-3599-4d88-83a5-b43ba93ac046}*SharedItemsImports = 5 Converters\Rhino\Speckle.Converters.RhinoShared\Speckle.Converters.RhinoShared.projitems*{56a909ae-6e99-4d4d-a22e-38bdc5528b8e}*SharedItemsImports = 5 From 2e52409db60a66fb78b7f070ffd9146a3a017d4e Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Tue, 19 Aug 2025 14:31:21 +0100 Subject: [PATCH 05/26] Update DisplayMeshExtractor.cs --- .../ToSpeckle/Meshing/DisplayMeshExtractor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs index c7d5586d0..21af6996a 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs @@ -54,7 +54,6 @@ public static class DisplayMeshExtractor // declare "renderMeshes" as a separate var, because it needs to be checked for null after each Mesh.Create method RG.Mesh[] renderMeshes; var joinedMesh = new RG.Mesh(); - switch (geometry) { case RG.Brep brep: From bc0fe17d087cc06c0de2ae0b6a1656d691428a8a Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Tue, 19 Aug 2025 14:38:33 +0100 Subject: [PATCH 06/26] Update MeshToSpeckleConverter.cs --- .../ToSpeckle/Raw/MeshToSpeckleConverter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs index 49d0f7f4f..e72a41464 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/MeshToSpeckleConverter.cs @@ -43,7 +43,6 @@ public class MeshToSpeckleConverter : ITypedConverter private double[] ConvertDoublePrecisionVertices(RG.Mesh target) { var vertexCoordinates = new double[target.Vertices.Count * 3]; - // target.Vertices.UseDoublePrecisionVertices = true; RG.Point3d[] vertices = target.Vertices.ToPoint3dArray(); var x = 0; for (int i = 0; i < vertices.Length; i++) From a0ce883a3fca628068cc62bfc3308b43b4b52068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Tue, 19 Aug 2025 15:48:25 +0200 Subject: [PATCH 07/26] feat: `DeconstructSpeckleParam` input to accept multiple objects --- .../Components/Dev/DeconstructSpeckleParam.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs index 393cba797..9f6873ccf 100644 --- a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs +++ b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs @@ -30,7 +30,7 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon protected override void RegisterInputParams(GH_InputParamManager pManager) { - pManager.AddGenericParameter("Speckle Param", "SP", "Speckle param to deconstruct", GH_ParamAccess.item); + pManager.AddGenericParameter("Speckle Param", "SP", "Speckle param(s) to deconstruct", GH_ParamAccess.list); } protected override void RegisterOutputParams(GH_OutputParamManager pManager) { } From 36863efc5abe2565544ea7d1099e871dfafd2bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Tue, 19 Aug 2025 16:01:41 +0200 Subject: [PATCH 08/26] refactor: update `SolveInstance` to collect multiple input objects --- .../Components/Dev/DeconstructSpeckleParam.cs | 104 ++++++++++-------- 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs index 9f6873ccf..554c69cdd 100644 --- a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs +++ b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs @@ -37,59 +37,71 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon protected override void SolveInstance(IGH_DataAccess da) { - object data = new(); - da.GetData(0, ref data); + List inputData = new(); + if (!da.GetDataList(0, inputData) || inputData.Count == 0) + { + AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "No objects provided to deconstruct."); + return; + } List outputParams = new(); - switch (data) + // Process each input object to collect all unique fields + foreach (object data in inputData) { - case SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null: - // get children elements from the wrapper to override the elements prop while parsing - List children = collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList(); - outputParams = ParseSpeckleWrapper(collectionGoo.Value, children); - break; - case SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null: - // get geometries from the wrapper to override the displayvalue prop while parsing - List display = dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList(); - outputParams = ParseSpeckleWrapper(dataObjectGoo.Value, null, display); - break; - case SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null: - outputParams = ParseSpeckleWrapper(objectGoo.Value); - break; - case SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null: - outputParams = ParseSpeckleWrapper(blockInstanceGoo.Value); - break; - case SpeckleBlockDefinitionWrapperGoo blockDef: - outputParams = ParseSpeckleWrapper(blockDef.Value); - break; - case SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null: - outputParams = ParseSpeckleWrapper(materialGoo.Value); - break; + switch (data) + { + case SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null: + // get children elements from the wrapper to override the elements prop while parsing + List children = collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList(); + outputParams = ParseSpeckleWrapper(collectionGoo.Value, children); + break; + case SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null: + // get geometries from the wrapper to override the displayvalue prop while parsing + List display = dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList(); + outputParams = ParseSpeckleWrapper(dataObjectGoo.Value, null, display); + break; + case SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null: + outputParams = ParseSpeckleWrapper(objectGoo.Value); + break; + case SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null: + outputParams = ParseSpeckleWrapper(blockInstanceGoo.Value); + break; + case SpeckleBlockDefinitionWrapperGoo blockDef: + outputParams = ParseSpeckleWrapper(blockDef.Value); + break; + case SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null: + outputParams = ParseSpeckleWrapper(materialGoo.Value); + break; - case SpecklePropertyGroupGoo propGoo: - Name = $"properties ({propGoo.Value.Count})"; - outputParams = new(); - foreach (var key in propGoo.Value.Keys) - { - ISpecklePropertyGoo value = propGoo.Value[key]; - object? outputValue = value is SpecklePropertyGoo prop - ? prop.Value - : value is SpecklePropertyGroupGoo propGroup - ? propGroup - : value; + case SpecklePropertyGroupGoo propGoo: + Name = $"properties ({propGoo.Value.Count})"; + outputParams = new(); + foreach (var key in propGoo.Value.Keys) + { + ISpecklePropertyGoo value = propGoo.Value[key]; + object? outputValue = value is SpecklePropertyGoo prop + ? prop.Value + : value is SpecklePropertyGroupGoo propGroup + ? propGroup + : value; - OutputParamWrapper output = - outputValue is IList - ? CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.list) - : CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item); - outputParams.Add(output); - } - break; + OutputParamWrapper output = + outputValue is IList + ? CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.list) + : CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item); + outputParams.Add(output); + } + break; - default: - AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}"); - return; + default: + AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}"); + return; + } + + // For now, we're still only processing the first object + // In the next step, we'll collect fields from ALL objects + break; } NickName = Name; From b9f4845fa74951593da5052582729a5bb4a48486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Tue, 19 Aug 2025 16:49:20 +0200 Subject: [PATCH 09/26] feat: handle multiple inputs --- .../Components/Dev/DeconstructSpeckleParam.cs | 177 +++++++++++------- 1 file changed, 113 insertions(+), 64 deletions(-) diff --git a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs index 554c69cdd..0241a554d 100644 --- a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs +++ b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs @@ -35,6 +35,9 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon protected override void RegisterOutputParams(GH_OutputParamManager pManager) { } + /// + /// Processes multiple objects and creates unified output parameters containing all unique fields from all input objects. + /// protected override void SolveInstance(IGH_DataAccess da) { List inputData = new(); @@ -44,98 +47,144 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon return; } - List outputParams = new(); + // collect all unique field names from all objects + HashSet allFieldNames = new(); + List> allObjectOutputs = new(); - // Process each input object to collect all unique fields + // process each input object to collect its fields foreach (object data in inputData) { - switch (data) + List? objectOutputs = DeconstructObject(data); + if (objectOutputs == null) { - case SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null: - // get children elements from the wrapper to override the elements prop while parsing - List children = collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList(); - outputParams = ParseSpeckleWrapper(collectionGoo.Value, children); - break; - case SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null: - // get geometries from the wrapper to override the displayvalue prop while parsing - List display = dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList(); - outputParams = ParseSpeckleWrapper(dataObjectGoo.Value, null, display); - break; - case SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null: - outputParams = ParseSpeckleWrapper(objectGoo.Value); - break; - case SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null: - outputParams = ParseSpeckleWrapper(blockInstanceGoo.Value); - break; - case SpeckleBlockDefinitionWrapperGoo blockDef: - outputParams = ParseSpeckleWrapper(blockDef.Value); - break; - case SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null: - outputParams = ParseSpeckleWrapper(materialGoo.Value); - break; - - case SpecklePropertyGroupGoo propGoo: - Name = $"properties ({propGoo.Value.Count})"; - outputParams = new(); - foreach (var key in propGoo.Value.Keys) - { - ISpecklePropertyGoo value = propGoo.Value[key]; - object? outputValue = value is SpecklePropertyGoo prop - ? prop.Value - : value is SpecklePropertyGroupGoo propGroup - ? propGroup - : value; - - OutputParamWrapper output = - outputValue is IList - ? CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.list) - : CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item); - outputParams.Add(output); - } - break; - - default: - AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}"); - return; + return; } - // For now, we're still only processing the first object - // In the next step, we'll collect fields from ALL objects - break; + allObjectOutputs.Add(objectOutputs); + foreach (var output in objectOutputs) + { + allFieldNames.Add(output.Param.Name); + } } + // create unified output parameters from all unique fields + List finalOutputParams = CreateUnifiedOutputs(allFieldNames, allObjectOutputs); + + // update component name depending on input + Name = inputData.Count == 1 ? Name : $"Multiple Objects ({inputData.Count})"; NickName = Name; - if (da.Iteration == 0 && OutputMismatch(outputParams)) + if (OutputMismatch(finalOutputParams)) { OnPingDocument() .ScheduleSolution( 5, _ => { - CreateOutputs(outputParams); + CreateOutputs(finalOutputParams); } ); } else { - for (int i = 0; i < outputParams.Count; i++) + for (int i = 0; i < finalOutputParams.Count; i++) { - var outParam = Params.Output[i]; - var outParamWrapper = outputParams[i]; - switch (outParam.Access) + var outParamWrapper = finalOutputParams[i]; + if (outParamWrapper.Value is IList list) { - case GH_ParamAccess.item: - da.SetData(i, outParamWrapper.Value); - break; - case GH_ParamAccess.list: - da.SetDataList(i, outParamWrapper.Value as IList); - break; + da.SetDataList(i, list); + } + else + { + da.SetDataList(i, new List { outParamWrapper.Value }); } } } } + /// + /// Deconstructs a single object into its constituent fields/properties. + /// + private List? DeconstructObject(object data) + { + switch (data) + { + case SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null: + var children = collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList(); + return ParseSpeckleWrapper(collectionGoo.Value, children); + + case SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null: + var display = dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList(); + return ParseSpeckleWrapper(dataObjectGoo.Value, null, display); + + case SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null: + return ParseSpeckleWrapper(objectGoo.Value); + + case SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null: + return ParseSpeckleWrapper(blockInstanceGoo.Value); + + case SpeckleBlockDefinitionWrapperGoo blockDef: + return ParseSpeckleWrapper(blockDef.Value); + + case SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null: + return ParseSpeckleWrapper(materialGoo.Value); + + case SpecklePropertyGroupGoo propGoo: + Name = $"properties ({propGoo.Value.Count})"; + List objectOutputs = new(); + foreach (var key in propGoo.Value.Keys) + { + ISpecklePropertyGoo value = propGoo.Value[key]; + object? outputValue = value is SpecklePropertyGoo prop + ? prop.Value + : value is SpecklePropertyGroupGoo propGroup + ? propGroup + : value; + objectOutputs.Add(CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item)); + } + return objectOutputs; + + default: + AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}"); + return null; + } + } + + /// + /// Creates unified output parameters by collecting all unique field names from all input objects and creating + /// list-based outputs where missing fields are represented as null values. + /// + private List CreateUnifiedOutputs( + HashSet allFieldNames, + List> allObjectOutputs + ) + { + List finalOutputParams = new(); + + foreach (string fieldName in allFieldNames.OrderBy(x => x)) + { + List fieldValues = new(); + + foreach (var objectOutputs in allObjectOutputs) + { + var fieldOutput = objectOutputs.FirstOrDefault(o => o.Param.Name == fieldName); + + if (fieldOutput?.Value is IList existingList && fieldOutput.Param.Access == GH_ParamAccess.list) + { + fieldValues.Add(existingList); + } + else + { + fieldValues.Add(fieldOutput?.Value); + } + } + + finalOutputParams.Add(CreateOutputParamByKeyValue(fieldName, fieldValues, GH_ParamAccess.list)); + } + + return finalOutputParams; + } + private List ParseSpeckleWrapper( SpeckleWrapper wrapper, List? elements = null, From 46e7d6e432109c0fcccdc47473d837fc7ffda0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Tue, 19 Aug 2025 17:04:12 +0200 Subject: [PATCH 10/26] chore: re-add comments --- .../Components/Dev/DeconstructSpeckleParam.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs index 0241a554d..89760f87b 100644 --- a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs +++ b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs @@ -110,10 +110,12 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon switch (data) { case SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null: + // get children elements from the wrapper to override the elements prop while parsing var children = collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList(); return ParseSpeckleWrapper(collectionGoo.Value, children); case SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null: + // get geometries from the wrapper to override the displayvalue prop while parsing var display = dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList(); return ParseSpeckleWrapper(dataObjectGoo.Value, null, display); From 9a6dda629b5dec0c375ba01921206495ef0dcd75 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Tue, 19 Aug 2025 16:32:03 +0100 Subject: [PATCH 11/26] also fixes an issue with sending low res meshes. uses current doc settings to convert display meshes for breps etc --- .../ToSpeckle/Meshing/DisplayMeshExtractor.cs | 38 ++++++------------- .../ToSpeckle/Raw/BrepToSpeckleConverter.cs | 6 ++- .../Raw/ExtrusionToSpeckleConverter.cs | 6 ++- .../ToSpeckle/Raw/HatchToSpeckleConverter.cs | 6 ++- .../ToSpeckle/Raw/SubDToSpeckleConverter.cs | 6 ++- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs index 21af6996a..d316f8900 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs @@ -1,3 +1,4 @@ +using Rhino; using Rhino.DocObjects; using Speckle.Converters.Common.Objects; using Speckle.Sdk.Common.Exceptions; @@ -6,7 +7,7 @@ namespace Speckle.Converters.Rhino.ToSpeckle.Meshing; public static class DisplayMeshExtractor { - public static RG.Mesh GetDisplayMesh(RhinoObject obj) + public static RG.Mesh GetDisplayMesh(RhinoObject obj, RhinoDoc doc) { // note: unsure this is nice, we get bigger meshes - we should to benchmark (conversion time vs size tradeoffs) var joinedMesh = new RG.Mesh(); @@ -21,15 +22,15 @@ public static class DisplayMeshExtractor switch (obj) { case BrepObject brep: - joinedMesh.Append(GetGeometryDisplayMesh(brep.BrepGeometry)); + joinedMesh.Append(GetGeometryDisplayMesh(brep.BrepGeometry, doc)); break; case ExtrusionObject extrusion: - joinedMesh.Append(GetGeometryDisplayMesh(extrusion.ExtrusionGeometry.ToBrep())); + joinedMesh.Append(GetGeometryDisplayMesh(extrusion.ExtrusionGeometry.ToBrep(), doc)); break; case SubDObject subDObject: if (subDObject.Geometry is RG.SubD subdGeometry) { - joinedMesh.Append(GetGeometryDisplayMesh(subdGeometry)); + joinedMesh.Append(GetGeometryDisplayMesh(subdGeometry, doc)); } else { @@ -47,17 +48,16 @@ public static class DisplayMeshExtractor /// /// Extracting Rhino Mesh from Rhino GeometryBase using specified MeshingParameters settings, e.g. minimumEdgeLength. /// - public static RG.Mesh GetGeometryDisplayMesh(RG.GeometryBase geometry, bool highPrecision = false) + public static RG.Mesh GetGeometryDisplayMesh(RG.GeometryBase geometry, RhinoDoc doc) { - double minEdgeLength = highPrecision ? GetAccurateMinEdgeLegth(geometry) : 0.05; - // declare "renderMeshes" as a separate var, because it needs to be checked for null after each Mesh.Create method RG.Mesh[] renderMeshes; var joinedMesh = new RG.Mesh(); + RG.MeshingParameters meshParams = RG.MeshingParameters.DocumentCurrentSetting(doc); switch (geometry) { case RG.Brep brep: - renderMeshes = RG.Mesh.CreateFromBrep(brep, new(0.05, minEdgeLength)); + renderMeshes = RG.Mesh.CreateFromBrep(brep, meshParams); break; case RG.SubD subd: #pragma warning disable CA2000 @@ -66,7 +66,7 @@ public static class DisplayMeshExtractor renderMeshes = [subdMesh]; break; case RG.Extrusion extrusion: - renderMeshes = RG.Mesh.CreateFromBrep(extrusion.ToBrep(), new(0.05, minEdgeLength)); + renderMeshes = RG.Mesh.CreateFromBrep(extrusion.ToBrep(), meshParams); break; default: throw new ConversionException($"Unsupported object for display mesh generation {geometry.GetType().FullName}"); @@ -89,31 +89,17 @@ public static class DisplayMeshExtractor return joinedMesh; } - /// - /// Calculating optimal meshing parameter 'minimumEdgeLength' for the given geometry. - /// - private static double GetAccurateMinEdgeLegth(RG.GeometryBase geometry) - { - // adjust meshing parameters if Brep edges are too close to the document tolerance - double minEdgeLength = 0.05; - if (geometry is RG.Brep brep && brep.Edges.Any(x => x.GetLength() < minEdgeLength)) - { - return 0; - } - - return minEdgeLength; - } - /// /// Extracting Rhino Mesh and converting to Speckle with the most suitable settings /// /// List of converted Speckle meshes public static List GetSpeckleMeshes( RG.GeometryBase geometry, - ITypedConverter meshConverter + ITypedConverter meshConverter, + RhinoDoc doc ) { - RG.Mesh displayMesh = GetGeometryDisplayMesh(geometry, true); + RG.Mesh displayMesh = GetGeometryDisplayMesh(geometry, doc); List displayValue = new() { meshConverter.Convert(displayMesh) }; return displayValue; } diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs index 8b7678c43..4cfe4b6c1 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs @@ -28,7 +28,11 @@ public class BrepToSpeckleConverter : ITypedConverter { var brepEncoding = RawEncodingCreator.Encode(target, _settingsStore.Current.Document); - List displayValue = DisplayMeshExtractor.GetSpeckleMeshes(target, _meshConverter); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( + target, + _meshConverter, + _settingsStore.Current.Document + ); var bx = new SOG.BrepX() { diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs index df9b7729c..c48440510 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs @@ -28,7 +28,11 @@ public class ExtrusionToSpeckleConverter : ITypedConverter displayValue = DisplayMeshExtractor.GetSpeckleMeshes(target, _meshConverter); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( + target, + _meshConverter, + _settingsStore.Current.Document + ); var bx = new SOG.ExtrusionX() { diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs index 9503218e7..5f9fc642f 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs @@ -39,7 +39,11 @@ public class HatchToSpeckleConverter : ITypedConverter // create display mesh from region by converting to brep first var brep = RG.Brep.TryConvertBrep(target); - List displayValue = DisplayMeshExtractor.GetSpeckleMeshes(brep, _meshConverter); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( + brep, + _meshConverter, + _settingsStore.Current.Document + ); return new SOG.Region { diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs index 7a4df1dcd..b4e66c247 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs @@ -28,7 +28,11 @@ public class SubDToSpeckleConverter : ITypedConverter { var subdEncoding = RawEncodingCreator.Encode(target, _settingsStore.Current.Document); - List displayValue = DisplayMeshExtractor.GetSpeckleMeshes(target, _meshConverter); + List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( + target, + _meshConverter, + _settingsStore.Current.Document + ); var bx = new SOG.SubDX() { From e487981e5b55df0316076308c4d570ef9b43d75a Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Wed, 20 Aug 2025 15:06:44 +0100 Subject: [PATCH 12/26] adds snapping for mesh, curve, and points --- .../LocalToGlobalToDirectShapeConverter.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs index cc424f8f0..20f86fd89 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs @@ -78,6 +78,27 @@ public class LocalToGlobalToDirectShapeConverter .DirectShapeLibrary.GetDirectShapeLibrary(_converterSettings.Current.Document) .FindDefinition(target.atomicObject.applicationId ?? target.atomicObject.id.NotNull()); result.SetShape(def); + + // add snapping references for valid geometry + foreach (var shape in def) + { + switch (shape) + { + case DB.Mesh m: + foreach (var v in m.Vertices) + { + result.AddReferencePoint(v); + } + break; + case DB.Curve c: + result.AddReferenceCurve(c); + break; + case DB.Point p: + result.AddReferencePoint(p.Coord); + break; + } + } + return result; // note fast exit here } From c3fa1bb0dce8e76db7b6a737d25906964cd1ec03 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Wed, 20 Aug 2025 15:32:12 +0100 Subject: [PATCH 13/26] Update LocalToGlobalToDirectShapeConverter.cs --- .../ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs index 20f86fd89..47eda99fc 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs @@ -79,7 +79,7 @@ public class LocalToGlobalToDirectShapeConverter .FindDefinition(target.atomicObject.applicationId ?? target.atomicObject.id.NotNull()); result.SetShape(def); - // add snapping references for valid geometry + // add snapping references for meshes and curves foreach (var shape in def) { switch (shape) @@ -93,9 +93,6 @@ public class LocalToGlobalToDirectShapeConverter case DB.Curve c: result.AddReferenceCurve(c); break; - case DB.Point p: - result.AddReferencePoint(p.Coord); - break; } } From abfdbdeffaeca0b36256d3f34d736dd2724f267f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:24:20 +0100 Subject: [PATCH 14/26] chore(deps): bump actions/checkout from 4 to 5 (#1034) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [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/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ed166bac7..336c0b5e8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -7,7 +7,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dfba74be7..671c0f3c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: file_version: ${{ steps.set-version.outputs.file_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 From a92b88f6d3d55eb1ef73c8250010d6485667fa69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinhagen?= Date: Thu, 21 Aug 2025 08:19:10 +0200 Subject: [PATCH 15/26] fix: replace list access with progressive field discovery in deconstruct component --- .../Components/Dev/DeconstructSpeckleParam.cs | 174 ++++++++---------- 1 file changed, 79 insertions(+), 95 deletions(-) diff --git a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs index 89760f87b..b8e58ea2f 100644 --- a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs +++ b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs @@ -16,6 +16,9 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Dev; [Guid("C491D26C-84CB-4684-8BD2-AA78D0F2FE53")] public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterComponent { + // Store all unique field names discovered across all iterations + private readonly HashSet _allDiscoveredFields = new(); + public DeconstructSpeckleParam() : base( "Deconstruct", @@ -30,81 +33,45 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon protected override void RegisterInputParams(GH_InputParamManager pManager) { - pManager.AddGenericParameter("Speckle Param", "SP", "Speckle param(s) to deconstruct", GH_ParamAccess.list); + pManager.AddGenericParameter("Speckle Param", "SP", "Speckle param to deconstruct", GH_ParamAccess.item); } protected override void RegisterOutputParams(GH_OutputParamManager pManager) { } - /// - /// Processes multiple objects and creates unified output parameters containing all unique fields from all input objects. - /// protected override void SolveInstance(IGH_DataAccess da) { - List inputData = new(); - if (!da.GetDataList(0, inputData) || inputData.Count == 0) + object data = new(); + da.GetData(0, ref data); + + List? outputParams = DeconstructObject(data); + if (outputParams == null) { - AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "No objects provided to deconstruct."); return; } - // collect all unique field names from all objects - HashSet allFieldNames = new(); - List> allObjectOutputs = new(); + // update our discovered fields set + var currentFields = outputParams.Select(p => p.Param.Name).ToHashSet(); + bool fieldsChanged = UpdateDiscoveredFields(currentFields); - // process each input object to collect its fields - foreach (object data in inputData) - { - List? objectOutputs = DeconstructObject(data); - if (objectOutputs == null) - { - return; - } - - allObjectOutputs.Add(objectOutputs); - foreach (var output in objectOutputs) - { - allFieldNames.Add(output.Param.Name); - } - } - - // create unified output parameters from all unique fields - List finalOutputParams = CreateUnifiedOutputs(allFieldNames, allObjectOutputs); - - // update component name depending on input - Name = inputData.Count == 1 ? Name : $"Multiple Objects ({inputData.Count})"; + // set component name based on the current object NickName = Name; - if (OutputMismatch(finalOutputParams)) + // if this is the first iteration OR field set has changed, check if we need to update outputs + if (da.Iteration == 0 || fieldsChanged) { - OnPingDocument() - .ScheduleSolution( - 5, - _ => - { - CreateOutputs(finalOutputParams); - } - ); - } - else - { - for (int i = 0; i < finalOutputParams.Count; i++) + var requiredOutputs = CreateOutputsFromAllFields(); + + if (OutputMismatch(requiredOutputs)) { - var outParamWrapper = finalOutputParams[i]; - if (outParamWrapper.Value is IList list) - { - da.SetDataList(i, list); - } - else - { - da.SetDataList(i, new List { outParamWrapper.Value }); - } + OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(requiredOutputs)); + return; } } + + // set output data - fill missing fields with nulls + SetOutputData(da, outputParams); } - /// - /// Deconstructs a single object into its constituent fields/properties. - /// private List? DeconstructObject(object data) { switch (data) @@ -152,39 +119,49 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon } } - /// - /// Creates unified output parameters by collecting all unique field names from all input objects and creating - /// list-based outputs where missing fields are represented as null values. - /// - private List CreateUnifiedOutputs( - HashSet allFieldNames, - List> allObjectOutputs - ) + private bool UpdateDiscoveredFields(HashSet currentFields) { - List finalOutputParams = new(); - - foreach (string fieldName in allFieldNames.OrderBy(x => x)) + bool changed = false; + foreach (string field in currentFields) { - List fieldValues = new(); - - foreach (var objectOutputs in allObjectOutputs) + if (_allDiscoveredFields.Add(field)) { - var fieldOutput = objectOutputs.FirstOrDefault(o => o.Param.Name == fieldName); - - if (fieldOutput?.Value is IList existingList && fieldOutput.Param.Access == GH_ParamAccess.list) - { - fieldValues.Add(existingList); - } - else - { - fieldValues.Add(fieldOutput?.Value); - } + changed = true; } - - finalOutputParams.Add(CreateOutputParamByKeyValue(fieldName, fieldValues, GH_ParamAccess.list)); } + return changed; + } - return finalOutputParams; + private List CreateOutputsFromAllFields() => + _allDiscoveredFields + .OrderBy(name => name) + .Select(fieldName => CreateOutputParamByKeyValue(fieldName, null, GH_ParamAccess.item)) + .ToList(); + + private void SetOutputData(IGH_DataAccess da, List currentOutputs) + { + // create a lookup for current outputs by field name + var outputLookup = currentOutputs.ToDictionary(o => o.Param.Name, o => o.Value); + + // set data for each output parameter + for (int i = 0; i < Params.Output.Count; i++) + { + var outputParam = Params.Output[i]; + string fieldName = outputParam.Name; + + // set the value if it exists, otherwise set null + object? value = outputLookup.TryGetValue(fieldName, out var fieldValue) ? fieldValue : null; + + switch (outputParam.Access) + { + case GH_ParamAccess.item: + da.SetData(i, value); + break; + case GH_ParamAccess.list: + da.SetDataList(i, value as IList ?? new List()); + break; + } + } } private List ParseSpeckleWrapper( @@ -212,7 +189,7 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon // cycle through base props foreach (var prop in @base.GetMembers(DynamicBaseMemberType.Instance | DynamicBaseMemberType.Dynamic)) { - // Convert and add to corresponding output structure + // convert and add to corresponding output structure var value = prop.Value; switch (value) { @@ -223,13 +200,13 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon case IList list: List nativeObjects = new(); - // override list value if base is a collection and this is the elements prop, since this is empty if coming from a collectionwrapper + // override list value if base is a collection and this is the elements prop if (@base is Collection && prop.Key == "elements" && elements != null) { list = elements; } - // override list value if base is a dataobject and this is the displayvalue prop, since this is empty if coming from a dataobject wrapper + // override list value if base is a dataobject and this is the displayvalue prop if (@base is Speckle.Objects.Data.DataObject && prop.Key == "displayValue" && displayValue != null) { list = displayValue; @@ -360,19 +337,17 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon return myParam; } - public bool DestroyParameter(GH_ParameterSide side, int index) - { - return side == GH_ParameterSide.Output; - } + public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Output; private void CreateOutputs(List outputParams) { - // TODO: better, nicer handling of creation/removal + // clear existing outputs while (Params.Output.Count > 0) { Params.UnregisterOutputParameter(Params.Output[^1]); } + // create new outputs foreach (var newParam in outputParams) { var param = new Param_GenericObject @@ -397,10 +372,10 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon return true; } - var count = 0; - foreach (var newParam in outputParams) + for (int i = 0; i < outputParams.Count; i++) { - var oldParam = Params.Output[count]; + var newParam = outputParams[i]; + var oldParam = Params.Output[i]; if ( oldParam.NickName != newParam.Param.NickName || oldParam.Name != newParam.Param.Name @@ -409,11 +384,20 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon { return true; } - count++; } return false; } + + // NOTE: Override ExpirePreview to reset discovered fields when component is expired + public override void ExpirePreview(bool recompute) + { + if (recompute) + { + _allDiscoveredFields.Clear(); + } + base.ExpirePreview(recompute); + } } public record OutputParamWrapper(Param_GenericObject Param, object? Value); From cd6888868e999944f4423d47ecd50282bad47fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= Date: Thu, 21 Aug 2025 10:10:07 +0200 Subject: [PATCH 16/26] fix: flickering, dynamic outputs and docstrings --- .../Components/Dev/DeconstructSpeckleParam.cs | 437 ++++++++++-------- 1 file changed, 247 insertions(+), 190 deletions(-) diff --git a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs index b8e58ea2f..e44dfd898 100644 --- a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs +++ b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs @@ -16,9 +16,6 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Dev; [Guid("C491D26C-84CB-4684-8BD2-AA78D0F2FE53")] public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterComponent { - // Store all unique field names discovered across all iterations - private readonly HashSet _allDiscoveredFields = new(); - public DeconstructSpeckleParam() : base( "Deconstruct", @@ -40,106 +37,152 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon protected override void SolveInstance(IGH_DataAccess da) { - object data = new(); - da.GetData(0, ref data); + // on first iteration, discover all fields from all objects to create stable output structure + if (da.Iteration == 0) + { + HashSet allFields = DiscoverAllFieldsFromInput(); - List? outputParams = DeconstructObject(data); + if (allFields.Count > 0) + { + var requiredOutputs = CreateOutputParamsFromFieldNames(allFields); + + if (OutputMismatch(requiredOutputs)) + { + OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(requiredOutputs)); + return; + } + } + } + + // process current object normally + object data = new(); + if (!da.GetData(0, ref data)) + { + return; + } + + var outputParams = DeconstructObject(data); if (outputParams == null) { return; } - // update our discovered fields set - var currentFields = outputParams.Select(p => p.Param.Name).ToHashSet(); - bool fieldsChanged = UpdateDiscoveredFields(currentFields); - // set component name based on the current object NickName = Name; - // if this is the first iteration OR field set has changed, check if we need to update outputs - if (da.Iteration == 0 || fieldsChanged) - { - var requiredOutputs = CreateOutputsFromAllFields(); - - if (OutputMismatch(requiredOutputs)) - { - OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(requiredOutputs)); - return; - } - } - - // set output data - fill missing fields with nulls + // set output data - fill missing fields with nulls for objects that don't have all fields SetOutputData(da, outputParams); } - private List? DeconstructObject(object data) + /// + /// Discovers all unique field names from all input objects by looking at volatile data directly. + /// + private HashSet DiscoverAllFieldsFromInput() { - switch (data) + HashSet allFields = new(); + + foreach (var item in Params.Input[0].VolatileData.AllData(true)) { - case SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null: - // get children elements from the wrapper to override the elements prop while parsing - var children = collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList(); - return ParseSpeckleWrapper(collectionGoo.Value, children); - - case SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null: - // get geometries from the wrapper to override the displayvalue prop while parsing - var display = dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList(); - return ParseSpeckleWrapper(dataObjectGoo.Value, null, display); - - case SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null: - return ParseSpeckleWrapper(objectGoo.Value); - - case SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null: - return ParseSpeckleWrapper(blockInstanceGoo.Value); - - case SpeckleBlockDefinitionWrapperGoo blockDef: - return ParseSpeckleWrapper(blockDef.Value); - - case SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null: - return ParseSpeckleWrapper(materialGoo.Value); - - case SpecklePropertyGroupGoo propGoo: - Name = $"properties ({propGoo.Value.Count})"; - List objectOutputs = new(); - foreach (var key in propGoo.Value.Keys) - { - ISpecklePropertyGoo value = propGoo.Value[key]; - object? outputValue = value is SpecklePropertyGoo prop - ? prop.Value - : value is SpecklePropertyGroupGoo propGroup - ? propGroup - : value; - objectOutputs.Add(CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item)); - } - return objectOutputs; - - default: - AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}"); - return null; - } - } - - private bool UpdateDiscoveredFields(HashSet currentFields) - { - bool changed = false; - foreach (string field in currentFields) - { - if (_allDiscoveredFields.Add(field)) + var objectOutputs = DeconstructObject(item); + if (objectOutputs != null) { - changed = true; + foreach (var output in objectOutputs) + { + allFields.Add(output.Param.Name); + } } } - return changed; + + return allFields; } - private List CreateOutputsFromAllFields() => - _allDiscoveredFields + /// + /// Creates output parameter wrappers from a set of field names, all with item access. + /// + private List CreateOutputParamsFromFieldNames(HashSet fieldNames) => + fieldNames .OrderBy(name => name) .Select(fieldName => CreateOutputParamByKeyValue(fieldName, null, GH_ParamAccess.item)) .ToList(); + /// + /// Deconstructs a single object into its constituent fields/properties. + /// + private List? DeconstructObject(object data) => + data switch + { + // get children elements from wrapper to override elements prop while parsing + SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null + => ParseSpeckleWrapper( + collectionGoo.Value, + collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList() + ), + + // get geometries from wrapper to override displayValue prop while parsing + SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null + => ParseSpeckleWrapper( + dataObjectGoo.Value, + null, + dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList() + ), + + SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null => ParseSpeckleWrapper(objectGoo.Value), + + SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null + => ParseSpeckleWrapper(blockInstanceGoo.Value), + + SpeckleBlockDefinitionWrapperGoo blockDef when blockDef.Value != null => ParseSpeckleWrapper(blockDef.Value), + + SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null => ParseSpeckleWrapper(materialGoo.Value), + + SpecklePropertyGroupGoo propGoo when propGoo.Value != null => ParsePropertyGroup(propGoo), + + _ => HandleUnsupportedType(data) + }; + + /// + /// Handles SpecklePropertyGroupGoo objects by extracting their key-value pairs. + /// + private List ParsePropertyGroup(SpecklePropertyGroupGoo propGoo) + { + Name = $"properties ({propGoo.Value.Count})"; + List objectOutputs = new(); + + foreach (var key in propGoo.Value.Keys) + { + ISpecklePropertyGoo value = propGoo.Value[key]; + object? outputValue = value switch + { + SpecklePropertyGoo prop => prop.Value, + SpecklePropertyGroupGoo propGroup => propGroup, + _ => value + }; + objectOutputs.Add(CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item)); + } + + return objectOutputs; + } + + /// + /// Handles unsupported object types by logging an error and returning null. + /// + private List? HandleUnsupportedType(object data) + { + AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}"); + return null; + } + + /// + /// Sets output data for the current iteration, filling missing fields with null values. + /// Uses a lookup dictionary for efficient field matching. + /// private void SetOutputData(IGH_DataAccess da, List currentOutputs) { + if (Params.Output.Count == 0) + { + return; + } + // create a lookup for current outputs by field name var outputLookup = currentOutputs.ToDictionary(o => o.Param.Name, o => o.Value); @@ -147,10 +190,9 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon for (int i = 0; i < Params.Output.Count; i++) { var outputParam = Params.Output[i]; - string fieldName = outputParam.Name; // set the value if it exists, otherwise set null - object? value = outputLookup.TryGetValue(fieldName, out var fieldValue) ? fieldValue : null; + object? value = outputLookup.TryGetValue(outputParam.Name, out var fieldValue) ? fieldValue : null; switch (outputParam.Access) { @@ -186,125 +228,146 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon return result; } - // cycle through base props + // process each property of the Base object foreach (var prop in @base.GetMembers(DynamicBaseMemberType.Instance | DynamicBaseMemberType.Dynamic)) { - // convert and add to corresponding output structure - var value = prop.Value; - switch (value) + // skip internal dynamic property keys + if (prop.Key == nameof(Base.DynamicPropertyKeys)) { - case null: - result.Add(CreateOutputParamByKeyValue(prop.Key, null, GH_ParamAccess.item)); - break; + continue; + } - case IList list: - List nativeObjects = new(); - - // override list value if base is a collection and this is the elements prop - if (@base is Collection && prop.Key == "elements" && elements != null) - { - list = elements; - } - - // override list value if base is a dataobject and this is the displayvalue prop - if (@base is Speckle.Objects.Data.DataObject && prop.Key == "displayValue" && displayValue != null) - { - list = displayValue; - } - - foreach (var x in list) - { - switch (x) - { - case SpeckleWrapper wrapper: - nativeObjects.Add(wrapper.CreateGoo()); - break; - - case Base xBase: - nativeObjects.AddRange(ConvertOrCreateWrapper(xBase)); - break; - - default: - nativeObjects.Add(x); - break; - } - } - - result.Add(CreateOutputParamByKeyValue(prop.Key, nativeObjects, GH_ParamAccess.list)); - break; - - case Dictionary dict: // this should be treated a properties dict - SpecklePropertyGroupGoo propertyGoo = new(); - propertyGoo.CastFrom(dict); - result.Add(CreateOutputParamByKeyValue(prop.Key, propertyGoo, GH_ParamAccess.item)); - break; - - case SpeckleWrapper wrapper: - result.Add(CreateOutputParamByKeyValue(prop.Key, wrapper.CreateGoo(), GH_ParamAccess.item)); - break; - - case Base baseValue: - result.Add(CreateOutputParamByKeyValue(prop.Key, ConvertOrCreateWrapper(baseValue), GH_ParamAccess.list)); - break; - - default: - // we don't want to output dynamic property keys - if (prop.Key == nameof(Base.DynamicPropertyKeys)) - { - continue; - } - - result.Add(CreateOutputParamByKeyValue(prop.Key, prop.Value, GH_ParamAccess.item)); - break; + var outputParam = CreateOutputParamForProperty(prop, @base, elements, displayValue); + if (outputParam != null) + { + result.Add(outputParam); } } return result; } + /// + /// Creates an output parameter for a single property, handling different value types appropriately. + /// + private OutputParamWrapper CreateOutputParamForProperty( + KeyValuePair prop, + Base @base, + List? elements, + List? displayValue + ) => + prop.Value switch + { + null => CreateOutputParamByKeyValue(prop.Key, null, GH_ParamAccess.item), + IList list => CreateListOutputParam(prop.Key, list, @base, elements, displayValue), + Dictionary dict => CreateDictionaryOutputParam(prop.Key, dict), + SpeckleWrapper wrapper => CreateOutputParamByKeyValue(prop.Key, wrapper.CreateGoo(), GH_ParamAccess.item), + Base baseValue => CreateOutputParamByKeyValue(prop.Key, ConvertOrCreateWrapper(baseValue), GH_ParamAccess.list), + _ => CreateOutputParamByKeyValue(prop.Key, prop.Value, GH_ParamAccess.item) + }; + + /// + /// Creates an output parameter for list properties, with special handling for collection elements and display values. + /// + private OutputParamWrapper CreateListOutputParam( + string key, + IList list, + Base @base, + List? elements, + List? displayValue + ) + { + // override list value for special cases + IList actualList = key switch + { + "elements" when @base is Collection && elements != null => elements, + "displayValue" when @base is Speckle.Objects.Data.DataObject && displayValue != null => displayValue, + _ => list + }; + + List nativeObjects = new(); + foreach (var item in actualList) + { + switch (item) + { + case SpeckleWrapper wrapper: + nativeObjects.Add(wrapper.CreateGoo()); + break; + case Base baseItem: + nativeObjects.AddRange(ConvertOrCreateWrapper(baseItem)); + break; + default: + nativeObjects.Add(item); + break; + } + } + + return CreateOutputParamByKeyValue(key, nativeObjects, GH_ParamAccess.list); + } + + /// + /// Creates an output parameter for dictionary properties, converting them to SpecklePropertyGroupGoo. + /// + private OutputParamWrapper CreateDictionaryOutputParam(string key, Dictionary dict) + { + SpecklePropertyGroupGoo propertyGoo = new(); + propertyGoo.CastFrom(dict); + return CreateOutputParamByKeyValue(key, propertyGoo, GH_ParamAccess.item); + } + + /// + /// Converts a Speckle Base object to host geometry or creates a wrapper if conversion fails. + /// Returns a list of SpeckleGeometryWrapperGoo objects. + /// private List ConvertOrCreateWrapper(Base @base) { try { - // convert the base and create a wrapper for each result + // attempt conversion to host geometry List<(object, Base)> convertedBase = SpeckleConversionContext.Current.ConvertToHost(@base); - List convertedWrappers = new(); - foreach ((object o, Base b) in convertedBase) - { - GeometryBase? g = o as GeometryBase; - SpeckleGeometryWrapper convertedWrapper = - new() - { - Base = b, - GeometryBase = g, - Name = b["name"] as string ?? "", - Color = null, - Material = null - }; - - convertedWrappers.Add(new(convertedWrapper)); - } - - return convertedWrappers; + return convertedBase.Select(CreateGeometryWrapper).ToList(); } catch (ConversionException) { - // some classes, like RawEncoding, have no direct conversion or fallback value. - // when this is the case, wrap it to allow users to further expand the object. - SpeckleGeometryWrapper convertedWrapper = - new() - { - Base = @base, - GeometryBase = null, - Name = @base[Constants.NAME_PROP] as string ?? "", - Color = null, - Material = null - }; - - return new() { new SpeckleGeometryWrapperGoo(convertedWrapper) }; + // fallback: create wrapper without conversion for objects that can't be converted + return new List { CreateFallbackWrapper(@base) }; } } + /// + /// Creates a SpeckleGeometryWrapperGoo from a converted geometry and base object pair. + /// + private SpeckleGeometryWrapperGoo CreateGeometryWrapper((object geometry, Base @base) converted) + { + SpeckleGeometryWrapper wrapper = + new() + { + Base = converted.@base, + GeometryBase = converted.geometry as GeometryBase, + Name = converted.@base["name"] as string ?? "", + Color = null, + Material = null + }; + return new SpeckleGeometryWrapperGoo(wrapper); + } + + /// + /// Creates a fallback wrapper for Base objects that cannot be converted to host geometry. + /// + private SpeckleGeometryWrapperGoo CreateFallbackWrapper(Base @base) + { + SpeckleGeometryWrapper wrapper = + new() + { + Base = @base, + GeometryBase = null, + Name = @base[Constants.NAME_PROP] as string ?? "", + Color = null, + Material = null + }; + return new SpeckleGeometryWrapperGoo(wrapper); + } + private OutputParamWrapper CreateOutputParamByKeyValue(string key, object? value, GH_ParamAccess access) { Param_GenericObject param = @@ -341,13 +404,13 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon private void CreateOutputs(List outputParams) { - // clear existing outputs + // remove all existing output parameters while (Params.Output.Count > 0) { Params.UnregisterOutputParameter(Params.Output[^1]); } - // create new outputs + // add new output parameters foreach (var newParam in outputParams) { var param = new Param_GenericObject @@ -360,11 +423,15 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon Params.RegisterOutputParam(param); } + // notify Grasshopper of parameter changes Params.OnParametersChanged(); VariableParameterMaintenance(); ExpireSolution(false); } + /// + /// Determines if the current output parameter structure differs from the required structure. + /// private bool OutputMismatch(List outputParams) { if (Params.Output.Count != outputParams.Count) @@ -388,16 +455,6 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon return false; } - - // NOTE: Override ExpirePreview to reset discovered fields when component is expired - public override void ExpirePreview(bool recompute) - { - if (recompute) - { - _allDiscoveredFields.Clear(); - } - base.ExpirePreview(recompute); - } } public record OutputParamWrapper(Param_GenericObject Param, object? Value); From 4fba12f9663aa085d7e46e24bafff67bc384c5f7 Mon Sep 17 00:00:00 2001 From: bimgeek Date: Thu, 21 Aug 2025 19:47:52 +0300 Subject: [PATCH 17/26] add area scheme switch statement --- .../ToSpeckle/Properties/ClassPropertiesExtractor.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs index 219fe909b..09901cbb3 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs @@ -24,13 +24,17 @@ public class ClassPropertiesExtractor // add type specific props not included in parameters. // so far, no extra props are needed - /* + switch (element) { + case DB.Area area: + elementPropertiesDict.Add("Area Scheme", area.AreaScheme?.Name ?? ""); + break; + default: break; } - */ + return elementPropertiesDict; } From 795d068175ff6be651b92b3accdf3b76e62889cc Mon Sep 17 00:00:00 2001 From: bimgeek Date: Fri, 22 Aug 2025 14:57:43 +0300 Subject: [PATCH 18/26] exclude parts from view filter --- .../Send/Filters/RevitViewsFilter.cs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs index b3b0b8599..2fdf7db72 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs @@ -2,7 +2,9 @@ using Autodesk.Revit.DB; using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.Utils; +using Speckle.Converters.RevitShared.Extensions; using Speckle.Converters.RevitShared.Helpers; +using Speckle.Sdk; namespace Speckle.Connectors.RevitShared.Operations.Send.Filters; @@ -75,8 +77,23 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt //this used to throw an exception, but we don't want to fail loudly if the view is not found return []; } + + IEnumerable elementsInView; + using var viewCollector = new FilteredElementCollector(_doc, view.Id); - var elementsInView = viewCollector.ToElements(); + + if (view.PartsVisibility == PartsVisibility.ShowPartsOnly) + { + + var allElements = viewCollector.ToElements(); + var idsToExclude = GetSourceElementIdsToExclude(allElements); + + elementsInView = viewCollector.Excluding(idsToExclude).ToElements(); + } + else + { + elementsInView = viewCollector.ToElements(); + } // NOTE: FilteredElementCollector() includes sweeps and reveals from a wall family's definition and includes them as additional objects // on this return. displayValue for Wall already includes these, therefore we end up with duplicate elements on wall sweeps @@ -125,4 +142,37 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt _revitContext = revitContext; _doc = _revitContext.UIApplication?.ActiveUIDocument.Document; } + + // NOTE: Element collector returns parts and source elements even when Parts Visibility is set as "Show Parts" only. + // Below function collects list of ids to exclude from final list. + private HashSet GetSourceElementIdsToExclude(IEnumerable elements) + { + var elementsToExclude = new HashSet(); + + foreach (var element in elements) + { + // check if element is a part + if (element.Category?.GetBuiltInCategory() == BuiltInCategory.OST_Parts && element is Part part) + { + try + { + // get source element ids from the part + var sourceIds = part.GetSourceElementIds(); + if (sourceIds != null) + { + foreach (var sourceId in sourceIds) + { + elementsToExclude.Add(sourceId.HostElementId); + } + } + } + catch (Exception e) when (!e.IsFatal()) + { + // silently continue processing other Parts if one fails + // this follows the pattern used elsewhere in the codebase + } + } + } + return elementsToExclude; + } } From 7c645e3c51f579fffba9904437ba8cfd25128e78 Mon Sep 17 00:00:00 2001 From: bimgeek Date: Fri, 22 Aug 2025 15:05:35 +0300 Subject: [PATCH 19/26] collector disposal --- .../Operations/Send/Filters/RevitViewsFilter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs index 2fdf7db72..3af122dd2 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs @@ -79,19 +79,19 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt } IEnumerable elementsInView; - - using var viewCollector = new FilteredElementCollector(_doc, view.Id); if (view.PartsVisibility == PartsVisibility.ShowPartsOnly) { - - var allElements = viewCollector.ToElements(); + using var initialCollector = new FilteredElementCollector(_doc, view.Id); + var allElements = initialCollector.ToElements(); var idsToExclude = GetSourceElementIdsToExclude(allElements); + using var viewCollector = new FilteredElementCollector(_doc, view.Id); elementsInView = viewCollector.Excluding(idsToExclude).ToElements(); } else { + using var viewCollector = new FilteredElementCollector(_doc, view.Id); elementsInView = viewCollector.ToElements(); } From 15425c5328f39819019f17262598f7bbfa03d700 Mon Sep 17 00:00:00 2001 From: bimgeek Date: Fri, 22 Aug 2025 15:12:33 +0300 Subject: [PATCH 20/26] no need for 2 db queries --- .../Operations/Send/Filters/RevitViewsFilter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs index 3af122dd2..749dcbd38 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs @@ -86,8 +86,7 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt var allElements = initialCollector.ToElements(); var idsToExclude = GetSourceElementIdsToExclude(allElements); - using var viewCollector = new FilteredElementCollector(_doc, view.Id); - elementsInView = viewCollector.Excluding(idsToExclude).ToElements(); + elementsInView = allElements.Where(e => !idsToExclude.Contains(e.Id)); } else { From 62a0cb895d063914e04e0f37614d4a0cce8049f2 Mon Sep 17 00:00:00 2001 From: bimgeek Date: Sun, 24 Aug 2025 17:56:21 +0300 Subject: [PATCH 21/26] pasha bjorns comments --- .../Send/Filters/RevitViewsFilter.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs index 749dcbd38..a9b0a1e09 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs @@ -78,21 +78,7 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt return []; } - IEnumerable elementsInView; - - if (view.PartsVisibility == PartsVisibility.ShowPartsOnly) - { - using var initialCollector = new FilteredElementCollector(_doc, view.Id); - var allElements = initialCollector.ToElements(); - var idsToExclude = GetSourceElementIdsToExclude(allElements); - - elementsInView = allElements.Where(e => !idsToExclude.Contains(e.Id)); - } - else - { - using var viewCollector = new FilteredElementCollector(_doc, view.Id); - elementsInView = viewCollector.ToElements(); - } + IEnumerable elementsInView = GetFilteredElementsForView(view); // NOTE: FilteredElementCollector() includes sweeps and reveals from a wall family's definition and includes them as additional objects // on this return. displayValue for Wall already includes these, therefore we end up with duplicate elements on wall sweeps @@ -141,7 +127,7 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt _revitContext = revitContext; _doc = _revitContext.UIApplication?.ActiveUIDocument.Document; } - + // NOTE: Element collector returns parts and source elements even when Parts Visibility is set as "Show Parts" only. // Below function collects list of ids to exclude from final list. private HashSet GetSourceElementIdsToExclude(IEnumerable elements) @@ -174,4 +160,19 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt } return elementsToExclude; } + + private IEnumerable GetFilteredElementsForView(View view) + { + using var viewCollector = new FilteredElementCollector(_doc, view.Id); + var allElements = viewCollector.ToElements(); + + // parts filtering when view is set to show Parts only (and overwrites allElements) + if (view.PartsVisibility == PartsVisibility.ShowPartsOnly) + { + var idsToExclude = GetSourceElementIdsToExclude(allElements); + return allElements.Where(e => !idsToExclude.Contains(e.Id)); + } + + return allElements; + } } From 7d0690f7a0df4681f5048bf21a5ae793123e6951 Mon Sep 17 00:00:00 2001 From: bimgeek Date: Sun, 24 Aug 2025 18:08:55 +0300 Subject: [PATCH 22/26] bjorn pasha asked for these changes --- .../ToSpeckle/Properties/ClassPropertiesExtractor.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs index 09901cbb3..22d7620f5 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs @@ -22,16 +22,13 @@ public class ClassPropertiesExtractor { Dictionary elementPropertiesDict = ExtractElementProperties(element); - // add type specific props not included in parameters. - // so far, no extra props are needed - + + // type specific properties switch (element) { + // area scheme for area elements case DB.Area area: - elementPropertiesDict.Add("Area Scheme", area.AreaScheme?.Name ?? ""); - break; - - default: + elementPropertiesDict.Add("areaScheme", area.AreaScheme?.Name); break; } From e1b5dea3f73d2af5fd11d7854b240e8b3dcce87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinhagen?= Date: Sun, 24 Aug 2025 17:15:29 +0200 Subject: [PATCH 23/26] fix: csharpier --- .../ToSpeckle/Properties/ClassPropertiesExtractor.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs index 22d7620f5..b28bab1e3 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs @@ -22,7 +22,6 @@ public class ClassPropertiesExtractor { Dictionary elementPropertiesDict = ExtractElementProperties(element); - // type specific properties switch (element) { @@ -31,7 +30,6 @@ public class ClassPropertiesExtractor elementPropertiesDict.Add("areaScheme", area.AreaScheme?.Name); break; } - return elementPropertiesDict; } From 378074799227602c8b7510a584b1351b92c5909a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Koral?= <45078678+oguzhankoral@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:54:50 +0300 Subject: [PATCH 24/26] Introduce global config (#1041) --- .../Bindings/ConfigBinding.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs b/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs index 9623bd428..a76a6ec73 100644 --- a/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs +++ b/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs @@ -115,6 +115,30 @@ public class ConfigBinding : IBinding } } + public GlobalConfig? GetGlobalConfig() + { + var rawConfig = _jsonCacheManager.GetObject("global"); + if (rawConfig is null) + { + return null; + } + + try + { + var config = _serializer.Deserialize(rawConfig); + if (config is null) + { + throw new SerializationException("Failed to deserialize global config"); + } + + return config; + } + catch (SerializationException) + { + return null; + } + } + public AccountsConfig? GetAccountsConfig() { var rawConfig = _jsonCacheManager.GetObject("accounts"); @@ -182,6 +206,11 @@ public class ConnectorConfig public bool DarkTheme { get; set; } = true; } +public class GlobalConfig +{ + public bool IsUpdateNotificationEnabled { get; set; } = true; +} + public class AccountsConfig { public string? UserSelectedAccountId { get; set; } From 4d9411de420e604d2f6c087f93f6266141df3b16 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 26 Aug 2025 16:31:48 +0100 Subject: [PATCH 25/26] fix(revit): Revit files persist model card data to a file like Tekla instead of into the file (#1045) * Revit files persist model card data to a file like Tekla instead of into the file * fmt * fixes logger * Update Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../HostApp/RevitDocumentStore.cs | 103 +++++------------- 1 file changed, 28 insertions(+), 75 deletions(-) diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs index 3ebd23788..3dd6d0405 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs @@ -3,48 +3,45 @@ using Autodesk.Revit.DB.ExtensibleStorage; using Autodesk.Revit.UI; using Autodesk.Revit.UI.Events; using Microsoft.Extensions.Logging; -using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bridge; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; using Speckle.Connectors.Revit.Plugin; using Speckle.Converters.RevitShared.Helpers; +using Speckle.Sdk; using Speckle.Sdk.Common; +using Speckle.Sdk.SQLite; namespace Speckle.Connectors.Revit.HostApp; // POC: should be interfaced out internal sealed class RevitDocumentStore : DocumentModelStore { - // POC: move to somewhere central? - private static readonly Guid s_revitDocumentStoreId = new("D35B3695-EDC9-4E15-B62A-D3FC2CB83FA3"); - + private readonly ILogger _logger; private readonly IAppIdleManager _idleManager; private readonly RevitContext _revitContext; private readonly DocumentModelStorageSchema _documentModelStorageSchema; - private readonly IdStorageSchema _idStorageSchema; private readonly ITopLevelExceptionHandler _topLevelExceptionHandler; - private readonly IThreadContext _threadContext; + private readonly ISqLiteJsonCacheManager _jsonCacheManager; public RevitDocumentStore( - ILogger logger, IAppIdleManager idleManager, RevitContext revitContext, IJsonSerializer jsonSerializer, DocumentModelStorageSchema documentModelStorageSchema, - IdStorageSchema idStorageSchema, ITopLevelExceptionHandler topLevelExceptionHandler, - IThreadContext threadContext, - IRevitTask revitTask + IRevitTask revitTask, + ISqLiteJsonCacheManagerFactory jsonCacheManagerFactory, + ILogger logger ) : base(logger, jsonSerializer) { + _jsonCacheManager = jsonCacheManagerFactory.CreateForUser("ConnectorsFileData"); _idleManager = idleManager; _revitContext = revitContext; _documentModelStorageSchema = documentModelStorageSchema; - _idStorageSchema = idStorageSchema; _topLevelExceptionHandler = topLevelExceptionHandler; - _threadContext = threadContext; + _logger = logger; UIApplication uiApplication = _revitContext.UIApplication.NotNull(); @@ -101,80 +98,36 @@ internal sealed class RevitDocumentStore : DocumentModelStore return; } - _threadContext - .RunOnMain(() => - { - //if not the same active document then don't save the current cards to a bad document! - if (!EnsureActiveDocumentIsSame(document)) - { - return; - } - using Transaction t = new(document, "Speckle Write State"); - t.Start(); - using DataStorage ds = GetSettingsDataStorage(document) ?? DataStorage.Create(document); - - using Entity stateEntity = new(_documentModelStorageSchema.GetSchema()); - string serializedModels = Serialize(); - stateEntity.Set("contents", serializedModels); - - using Entity idEntity = new(_idStorageSchema.GetSchema()); - idEntity.Set("Id", s_revitDocumentStoreId); - - ds.SetEntity(idEntity); - ds.SetEntity(stateEntity); - t.Commit(); - }) - .FireAndForget(); - } - - private bool EnsureActiveDocumentIsSame(Document document) - { - var localDoc = _revitContext.UIApplication?.ActiveUIDocument?.Document; - if (localDoc == null) + try { - return false; + var key = document.ProjectInformation.UniqueId.NotNull(); + _jsonCacheManager.UpdateObject(key, modelCardState); + } + catch (Exception ex) when (!ex.IsFatal()) + { + var key = document.ProjectInformation.UniqueId.NotNull(); + _logger.LogError(ex, "Failed to save model card state for document {DocumentId}", key); } - - return localDoc.Equals(document); } protected override void LoadState() { - var stateEntity = GetSpeckleEntity(_revitContext.UIApplication?.ActiveUIDocument?.Document); + var document = _revitContext.UIApplication?.ActiveUIDocument?.Document; + // POC: this can happen? A: Not really, imho (dim) (Adam seyz yes it can if loading also triggers a save) + if (document == null) + { + return; + } + + var stateEntity = GetSpeckleEntity(document); if (stateEntity == null || !stateEntity.IsValid()) { ClearAndSave(); return; } - - string modelsString = stateEntity.Get("contents"); - LoadFromString(modelsString); - } - - private DataStorage? GetSettingsDataStorage(Document doc) - { - using FilteredElementCollector collector = new(doc); - FilteredElementCollector dataStorages = collector.OfClass(typeof(DataStorage)); - - foreach (Element element in dataStorages) - { - DataStorage dataStorage = (DataStorage)element; - Entity settingIdEntity = dataStorage.GetEntity(_idStorageSchema.GetSchema()); - if (!settingIdEntity.IsValid()) - { - continue; - } - - Guid id = settingIdEntity.Get("Id"); - if (!id.Equals(s_revitDocumentStoreId)) - { - continue; - } - - return dataStorage; - } - - return null; + var key = document.ProjectInformation.UniqueId.NotNull(); + var state = _jsonCacheManager.GetObject(key); + LoadFromString(state); } private Entity? GetSpeckleEntity(Document? doc) From 79a5228899b96e79309ecd17c33895b2ee55aad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Koral?= <45078678+oguzhankoral@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:52:21 +0300 Subject: [PATCH 26/26] Fix: invert boolean flag (#1049) * Introduce global config * invert boolean flag --- DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs b/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs index a76a6ec73..aead51b4d 100644 --- a/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs +++ b/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs @@ -208,7 +208,7 @@ public class ConnectorConfig public class GlobalConfig { - public bool IsUpdateNotificationEnabled { get; set; } = true; + public bool IsUpdateNotificationDisabled { get; set; } } public class AccountsConfig