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 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) 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..a9b0a1e09 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,8 @@ 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 []; } - using var viewCollector = new FilteredElementCollector(_doc, view.Id); - var 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 @@ -125,4 +127,52 @@ 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; + } + + 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; + } } diff --git a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs index 393cba797..e44dfd898 100644 --- a/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs +++ b/Connectors/Rhino/Speckle.Connectors.GrasshopperShared/Components/Dev/DeconstructSpeckleParam.cs @@ -37,91 +37,173 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon protected override void SolveInstance(IGH_DataAccess da) { - object data = new(); - da.GetData(0, ref data); - - List outputParams = new(); - - switch (data) + // on first iteration, discover all fields from all objects to create stable output structure + if (da.Iteration == 0) { - 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; + HashSet allFields = DiscoverAllFieldsFromInput(); - case SpecklePropertyGroupGoo propGoo: - Name = $"properties ({propGoo.Value.Count})"; - outputParams = new(); - foreach (var key in propGoo.Value.Keys) + if (allFields.Count > 0) + { + var requiredOutputs = CreateOutputParamsFromFieldNames(allFields); + + if (OutputMismatch(requiredOutputs)) { - 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); + OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(requiredOutputs)); + return; } - break; - - default: - AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}"); - return; + } } + // process current object normally + object data = new(); + if (!da.GetData(0, ref data)) + { + return; + } + + var outputParams = DeconstructObject(data); + if (outputParams == null) + { + return; + } + + // set component name based on the current object NickName = Name; - if (da.Iteration == 0 && OutputMismatch(outputParams)) + // set output data - fill missing fields with nulls for objects that don't have all fields + SetOutputData(da, outputParams); + } + + /// + /// Discovers all unique field names from all input objects by looking at volatile data directly. + /// + private HashSet DiscoverAllFieldsFromInput() + { + HashSet allFields = new(); + + foreach (var item in Params.Input[0].VolatileData.AllData(true)) { - OnPingDocument() - .ScheduleSolution( - 5, - _ => - { - CreateOutputs(outputParams); - } - ); - } - else - { - for (int i = 0; i < outputParams.Count; i++) + var objectOutputs = DeconstructObject(item); + if (objectOutputs != null) { - var outParam = Params.Output[i]; - var outParamWrapper = outputParams[i]; - switch (outParam.Access) + foreach (var output in objectOutputs) { - case GH_ParamAccess.item: - da.SetData(i, outParamWrapper.Value); - break; - case GH_ParamAccess.list: - da.SetDataList(i, outParamWrapper.Value as IList); - break; + allFields.Add(output.Param.Name); } } } + + return allFields; + } + + /// + /// 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); + + // set data for each output parameter + for (int i = 0; i < Params.Output.Count; i++) + { + var outputParam = Params.Output[i]; + + // set the value if it exists, otherwise set null + object? value = outputLookup.TryGetValue(outputParam.Name, 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( @@ -146,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, since this is empty if coming from a collectionwrapper - 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 - 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 = @@ -297,19 +400,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 + // remove all existing output parameters while (Params.Output.Count > 0) { Params.UnregisterOutputParameter(Params.Output[^1]); } + // add new output parameters foreach (var newParam in outputParams) { var param = new Param_GenericObject @@ -322,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) @@ -334,10 +439,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 @@ -346,7 +451,6 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon { return true; } - count++; } return false; diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs index cc424f8f0..47eda99fc 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/LocalToGlobalToDirectShapeConverter.cs @@ -78,6 +78,24 @@ public class LocalToGlobalToDirectShapeConverter .DirectShapeLibrary.GetDirectShapeLibrary(_converterSettings.Current.Document) .FindDefinition(target.atomicObject.applicationId ?? target.atomicObject.id.NotNull()); result.SetShape(def); + + // add snapping references for meshes and curves + 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; + } + } + return result; // note fast exit here } diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs index 219fe909b..b28bab1e3 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToSpeckle/Properties/ClassPropertiesExtractor.cs @@ -22,15 +22,14 @@ 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) { - default: + // area scheme for area elements + case DB.Area area: + elementPropertiesDict.Add("areaScheme", area.AreaScheme?.Name); break; } - */ return elementPropertiesDict; } 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, 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..d316f8900 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs @@ -1,14 +1,13 @@ +using Rhino; 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; 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(); @@ -23,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 { @@ -49,18 +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 @@ -69,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}"); @@ -93,61 +90,17 @@ public static class DisplayMeshExtractor } /// - /// 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 (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 + ITypedConverter meshConverter, + RhinoDoc doc ) { - 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, doc); + 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..4cfe4b6c1 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/BrepToSpeckleConverter.cs @@ -30,9 +30,8 @@ public class BrepToSpeckleConverter : ITypedConverter List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( target, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter + _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 1c086fc23..c48440510 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/ExtrusionToSpeckleConverter.cs @@ -30,9 +30,8 @@ public class ExtrusionToSpeckleConverter : ITypedConverter displayValue = DisplayMeshExtractor.GetSpeckleMeshes( target, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter + _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 94c7bbdd6..5f9fc642f 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/HatchToSpeckleConverter.cs @@ -41,9 +41,8 @@ public class HatchToSpeckleConverter : ITypedConverter List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( brep, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter + _meshConverter, + _settingsStore.Current.Document ); 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 2bbe4f676..e72a41464 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,34 @@ 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) + // 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]; + 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 +74,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(); diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs index 98091f519..b4e66c247 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Raw/SubDToSpeckleConverter.cs @@ -30,9 +30,8 @@ public class SubDToSpeckleConverter : ITypedConverter List displayValue = DisplayMeshExtractor.GetSpeckleMeshes( target, - _settingsStore.Current.ModelFarFromOrigin, - _settingsStore.Current.SpeckleUnits, - _meshConverter + _meshConverter, + _settingsStore.Current.Document ); var bx = new SOG.SubDX() diff --git a/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs b/DUI3/Speckle.Connectors.DUI/Bindings/ConfigBinding.cs index 9623bd428..aead51b4d 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 IsUpdateNotificationDisabled { get; set; } +} + public class AccountsConfig { public string? UserSelectedAccountId { get; set; } 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; } } 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