Files
speckle-sharp-connectors/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/RevitRootObjectBuilder.cs
T
Oğuzhan Koral 744b185cfe Feat(revit): proxify levels with their all props (#940)
* Unpack levels as proxy

* Dynamically attach elevation and units to data object

* Do not add level properties to collection

* Bump SDK to 3.4.3
2025-06-23 17:48:18 +03:00

258 lines
9.5 KiB
C#

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<RevitConversionSettings> converterSettings,
ISendConversionCache sendConversionCache,
ElementUnpacker elementUnpacker,
LevelUnpacker levelUnpacker,
IThreadContext threadContext,
SendCollectionManager sendCollectionManager,
ILogger<RevitRootObjectBuilder> logger,
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
LinkedModelHandler linkedModelHandler
) : IRootObjectBuilder<DocumentToConvert>
{
public Task<RootObjectBuilderResult> Build(
IReadOnlyList<DocumentToConvert> documentElementContexts,
SendInfo sendInfo,
IProgress<CardProgress> onOperationProgressed,
CancellationToken ct = default
) =>
threadContext.RunOnMainAsync(
() => Task.FromResult(BuildSync(documentElementContexts, sendInfo, onOperationProgressed, ct))
);
private RootObjectBuilderResult BuildSync(
IReadOnlyList<DocumentToConvert> documentElementContexts,
SendInfo sendInfo,
IProgress<CardProgress> 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<DocumentToConvert>();
bool sendWithLinkedModels = converterSettings.Current.SendLinkedModels;
List<SendConversionResult> 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<Elements> will be empty, and we won't enter foreach loop
var elementsInTransform = new List<Element>();
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<DocumentToConvert>();
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);
}
}