Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f763344e0 | |||
| 22badcbe35 | |||
| 2bba0dde3a | |||
| 0ff6992ced | |||
| 8e653890e6 | |||
| 4c76ea393a | |||
| 851ce6ab74 | |||
| 45a0e09c8a | |||
| d43d5b8102 | |||
| 484d95cab1 | |||
| 09ec1a8986 | |||
| eeff41af20 | |||
| 2a89b50cb3 | |||
| d294842515 | |||
| e49835140f | |||
| 56416584ff | |||
| aeeba4151b | |||
| ded9135e80 | |||
| b5a3a0a180 |
@@ -33,7 +33,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
private readonly ElementUnpacker _elementUnpacker;
|
||||
private readonly IRevitConversionSettingsFactory _revitConversionSettingsFactory;
|
||||
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
|
||||
private readonly LinkedModelHandler _linkedModelHandler;
|
||||
private readonly LinkedModelDocumentHandler _linkedModelHandler;
|
||||
private readonly IThreadContext _threadContext;
|
||||
private readonly ISendOperationManagerFactory _sendOperationManagerFactory;
|
||||
|
||||
@@ -56,7 +56,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
ElementUnpacker elementUnpacker,
|
||||
IRevitConversionSettingsFactory revitConversionSettingsFactory,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
LinkedModelHandler linkedModelHandler,
|
||||
LinkedModelDocumentHandler linkedModelHandler,
|
||||
IThreadContext threadContext,
|
||||
IRevitTask revitTask,
|
||||
ISendOperationManagerFactory sendOperationManagerFactory
|
||||
@@ -182,11 +182,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
}
|
||||
|
||||
// transform maps linked model elements into the main model's reference point coordinate system
|
||||
// first apply the user's reference point transform (setting) then adjust for the linked model's placement relative to host.
|
||||
Transform transform = (mainModelTransform ?? Transform.Identity).Multiply(
|
||||
linkedModel.GetTotalTransform().Inverse
|
||||
);
|
||||
|
||||
Transform transform = (mainModelTransform ?? Transform.Identity).Multiply(linkedModel.GetTotalTransform());
|
||||
// decision about whether to process elements is made here, not in the handler
|
||||
// only collects elements from linked models when the setting is enabled
|
||||
if (includeLinkedModels)
|
||||
|
||||
+3
-1
@@ -60,10 +60,12 @@ public static class ServiceRegistration
|
||||
serviceCollection.AddScoped<LevelUnpacker>();
|
||||
serviceCollection.AddScoped<SendCollectionManager>();
|
||||
serviceCollection.AddScoped<IRootObjectBuilder<DocumentToConvert>, RevitRootObjectBuilder>();
|
||||
serviceCollection.AddScoped<DocumentProcessor>();
|
||||
serviceCollection.AddScoped<ProxyManager>();
|
||||
serviceCollection.AddSingleton<ISendConversionCache, SendConversionCache>();
|
||||
serviceCollection.AddSingleton<ToSpeckleSettingsManager>();
|
||||
serviceCollection.AddSingleton<ToHostSettingsManager>();
|
||||
serviceCollection.AddSingleton<LinkedModelHandler>();
|
||||
serviceCollection.AddSingleton<LinkedModelDocumentHandler>();
|
||||
|
||||
// receive operation and dependencies
|
||||
serviceCollection.AddScoped<IHostObjectBuilder, RevitHostObjectBuilder>();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.IO;
|
||||
using Autodesk.Revit.DB;
|
||||
using Speckle.Connectors.DUI.Models.Card.SendFilter;
|
||||
using Speckle.Connectors.RevitShared;
|
||||
@@ -10,123 +9,45 @@ using Speckle.Sdk.Common;
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
/// <summary>
|
||||
/// Handles unpacking elements inside linked models.
|
||||
/// This class is responsible for the mechanics of retrieving elements from linked documents
|
||||
/// based on different filter types, but not for making decisions about whether linked models
|
||||
/// should be processed (which is the responsibility of the calling code)!
|
||||
/// Handles linked model document operations
|
||||
/// </summary>
|
||||
public class LinkedModelHandler
|
||||
public class LinkedModelDocumentHandler
|
||||
{
|
||||
private readonly RevitContext _revitContext;
|
||||
public Dictionary<string, string> LinkedModelDisplayNames { get; } = new();
|
||||
|
||||
public LinkedModelHandler(RevitContext revitContext)
|
||||
public LinkedModelDocumentHandler(RevitContext revitContext)
|
||||
{
|
||||
_revitContext = revitContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets elements from a linked document based on the provided send filter.
|
||||
/// This method handles the specifics of element collection but doesn't make decisions
|
||||
/// about whether the linked model should be processed - that's the caller's responsibility.
|
||||
/// </summary>
|
||||
public List<Element> GetLinkedModelElements(ISendFilter sendFilter, Document linkedDocument, Transform? transform)
|
||||
{
|
||||
// send mode → Categories
|
||||
if (sendFilter is RevitCategoriesFilter categoryFilter && categoryFilter.SelectedCategories is not null)
|
||||
public List<Element> GetLinkedModelElements(ISendFilter sendFilter, Document linkedDocument, Transform? transform) =>
|
||||
sendFilter switch
|
||||
{
|
||||
var categoryIds = categoryFilter
|
||||
.SelectedCategories.Select(c => ElementIdHelper.GetElementId(c))
|
||||
.OfType<ElementId>()
|
||||
.ToList();
|
||||
|
||||
if (categoryIds.Count > 0)
|
||||
{
|
||||
return GetElementsByCategory(linkedDocument, categoryIds);
|
||||
}
|
||||
return new List<Element>();
|
||||
}
|
||||
|
||||
// send mode → Views (taken from the legacy code)
|
||||
if (sendFilter is RevitViewsFilter viewFilter && viewFilter.GetView() != null)
|
||||
{
|
||||
RevitLinkInstance linkInstance = FindLinkInstanceForDocument(
|
||||
linkedDocument.PathName,
|
||||
_revitContext.UIApplication.NotNull().ActiveUIDocument.Document,
|
||||
transform
|
||||
);
|
||||
|
||||
#if REVIT2024_OR_GREATER
|
||||
// revit 2024 and 2025 we can use the three-parameter constructor to get only visible elements
|
||||
using var viewCollector = new FilteredElementCollector(
|
||||
_revitContext.UIApplication.ActiveUIDocument.Document,
|
||||
viewFilter.GetView().NotNull().Id,
|
||||
linkInstance.Id
|
||||
);
|
||||
|
||||
// NOTE: related to [CNX-1482](https://linear.app/speckle/issue/CNX-1482/wall-sweeps-published-duplicated). See RevitViewsFilter.cs
|
||||
return viewCollector.WhereElementIsNotElementType().Where(e => !string.IsNullOrEmpty(e.Name)).ToList();
|
||||
#else
|
||||
// 🚨 LIMITATION: in Revit 2023 and below, we can only check if the entire linked model is visible,
|
||||
// not individual elements within it. If the linked model is visible, all its elements will be included.
|
||||
// constructor overload pertaining to searching and filtering visible elements from a revit link only added 2024.
|
||||
if (linkInstance.IsHidden(viewFilter.GetView().NotNull()))
|
||||
{
|
||||
return new List<Element>(); // if the linked model is hidden, return no elements
|
||||
}
|
||||
// 💩 fallback to getting all elements if the linked model is visible
|
||||
return GetAllElementsForLinkedModelSelection(linkedDocument);
|
||||
#endif
|
||||
}
|
||||
|
||||
// send mode → Selection
|
||||
return GetAllElementsForLinkedModelSelection(linkedDocument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares display names for linked model documents based on filename
|
||||
/// </summary>
|
||||
public void PrepareLinkedModelNames(IReadOnlyList<DocumentToConvert> documentElementContexts)
|
||||
{
|
||||
LinkedModelDisplayNames.Clear();
|
||||
// Group linked models by filename
|
||||
var linkedModels = documentElementContexts
|
||||
.Where(ctx => ctx.Doc.IsLinked)
|
||||
.GroupBy(ctx => Path.GetFileNameWithoutExtension(ctx.Doc.PathName))
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
// Create a unique key for each instance
|
||||
foreach (var group in linkedModels)
|
||||
{
|
||||
string baseName = group.Key;
|
||||
var instances = group.Value;
|
||||
|
||||
// Single instance - just use the base name
|
||||
if (instances.Count == 1)
|
||||
{
|
||||
string id = GetIdFromDocumentToConvert(instances[0]);
|
||||
LinkedModelDisplayNames[id] = baseName;
|
||||
}
|
||||
// Multiple instances - add numbering
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < instances.Count; i++)
|
||||
{
|
||||
string id = GetIdFromDocumentToConvert(instances[i]);
|
||||
LinkedModelDisplayNames[id] = $"{baseName}_{i + 1}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GetIdFromDocumentToConvert(DocumentToConvert documentToConvert) =>
|
||||
documentToConvert.Doc.GetHashCode() + "-" + (documentToConvert.Transform?.GetHashCode() ?? 0);
|
||||
RevitCategoriesFilter categoryFilter when categoryFilter.SelectedCategories is not null
|
||||
=> GetElementsByCategory(linkedDocument, categoryFilter),
|
||||
RevitViewsFilter viewFilter when viewFilter.GetView() != null
|
||||
=> GetElementsFromView(linkedDocument, viewFilter, transform),
|
||||
_ => GetAllElementsForLinkedModelSelection(linkedDocument)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets elements from a document that belong to the specified categories.
|
||||
/// </summary>
|
||||
private List<Element> GetElementsByCategory(Document linkedDoc, List<ElementId> categoryIds)
|
||||
private List<Element> GetElementsByCategory(Document linkedDoc, RevitCategoriesFilter categoryFilter)
|
||||
{
|
||||
var categoryIds = categoryFilter
|
||||
.SelectedCategories!.Select(c => ElementIdHelper.GetElementId(c))
|
||||
.OfType<ElementId>()
|
||||
.ToList();
|
||||
|
||||
if (categoryIds.Count == 0)
|
||||
{
|
||||
return new List<Element>();
|
||||
}
|
||||
|
||||
using var multicategoryFilter = new ElementMulticategoryFilter(categoryIds);
|
||||
using var collector = new FilteredElementCollector(linkedDoc);
|
||||
return collector
|
||||
@@ -136,33 +57,39 @@ public class LinkedModelHandler
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Helper method to generate a simple hash for a transform
|
||||
// transformedElement.applicationId = ${applicationId}-t{transformHash}
|
||||
public string GetTransformHash(Transform transform)
|
||||
/// <summary>
|
||||
/// Gets elements from a linked document visible in a specific view.
|
||||
/// </summary>
|
||||
private List<Element> GetElementsFromView(Document linkedDocument, RevitViewsFilter viewFilter, Transform? transform)
|
||||
{
|
||||
// create a simplified representation of the transform
|
||||
string json =
|
||||
$@"{{
|
||||
""origin"": [{transform.Origin.X:F2}, {transform.Origin.Y:F2}, {transform.Origin.Z:F2}],
|
||||
""basis"": [{transform.BasisX.X:F1}, {transform.BasisY.Y:F1}, {transform.BasisZ.Z:F1}]
|
||||
}}";
|
||||
var view = viewFilter.GetView()!;
|
||||
var linkInstance = FindLinkInstanceForDocument(
|
||||
linkedDocument.PathName,
|
||||
_revitContext.UIApplication.NotNull().ActiveUIDocument.Document,
|
||||
transform
|
||||
);
|
||||
|
||||
byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
#if REVIT2024_OR_GREATER
|
||||
// Revit 2024+ can filter visible elements in linked models
|
||||
using var viewCollector = new FilteredElementCollector(
|
||||
_revitContext.UIApplication.ActiveUIDocument.Document,
|
||||
view.Id,
|
||||
linkInstance.Id
|
||||
);
|
||||
|
||||
#pragma warning disable CA1850
|
||||
using (var sha256 = System.Security.Cryptography.SHA256.Create())
|
||||
return viewCollector.WhereElementIsNotElementType().Where(e => !string.IsNullOrEmpty(e.Name)).ToList();
|
||||
#else
|
||||
// Revit 2023 and below limitation - check if entire linked model is visible
|
||||
if (linkInstance.IsHidden(view))
|
||||
{
|
||||
byte[] hashBytes = sha256.ComputeHash(jsonBytes);
|
||||
// keep only the first 8 characters for a short but unique hash
|
||||
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant()[..8];
|
||||
return new List<Element>();
|
||||
}
|
||||
#pragma warning restore CA1850
|
||||
return GetAllElementsForLinkedModelSelection(linkedDocument);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all elements from the linked document when using selection filters.
|
||||
/// When a linked model is selected in the main document, we include all elements
|
||||
/// from that linked model since the selection is of the entire linked instance.
|
||||
/// </summary>
|
||||
private List<Element> GetAllElementsForLinkedModelSelection(Document linkedDoc)
|
||||
{
|
||||
@@ -173,11 +100,6 @@ public class LinkedModelHandler
|
||||
/// <summary>
|
||||
/// Finds a specific RevitLinkInstance that corresponds to a linked document with a matching transform.
|
||||
/// </summary>
|
||||
/// <param name="linkedDocumentPath">The file path of the linked document</param>
|
||||
/// <param name="transform">The transform to match (expected to already be an inverse transform).
|
||||
/// When provided with multiple instances of the same linked document, this is used to find the specific instance.</param>
|
||||
/// <param name="mainDocument">The main Revit document containing the link instances</param>
|
||||
/// <returns>The matching RevitLinkInstance, or the first available instance if no match is found</returns>
|
||||
private RevitLinkInstance FindLinkInstanceForDocument(
|
||||
string linkedDocumentPath,
|
||||
Document mainDocument,
|
||||
@@ -191,23 +113,19 @@ public class LinkedModelHandler
|
||||
.Where(link => link.GetLinkDocument()?.PathName == linkedDocumentPath)
|
||||
.ToList();
|
||||
|
||||
// if no transform or only one instance, just return the first
|
||||
// if no transform or only one instance, return the first
|
||||
if (transform == null || linkInstances.Count <= 1)
|
||||
{
|
||||
return linkInstances.FirstOrDefault()
|
||||
?? throw new SpeckleException($"No link instance found for {linkedDocumentPath}");
|
||||
}
|
||||
|
||||
// a match consists of not only the linked document path name but the transformation too (think linked instances)
|
||||
// precompute our target hash once
|
||||
string targetHash = GetTransformHash(transform);
|
||||
|
||||
// directly find the matching instance
|
||||
// find matching instance by transform hash
|
||||
string targetHash = TransformUtils.CreateTransformHash(transform);
|
||||
var matchingInstance = linkInstances.FirstOrDefault(link =>
|
||||
GetTransformHash(link.GetTotalTransform().Inverse) == targetHash
|
||||
TransformUtils.CreateTransformHash(link.GetTotalTransform().Inverse) == targetHash
|
||||
);
|
||||
|
||||
// return matching with a fallback to first (main) instance in case something goes funky with the hash
|
||||
return matchingInstance ?? linkInstances.First();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.IO;
|
||||
using Autodesk.Revit.DB;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
/// <summary>
|
||||
/// Simplified utility for generating unique IDs for linked models and instances.
|
||||
/// </summary>
|
||||
public static class TransformUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a unique definition ID for a linked model based on its document path.
|
||||
/// Same linked model always gets the same definition ID.
|
||||
/// </summary>
|
||||
/// <returns>Unique definition ID like "LinkedModel_Building_A_1234"</returns>
|
||||
public static string CreateDefinitionId(string documentPath)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(documentPath);
|
||||
var hash = CreateSimpleHash(documentPath);
|
||||
return $"LinkedModel_{fileName}_{hash}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a unique instance ID for a specific positioned instance of a linked model.
|
||||
/// </summary>
|
||||
/// <param name="definitionId">The definition ID from CreateDefinitionId</param>
|
||||
/// <param name="instanceIndex">1-based index of this instance</param>
|
||||
/// <returns>Unique instance ID like "LinkedModel_Building_A_1234_instance_1"</returns>
|
||||
public static string CreateInstanceId(string definitionId, int instanceIndex) =>
|
||||
$"{definitionId}_instance_{instanceIndex}";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a simple hash for transform identification.
|
||||
/// Used to distinguish different positioned instances of the same linked model.
|
||||
/// </summary>
|
||||
/// <param name="transform">The transform to hash</param>
|
||||
/// <returns>8-character hex hash for quick comparison</returns>
|
||||
public static string CreateTransformHash(Transform transform)
|
||||
{
|
||||
// Create simplified transform representation
|
||||
var transformData =
|
||||
$"{transform.Origin.X:F2},{transform.Origin.Y:F2},{transform.Origin.Z:F2},"
|
||||
+ $"{transform.BasisX.X:F1},{transform.BasisY.Y:F1},{transform.BasisZ.Z:F1}";
|
||||
|
||||
return CreateSimpleHash(transformData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a consistent 8-character hash from input string.
|
||||
/// Using built-in GetHashCode for simplicity - adequate for our use case.
|
||||
/// </summary>
|
||||
private static string CreateSimpleHash(string input)
|
||||
{
|
||||
// Use built-in hash code - much simpler than SHA256 for this use case
|
||||
var hash = input.GetHashCode();
|
||||
return Math.Abs(hash).ToString("X8"); // 8-char hex string
|
||||
}
|
||||
}
|
||||
|
||||
// Extension method to make usage cleaner
|
||||
public static class DocumentToConvertExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a simple display name for the document.
|
||||
/// </summary>
|
||||
public static string GetDisplayName(this DocumentToConvert document) =>
|
||||
Path.GetFileNameWithoutExtension(document.Doc.PathName);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Speckle.Connectors.Common.Conversion;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.DUI.Exceptions;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Send;
|
||||
|
||||
/// <summary>
|
||||
/// Unified processor for all document types (main and linked models).
|
||||
/// </summary>
|
||||
public class DocumentProcessor
|
||||
{
|
||||
private readonly IRootToSpeckleConverter _converter;
|
||||
private readonly ElementUnpacker _elementUnpacker;
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly SendCollectionManager _sendCollectionManager;
|
||||
|
||||
public DocumentProcessor(
|
||||
IRootToSpeckleConverter converter,
|
||||
ElementUnpacker elementUnpacker,
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
SendCollectionManager sendCollectionManager
|
||||
)
|
||||
{
|
||||
_converter = converter;
|
||||
_elementUnpacker = elementUnpacker;
|
||||
_converterSettings = converterSettings;
|
||||
_sendCollectionManager = sendCollectionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main entry point: processes all documents from grouping through conversion.
|
||||
/// </summary>
|
||||
public DocumentConversionResults ProcessDocuments(
|
||||
IReadOnlyList<DocumentToConvert> documents,
|
||||
ConversionContext context
|
||||
)
|
||||
{
|
||||
// Step 1: Group and validate documents in one pass
|
||||
var processed = GroupAndValidateDocuments(documents, context);
|
||||
|
||||
if (!processed.HasProcessableContent)
|
||||
{
|
||||
throw new SpeckleSendFilterException("No objects were found. Please update your publish filter!");
|
||||
}
|
||||
|
||||
// Step 2: Convert main model if present
|
||||
MainModelConversionResult? mainResult = null;
|
||||
if (processed.MainModel != null)
|
||||
{
|
||||
mainResult = ConvertMainModel(processed.MainModel, context);
|
||||
}
|
||||
|
||||
// Step 3: Convert linked models if enabled and present
|
||||
LinkedModelConversionResults? linkedResults = null;
|
||||
if (context.SendWithLinkedModels && processed.LinkedModelGroups.Count != 0)
|
||||
{
|
||||
linkedResults = ConvertLinkedModels(processed.LinkedModelGroups, context);
|
||||
}
|
||||
|
||||
// Step 4: Create final results
|
||||
var finalResults = new DocumentConversionResults(mainResult, linkedResults);
|
||||
ValidateResults(finalResults);
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups documents by type and validates them in a single pass.
|
||||
/// </summary>
|
||||
private ProcessedDocuments GroupAndValidateDocuments(
|
||||
IReadOnlyList<DocumentToConvert> documents,
|
||||
ConversionContext context
|
||||
)
|
||||
{
|
||||
var result = new ProcessedDocuments();
|
||||
|
||||
foreach (var doc in documents)
|
||||
{
|
||||
if (doc?.Doc == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter out invalid elements early
|
||||
var validElements = doc.Elements.Where(e => e?.Category != null).ToList();
|
||||
if (validElements.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var validDoc = doc with { Elements = validElements };
|
||||
|
||||
if (doc.Doc.IsLinked)
|
||||
{
|
||||
// NOTE: mutates result object, probably not the greatest design
|
||||
ProcessLinkedModel(validDoc, context, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.MainModel = validDoc;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ProcessLinkedModel(DocumentToConvert linkedDoc, ConversionContext context, ProcessedDocuments result)
|
||||
{
|
||||
if (!context.SendWithLinkedModels)
|
||||
{
|
||||
var warning = new SendConversionResult(
|
||||
Status.WARNING,
|
||||
linkedDoc.Doc.PathName,
|
||||
typeof(RevitLinkInstance).ToString(),
|
||||
null,
|
||||
new SpeckleException("Enable linked model support from the settings to send this object")
|
||||
);
|
||||
|
||||
result.ValidationResults.Add(warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// group by document path (same file, different positions)
|
||||
var documentPath = linkedDoc.Doc.PathName;
|
||||
if (!result.LinkedModelGroups.TryGetValue(documentPath, out var group))
|
||||
{
|
||||
group = [];
|
||||
result.LinkedModelGroups[documentPath] = group;
|
||||
}
|
||||
|
||||
group.Add(linkedDoc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts main model
|
||||
/// </summary>
|
||||
private MainModelConversionResult ConvertMainModel(DocumentToConvert mainModel, ConversionContext context)
|
||||
{
|
||||
var unpackedModel = UnpackDocument(mainModel);
|
||||
var results = new List<SendConversionResult>();
|
||||
int totalElements = mainModel.Elements.Count;
|
||||
int processedElements = 0;
|
||||
|
||||
foreach (var element in unpackedModel.Elements)
|
||||
{
|
||||
context.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var result = ConvertElement(element, context, null);
|
||||
results.Add(result);
|
||||
|
||||
context.Progress.Report(new CardProgress("Converting", ++processedElements / (double)totalElements));
|
||||
}
|
||||
|
||||
return new MainModelConversionResult(results, unpackedModel.Elements);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts linked models with proxy tracking
|
||||
/// </summary>
|
||||
private LinkedModelConversionResults ConvertLinkedModels(
|
||||
Dictionary<string, List<DocumentToConvert>> linkedGroups,
|
||||
ConversionContext context
|
||||
)
|
||||
{
|
||||
var conversions = new List<LinkedModelConversionResult>();
|
||||
var allConversionResults = new List<SendConversionResult>();
|
||||
var allConvertedElements = new List<Element>();
|
||||
|
||||
foreach (var kvp in linkedGroups)
|
||||
{
|
||||
var documentPath = kvp.Key;
|
||||
var instances = kvp.Value;
|
||||
|
||||
// convert the first instance (unique model) without transform
|
||||
var firstInstance = instances.First();
|
||||
var modelWithoutTransform = firstInstance with { Transform = null };
|
||||
|
||||
var unpackedModel = UnpackDocument(modelWithoutTransform);
|
||||
var trackingResult = new LinkedModelConversionResult(documentPath, instances);
|
||||
|
||||
foreach (var element in unpackedModel.Elements)
|
||||
{
|
||||
context.CancellationToken.ThrowIfCancellationRequested();
|
||||
var result = ConvertElement(element, context, trackingResult);
|
||||
allConversionResults.Add(result);
|
||||
}
|
||||
|
||||
conversions.Add(trackingResult);
|
||||
allConvertedElements.AddRange(unpackedModel.Elements);
|
||||
}
|
||||
|
||||
return new LinkedModelConversionResults(conversions, allConversionResults, allConvertedElements);
|
||||
}
|
||||
|
||||
private SendConversionResult ConvertElement(
|
||||
Element element,
|
||||
ConversionContext context,
|
||||
LinkedModelConversionResult? trackingResult
|
||||
)
|
||||
{
|
||||
string applicationId = element.UniqueId;
|
||||
string sourceType = element.GetType().Name;
|
||||
|
||||
try
|
||||
{
|
||||
using (_converterSettings.Push(settings => settings with { Document = element.Document }))
|
||||
{
|
||||
var converted = _converter.Convert(element);
|
||||
converted.applicationId = applicationId;
|
||||
|
||||
// add to appropriate collection
|
||||
var collection = _sendCollectionManager.GetAndCreateObjectHostCollection(
|
||||
element,
|
||||
context.RootObject,
|
||||
context.SendWithLinkedModels,
|
||||
null // TODO: add model display name logic later if needed
|
||||
);
|
||||
collection.elements.Add(converted);
|
||||
|
||||
// add to tracking result if provided
|
||||
trackingResult?.ConvertedElementIds.Add(converted.applicationId ?? applicationId);
|
||||
|
||||
return new SendConversionResult(Status.SUCCESS, applicationId, sourceType, converted);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
return new SendConversionResult(Status.ERROR, applicationId, sourceType, null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks document elements
|
||||
/// </summary>
|
||||
private DocumentToConvert UnpackDocument(DocumentToConvert document)
|
||||
{
|
||||
using (_converterSettings.Push(settings => settings with { Document = document.Doc }))
|
||||
{
|
||||
var atomicObjects = _elementUnpacker.UnpackSelectionForConversion(document.Elements, document.Doc).ToList();
|
||||
|
||||
return document with
|
||||
{
|
||||
Elements = atomicObjects
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple validation of final results.
|
||||
/// </summary>
|
||||
private static void ValidateResults(DocumentConversionResults results)
|
||||
{
|
||||
if (results.AllFailed)
|
||||
{
|
||||
throw new SpeckleException("Failed to convert all objects");
|
||||
}
|
||||
|
||||
var totalResults = results.AllResults.Count;
|
||||
var skippedCount = results.AllResults.Count(r => r.Status == Status.WARNING);
|
||||
|
||||
if (skippedCount == totalResults)
|
||||
{
|
||||
throw new SpeckleException("No supported objects visible. Update publish filter or check publish settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Speckle.Connectors.Common.Conversion;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Send;
|
||||
|
||||
/// <summary>
|
||||
/// Contains all the information needed for document processing in one place.
|
||||
/// </summary>
|
||||
public class ProcessedDocuments
|
||||
{
|
||||
/// <summary>
|
||||
/// The main model document (non-linked), if any.
|
||||
/// </summary>
|
||||
public DocumentToConvert? MainModel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Linked model instances grouped by document path.
|
||||
/// Key: Document path, Value: List of instances with different transforms.
|
||||
/// </summary>
|
||||
public Dictionary<string, List<DocumentToConvert>> LinkedModelGroups { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Any validation results/warnings generated during processing.
|
||||
/// </summary>
|
||||
public List<SendConversionResult> ValidationResults { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Check if we have any processable content.
|
||||
/// </summary>
|
||||
public bool HasProcessableContent =>
|
||||
(MainModel?.Elements.Count > 0) || LinkedModelGroups.Values.Any(group => group.Count != 0);
|
||||
|
||||
/// <summary>
|
||||
/// Get total element count across all documents.
|
||||
/// </summary>
|
||||
public int TotalElementCount =>
|
||||
(MainModel?.Elements.Count ?? 0) + LinkedModelGroups.Values.Sum(group => group.Sum(doc => doc.Elements.Count));
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using System.IO;
|
||||
using Autodesk.Revit.DB;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Send;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the creation and organization of all proxy objects (instances, materials, levels, etc.).
|
||||
/// </summary>
|
||||
public class ProxyManager
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly ITypedConverter<(Transform transform, string units), Matrix4x4> _transformConverter;
|
||||
private readonly ElementUnpacker _elementUnpacker;
|
||||
private readonly LevelUnpacker _levelUnpacker;
|
||||
private readonly RevitToSpeckleCacheSingleton _revitToSpeckleCacheSingleton;
|
||||
private readonly ILogger<ProxyManager> _logger;
|
||||
|
||||
public ProxyManager(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
ITypedConverter<(Transform transform, string units), Matrix4x4> transformConverter,
|
||||
ElementUnpacker elementUnpacker,
|
||||
LevelUnpacker levelUnpacker,
|
||||
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
|
||||
ILogger<ProxyManager> logger
|
||||
)
|
||||
{
|
||||
_converterSettings = converterSettings;
|
||||
_transformConverter = transformConverter;
|
||||
_elementUnpacker = elementUnpacker;
|
||||
_levelUnpacker = levelUnpacker;
|
||||
_revitToSpeckleCacheSingleton = revitToSpeckleCacheSingleton;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all types of proxies to the root object based on conversion results
|
||||
/// </summary>
|
||||
public void AddAllProxies(Collection rootObject, DocumentConversionResults conversionResults)
|
||||
{
|
||||
// add instance proxies (definitions and instances)
|
||||
if (conversionResults.LinkedModelResults?.LinkedModelConversions.Count > 0)
|
||||
{
|
||||
AddInstanceProxies(rootObject, conversionResults.LinkedModelResults);
|
||||
}
|
||||
|
||||
// add material and level proxies
|
||||
AddMaterialProxies(rootObject, conversionResults.AllElements);
|
||||
AddLevelProxies(rootObject, conversionResults.AllElements);
|
||||
|
||||
// add reference point transform if available
|
||||
AddReferencePointTransform(rootObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and adds instance definition proxies and instance proxies for linked models
|
||||
/// </summary>
|
||||
private void AddInstanceProxies(Collection rootObject, LinkedModelConversionResults linkedResults)
|
||||
{
|
||||
var instanceDefinitionProxies = CreateInstanceDefinitionProxies(linkedResults);
|
||||
CreateInstanceProxyCollections(rootObject, linkedResults);
|
||||
|
||||
if (instanceDefinitionProxies.Count > 0)
|
||||
{
|
||||
rootObject[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates instance definition proxies for each unique linked model
|
||||
/// </summary>
|
||||
private List<InstanceDefinitionProxy> CreateInstanceDefinitionProxies(LinkedModelConversionResults linkedResults)
|
||||
{
|
||||
var instanceDefinitionProxies = new List<InstanceDefinitionProxy>();
|
||||
|
||||
foreach (var conversionResult in linkedResults.LinkedModelConversions)
|
||||
{
|
||||
if (conversionResult.ConvertedElementIds.Count == 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Skipping InstanceDefinitionProxy for '{DocumentPath}' - no elements were converted",
|
||||
Path.GetFileName(conversionResult.DocumentPath)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
string definitionId = TransformUtils.CreateDefinitionId(conversionResult.DocumentPath);
|
||||
string modelName = Path.GetFileNameWithoutExtension(conversionResult.DocumentPath);
|
||||
|
||||
var instanceDefinitionProxy = new InstanceDefinitionProxy
|
||||
{
|
||||
applicationId = definitionId,
|
||||
objects = conversionResult.ConvertedElementIds.ToList(),
|
||||
maxDepth = 0, // linked models are at depth 0 for now
|
||||
name = modelName
|
||||
};
|
||||
|
||||
instanceDefinitionProxies.Add(instanceDefinitionProxy);
|
||||
}
|
||||
|
||||
return instanceDefinitionProxies;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates instance proxy collections and adds them to the appropriate model collections
|
||||
/// </summary>
|
||||
private void CreateInstanceProxyCollections(Collection rootObject, LinkedModelConversionResults linkedResults)
|
||||
{
|
||||
foreach (var conversionResult in linkedResults.LinkedModelConversions)
|
||||
{
|
||||
if (conversionResult.ConvertedElementIds.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string definitionId = TransformUtils.CreateDefinitionId(conversionResult.DocumentPath);
|
||||
string modelName = Path.GetFileNameWithoutExtension(conversionResult.DocumentPath);
|
||||
|
||||
var instanceProxies = CreateInstanceProxiesForLinkedModel(conversionResult, definitionId);
|
||||
|
||||
if (instanceProxies.Count > 0)
|
||||
{
|
||||
AddInstanceProxiesToCollection(rootObject, modelName, instanceProxies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates instance proxies for each instance of a linked model
|
||||
/// </summary>
|
||||
private List<InstanceProxy> CreateInstanceProxiesForLinkedModel(
|
||||
LinkedModelConversionResult conversionResult,
|
||||
string definitionId
|
||||
)
|
||||
{
|
||||
var instanceProxies = new List<InstanceProxy>();
|
||||
int instanceIndex = 0;
|
||||
|
||||
foreach (var instance in conversionResult.Instances)
|
||||
{
|
||||
instanceIndex++;
|
||||
|
||||
if (instance.Transform == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string instanceId = TransformUtils.CreateInstanceId(definitionId, instanceIndex);
|
||||
var transformMatrix = _transformConverter.Convert((instance.Transform, _converterSettings.Current.SpeckleUnits));
|
||||
var instanceProxy = new InstanceProxy
|
||||
{
|
||||
applicationId = instanceId,
|
||||
definitionId = definitionId,
|
||||
transform = transformMatrix,
|
||||
units = _converterSettings.Current.SpeckleUnits,
|
||||
maxDepth = 0 // linked models are at depth 0 for now
|
||||
};
|
||||
|
||||
instanceProxies.Add(instanceProxy);
|
||||
}
|
||||
|
||||
return instanceProxies;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds instance proxies to the appropriate collection structure
|
||||
/// </summary>
|
||||
private void AddInstanceProxiesToCollection(
|
||||
Collection rootObject,
|
||||
string modelName,
|
||||
List<InstanceProxy> instanceProxies
|
||||
)
|
||||
{
|
||||
// find or create the linked model collection
|
||||
var linkedModelCollection = FindOrCreateLinkedModelCollection(rootObject, modelName);
|
||||
|
||||
// find or create the "instances" subcollection
|
||||
var instancesCollection = FindOrCreateInstancesCollection(linkedModelCollection);
|
||||
|
||||
// add all instance proxies to the instances collection
|
||||
foreach (var instanceProxy in instanceProxies)
|
||||
{
|
||||
instancesCollection.elements.Add(instanceProxy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds or creates a collection for a linked model
|
||||
/// </summary>
|
||||
private Collection FindOrCreateLinkedModelCollection(Collection rootObject, string modelName)
|
||||
{
|
||||
// look for existing linked model collection
|
||||
foreach (var element in rootObject.elements)
|
||||
{
|
||||
if (element is Collection collection && collection.name == modelName)
|
||||
{
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
// create new collection if not found
|
||||
var linkedModelCollection = new Collection(modelName);
|
||||
rootObject.elements.Add(linkedModelCollection);
|
||||
return linkedModelCollection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds or creates an "instances" subcollection within a linked model collection
|
||||
/// </summary>
|
||||
private Collection FindOrCreateInstancesCollection(Collection linkedModelCollection)
|
||||
{
|
||||
// look for existing instances collection
|
||||
foreach (var element in linkedModelCollection.elements)
|
||||
{
|
||||
if (element is Collection collection && collection.name == "instances")
|
||||
{
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
// create new instances collection
|
||||
var instancesCollection = new Collection("instances");
|
||||
linkedModelCollection.elements.Add(instancesCollection);
|
||||
return instancesCollection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds material proxies to the root object
|
||||
/// </summary>
|
||||
private void AddMaterialProxies(Collection rootObject, List<Element> allElements)
|
||||
{
|
||||
var idsAndSubElementIds = _elementUnpacker.GetElementsAndSubelementIdsFromAtomicObjects(allElements);
|
||||
var renderMaterialProxies = _revitToSpeckleCacheSingleton.GetRenderMaterialProxyListForObjects(idsAndSubElementIds);
|
||||
|
||||
if (renderMaterialProxies.Count > 0)
|
||||
{
|
||||
rootObject[ProxyKeys.RENDER_MATERIAL] = renderMaterialProxies;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds level proxies to the root object
|
||||
/// </summary>
|
||||
private void AddLevelProxies(Collection rootObject, List<Element> allElements)
|
||||
{
|
||||
var levelProxies = _levelUnpacker.Unpack(allElements);
|
||||
|
||||
if (levelProxies.Count > 0)
|
||||
{
|
||||
rootObject[ProxyKeys.LEVEL] = levelProxies;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds reference point transform information to the root object if available
|
||||
/// </summary>
|
||||
private void AddReferencePointTransform(Collection rootObject)
|
||||
{
|
||||
if (_converterSettings.Current.ReferencePointTransform is Transform transform)
|
||||
{
|
||||
var transformMatrix = ReferencePointHelper.CreateTransformDataForRootObject(transform);
|
||||
rootObject[ReferencePointHelper.REFERENCE_POINT_TRANSFORM_KEY] = transformMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-226
@@ -1,43 +1,44 @@
|
||||
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>
|
||||
/// <summary>
|
||||
/// Orchestrates the building of the root Speckle object from Revit documents.
|
||||
/// </summary>
|
||||
public class RevitRootObjectBuilder : IRootObjectBuilder<DocumentToConvert>
|
||||
{
|
||||
private readonly DocumentProcessor _documentProcessor;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly IThreadContext _threadContext;
|
||||
|
||||
public RevitRootObjectBuilder(
|
||||
DocumentProcessor documentProcessor,
|
||||
ProxyManager proxyManager,
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
IThreadContext threadContext
|
||||
)
|
||||
{
|
||||
_documentProcessor = documentProcessor;
|
||||
_proxyManager = proxyManager;
|
||||
_converterSettings = converterSettings;
|
||||
_threadContext = threadContext;
|
||||
}
|
||||
|
||||
public Task<RootObjectBuilderResult> Build(
|
||||
IReadOnlyList<DocumentToConvert> documentElementContexts,
|
||||
string projectId,
|
||||
IProgress<CardProgress> onOperationProgressed,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
threadContext.RunOnMainAsync(
|
||||
_threadContext.RunOnMainAsync(
|
||||
() => Task.FromResult(BuildSync(documentElementContexts, projectId, onOperationProgressed, ct))
|
||||
);
|
||||
|
||||
@@ -48,216 +49,26 @@ public class RevitRootObjectBuilder(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var doc = converterSettings.Current.Document;
|
||||
|
||||
if (doc.IsFamilyDocument)
|
||||
var document = _converterSettings.Current.Document;
|
||||
if (document.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 documentName = document.PathName.Split('\\').Last().Split('.').First();
|
||||
var rootObject = new Collection(documentName) { ["units"] = _converterSettings.Current.SpeckleUnits };
|
||||
|
||||
var filteredDocumentsToConvert = new List<DocumentToConvert>();
|
||||
bool sendWithLinkedModels = converterSettings.Current.SendLinkedModels;
|
||||
List<SendConversionResult> results = new();
|
||||
var context = new ConversionContext(
|
||||
projectId,
|
||||
rootObject,
|
||||
_converterSettings.Current.SendLinkedModels,
|
||||
onOperationProgressed,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
// Prepare linked model display names if needed
|
||||
if (sendWithLinkedModels)
|
||||
{
|
||||
linkedModelHandler.PrepareLinkedModelNames(documentElementContexts);
|
||||
}
|
||||
var conversionResults = _documentProcessor.ProcessDocuments(documentElementContexts, context);
|
||||
_proxyManager.AddAllProxies(rootObject, conversionResults);
|
||||
|
||||
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(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 we ended up skipping everything, there is a reason for this, that users can diagnose themselves
|
||||
// this can occur if a published view contains only unsupported objects or if user trying to ONLY send linked model
|
||||
// docs but the setting is disabled
|
||||
if (skippedObjectCount == atomicObjectCount)
|
||||
{
|
||||
throw new SpeckleException("No supported objects visible. Update publish filter or check publish settings.");
|
||||
}
|
||||
|
||||
// this is, I suppose, fully on us?
|
||||
if (results.All(x => x.Status == Status.ERROR))
|
||||
{
|
||||
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);
|
||||
return new RootObjectBuilderResult(rootObject, conversionResults.AllResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Speckle.Connectors.Common.Conversion;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Send;
|
||||
|
||||
/// <summary>
|
||||
/// Context object that contains all the parameters needed for conversion operations.
|
||||
/// Reduces parameter passing and makes method signatures cleaner.
|
||||
/// </summary>
|
||||
public record ConversionContext(
|
||||
string ProjectId,
|
||||
Collection RootObject,
|
||||
bool SendWithLinkedModels,
|
||||
IProgress<CardProgress> Progress,
|
||||
CancellationToken CancellationToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the conversion results for a single linked model.
|
||||
/// Used to coordinate between element conversion and proxy creation.
|
||||
/// </summary>
|
||||
public class LinkedModelConversionResult
|
||||
{
|
||||
public string DocumentPath { get; }
|
||||
public List<DocumentToConvert> Instances { get; }
|
||||
public List<string> ConvertedElementIds { get; }
|
||||
|
||||
public LinkedModelConversionResult(string documentPath, List<DocumentToConvert> instances)
|
||||
{
|
||||
DocumentPath = documentPath;
|
||||
Instances = instances;
|
||||
ConvertedElementIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the results of converting linked models with proxy tracking.
|
||||
/// </summary>
|
||||
public record LinkedModelConversionResults(
|
||||
List<LinkedModelConversionResult> LinkedModelConversions,
|
||||
List<SendConversionResult> ConversionResults,
|
||||
List<Element> ConvertedElements
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Contains the results of converting the main model.
|
||||
/// </summary>
|
||||
public record MainModelConversionResult(List<SendConversionResult> ConversionResults, List<Element> ConvertedElements);
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates all conversion results from main model and linked models.
|
||||
/// </summary>
|
||||
public record DocumentConversionResults(
|
||||
MainModelConversionResult? MainModelResult,
|
||||
LinkedModelConversionResults? LinkedModelResults
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// All conversion results combined from main and linked models.
|
||||
/// </summary>
|
||||
public List<SendConversionResult> AllResults
|
||||
{
|
||||
get
|
||||
{
|
||||
var results = new List<SendConversionResult>();
|
||||
if (MainModelResult != null)
|
||||
{
|
||||
results.AddRange(MainModelResult.ConversionResults);
|
||||
}
|
||||
if (LinkedModelResults != null)
|
||||
{
|
||||
results.AddRange(LinkedModelResults.ConversionResults);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All converted elements from main and linked models.
|
||||
/// </summary>
|
||||
public List<Element> AllElements
|
||||
{
|
||||
get
|
||||
{
|
||||
var elements = new List<Element>();
|
||||
if (MainModelResult != null)
|
||||
{
|
||||
elements.AddRange(MainModelResult.ConvertedElements);
|
||||
}
|
||||
if (LinkedModelResults != null)
|
||||
{
|
||||
elements.AddRange(LinkedModelResults.ConvertedElements);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if any conversions resulted in errors.
|
||||
/// </summary>
|
||||
public bool HasErrors => AllResults.Any(r => r.Status == Status.ERROR);
|
||||
|
||||
/// <summary>
|
||||
/// Check if all conversions failed.
|
||||
/// </summary>
|
||||
public bool AllFailed => AllResults.All(r => r.Status == Status.ERROR);
|
||||
}
|
||||
+5
@@ -26,6 +26,7 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitMaterialBaker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\SupportedCategoriesUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitViewManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\TransformUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\HideWarningsFailuresPreprocessor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\IdStorageSchema.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\IStorageSchema.cs" />
|
||||
@@ -40,11 +41,15 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\RevitHostObjectBuilder.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\ToHostSettingsManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\TransactionManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\DocumentProcessor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Filters\IRevitSendFilter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Filters\RevitCategoriesFilter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Filters\RevitSelectionFilter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Filters\RevitViewsFilter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\ProcessedDocuments.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\ProxyManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\RevitRootObjectBuilder.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\SendContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\LinkedModelsSetting.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\SendParameterNullOrEmptyStringsSetting.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\SendRebarsAsVolumetricSetting.cs" />
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.Common.Registration;
|
||||
using Speckle.Converters.Revit2023.ToSpeckle.Properties;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Converters.RevitShared.Services;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.Converters.RevitShared.ToSpeckle;
|
||||
using Speckle.Converters.RevitShared.ToSpeckle.Properties;
|
||||
using Speckle.Converters.ToSpeckle.Properties;
|
||||
using Speckle.Converters.ToSpeckle.Raw.Geometry;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Converters.RevitShared;
|
||||
@@ -34,6 +37,7 @@ public static class ServiceRegistration
|
||||
// POC: do we need ToSpeckleScalingService as is, do we need to interface it out?
|
||||
serviceCollection.AddScoped<ScalingServiceToSpeckle>();
|
||||
serviceCollection.AddScoped<ScalingServiceToHost>();
|
||||
serviceCollection.AddSingleton<ITypedConverter<(DB.Transform, string), Matrix4x4>, TransformConverterToSpeckle>();
|
||||
|
||||
// POC: the concrete type can come out if we remove all the reference to it
|
||||
serviceCollection.AddScoped<
|
||||
|
||||
+1
@@ -70,6 +70,7 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\PlaneToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\PointConversionToSpeckle.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\PolylineToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\TransformConverterToSpeckle.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\VectorToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\XyzConversionToPoint.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\MaterialAsSpeckleMaterialConversionToSpeckle.cs" />
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Common;
|
||||
|
||||
namespace Speckle.Converters.Revit2023.ToSpeckle.Properties;
|
||||
namespace Speckle.Converters.ToSpeckle.Properties;
|
||||
|
||||
public readonly struct StructuralAssetProperties(
|
||||
string name,
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.RevitShared.Services;
|
||||
using Speckle.DoubleNumerics;
|
||||
|
||||
namespace Speckle.Converters.ToSpeckle.Raw.Geometry;
|
||||
|
||||
public class TransformConverterToSpeckle : ITypedConverter<(DB.Transform transform, string units), Matrix4x4>
|
||||
{
|
||||
private readonly IScalingServiceToSpeckle _scalingService;
|
||||
|
||||
public TransformConverterToSpeckle(IScalingServiceToSpeckle scalingService)
|
||||
{
|
||||
_scalingService = scalingService;
|
||||
}
|
||||
|
||||
public Matrix4x4 Convert((DB.Transform transform, string units) target)
|
||||
{
|
||||
var tX = _scalingService.ScaleLength(target.transform.Origin.X);
|
||||
var tY = _scalingService.ScaleLength(target.transform.Origin.Y);
|
||||
var tZ = _scalingService.ScaleLength(target.transform.Origin.Z);
|
||||
|
||||
return new Matrix4x4(
|
||||
target.transform.BasisX.X,
|
||||
target.transform.BasisY.X,
|
||||
target.transform.BasisZ.X,
|
||||
tX,
|
||||
target.transform.BasisX.Y,
|
||||
target.transform.BasisY.Y,
|
||||
target.transform.BasisZ.Y,
|
||||
tY,
|
||||
target.transform.BasisX.Z,
|
||||
target.transform.BasisY.Z,
|
||||
target.transform.BasisZ.Z,
|
||||
tZ,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.Revit2023.ToSpeckle.Properties;
|
||||
using Speckle.Converters.RevitShared.Services;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.Converters.ToSpeckle.Properties;
|
||||
using Speckle.Sdk.Common.Exceptions;
|
||||
using ApplicationException = Autodesk.Revit.Exceptions.ApplicationException;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user