using Autodesk.Revit.DB; using Microsoft.Extensions.Logging; using Speckle.Connectors.Common.Builders; using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Conversion; using Speckle.Connectors.Common.Extensions; using Speckle.Connectors.Common.Operations; using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.Revit.HostApp; using Speckle.Converters.Common; using Speckle.Converters.RevitShared.Helpers; using Speckle.Converters.RevitShared.Settings; using Speckle.Sdk; using Speckle.Sdk.Common; using Speckle.Sdk.Models; using Speckle.Sdk.Models.Collections; namespace Speckle.Connectors.Revit.Operations.Send; public class RevitRootObjectBuilder( IRootToSpeckleConverter converter, IConverterSettingsStore converterSettings, ISendConversionCache sendConversionCache, ElementUnpacker elementUnpacker, LevelUnpacker levelUnpacker, IThreadContext threadContext, SendCollectionManager sendCollectionManager, ILogger logger, RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton, LinkedModelHandler linkedModelHandler ) : IRootObjectBuilder { public Task Build( IReadOnlyList documentElementContexts, SendInfo sendInfo, IProgress onOperationProgressed, CancellationToken ct = default ) => threadContext.RunOnMainAsync( () => Task.FromResult(BuildSync(documentElementContexts, sendInfo, onOperationProgressed, ct)) ); private RootObjectBuilderResult BuildSync( IReadOnlyList documentElementContexts, SendInfo sendInfo, IProgress onOperationProgressed, CancellationToken cancellationToken ) { var doc = converterSettings.Current.Document; if (doc.IsFamilyDocument) { throw new SpeckleException("Family Environment documents are not supported."); } // init the root Collection rootObject = new() { name = converterSettings.Current.Document.PathName.Split('\\').Last().Split('.').First() }; rootObject["units"] = converterSettings.Current.SpeckleUnits; var filteredDocumentsToConvert = new List(); bool sendWithLinkedModels = converterSettings.Current.SendLinkedModels; List results = new(); // Prepare linked model display names if needed if (sendWithLinkedModels) { linkedModelHandler.PrepareLinkedModelNames(documentElementContexts); } foreach (var documentElementContext in documentElementContexts) { // add appropriate warnings for linked documents if (documentElementContext.Doc.IsLinked && !sendWithLinkedModels) { results.Add( new( Status.WARNING, documentElementContext.Doc.PathName, typeof(RevitLinkInstance).ToString(), null, new SpeckleException("Enable linked model support from the settings to send this object") ) ); continue; } // filter for valid elements // if send linked models setting is disabled List will be empty, and we won't enter foreach loop var elementsInTransform = new List(); foreach (var el in documentElementContext.Elements) { if (el == null || el.Category == null) { continue; } elementsInTransform.Add(el); } // only add contexts with elements if (elementsInTransform.Count > 0) { filteredDocumentsToConvert.Add(documentElementContext with { Elements = elementsInTransform }); } } // TODO: check the exception!!!! if (filteredDocumentsToConvert.Count == 0) { throw new SpeckleSendFilterException("No objects were found. Please update your publish filter!"); } // Unpack groups (& other complex data structures) var atomicObjectsByDocumentAndTransform = new List(); var atomicObjectCount = 0; foreach (var filteredDocumentToConvert in filteredDocumentsToConvert) { using ( converterSettings.Push(currentSettings => currentSettings with { Document = filteredDocumentToConvert.Doc }) ) { var atomicObjects = elementUnpacker .UnpackSelectionForConversion(filteredDocumentToConvert.Elements, filteredDocumentToConvert.Doc) .ToList(); atomicObjectsByDocumentAndTransform.Add(filteredDocumentToConvert with { Elements = atomicObjects }); atomicObjectCount += atomicObjects.Count; } } var countProgress = 0; var cacheHitCount = 0; var skippedObjectCount = 0; foreach (var atomicObjectByDocumentAndTransform in atomicObjectsByDocumentAndTransform) { string? modelDisplayName = null; if (atomicObjectByDocumentAndTransform.Doc.IsLinked) { string id = linkedModelHandler.GetIdFromDocumentToConvert(atomicObjectByDocumentAndTransform); linkedModelHandler.LinkedModelDisplayNames.TryGetValue(id, out modelDisplayName); } // here we do magic for changing the transform and the related document according to model. first one is always the main model. using ( converterSettings.Push(currentSettings => currentSettings with { ReferencePointTransform = atomicObjectByDocumentAndTransform.Transform, Document = atomicObjectByDocumentAndTransform.Doc, } ) ) { var atomicObjects = atomicObjectByDocumentAndTransform.Elements; foreach (Element revitElement in atomicObjects) { cancellationToken.ThrowIfCancellationRequested(); string applicationId = revitElement.UniqueId; string sourceType = revitElement.GetType().Name; try { if (!SupportedCategoriesUtils.IsSupportedCategory(revitElement.Category)) { var cat = revitElement.Category != null ? revitElement.Category.Name : "No category"; results.Add( new( Status.WARNING, revitElement.UniqueId, cat, null, new SpeckleException($"Category {cat} is not supported.") ) ); skippedObjectCount++; continue; } Base converted; bool hasTransform = atomicObjectByDocumentAndTransform.Transform != null; // non-transformed elements can safely rely on cache // TODO: Potential here to transform cached objects and NOT reconvert, // TODO: we wont do !hasTransform here, and re-set application id before this if ( !hasTransform && sendConversionCache.TryGetValue(sendInfo.ProjectId, applicationId, out ObjectReference? value) ) { converted = value; cacheHitCount++; } // not in cache means we convert else { // if it has a transform we append transform hash to the applicationId to distinguish the elements from other instances if (hasTransform) { string transformHash = linkedModelHandler.GetTransformHash( atomicObjectByDocumentAndTransform.Transform.NotNull() ); applicationId = $"{applicationId}_t{transformHash}"; } // normal conversions converted = converter.Convert(revitElement); converted.applicationId = applicationId; } var collection = sendCollectionManager.GetAndCreateObjectHostCollection( revitElement, rootObject, sendWithLinkedModels, modelDisplayName ); collection.elements.Add(converted); results.Add(new(Status.SUCCESS, applicationId, sourceType, converted)); } catch (Exception ex) when (!ex.IsFatal()) { logger.LogSendConversionError(ex, sourceType); results.Add(new(Status.ERROR, applicationId, sourceType, null, ex)); } onOperationProgressed.Report(new("Converting", (double)++countProgress / atomicObjectCount)); } } } if (results.All(x => x.Status == Status.ERROR) || skippedObjectCount == atomicObjectCount) { throw new SpeckleException("Failed to convert all objects."); } var flatElements = atomicObjectsByDocumentAndTransform.SelectMany(t => t.Elements).ToList(); var idsAndSubElementIds = elementUnpacker.GetElementsAndSubelementIdsFromAtomicObjects(flatElements); var renderMaterialProxies = revitToSpeckleCacheSingleton.GetRenderMaterialProxyListForObjects(idsAndSubElementIds); rootObject[ProxyKeys.RENDER_MATERIAL] = renderMaterialProxies; var levelProxies = levelUnpacker.Unpack(flatElements); rootObject[ProxyKeys.LEVEL] = levelProxies; // NOTE: these are currently not used anywhere, we'll skip them until someone calls for it back // rootObject[ProxyKeys.PARAMETER_DEFINITIONS] = _parameterDefinitionHandler.Definitions; // we want to store transform data for chosen reference point setting if (converterSettings.Current.ReferencePointTransform is Transform transform) { var transformMatrix = ReferencePointHelper.CreateTransformDataForRootObject(transform); rootObject[ReferencePointHelper.REFERENCE_POINT_TRANSFORM_KEY] = transformMatrix; } return new RootObjectBuilderResult(rootObject, results); } }