Compare commits

...

19 Commits

Author SHA1 Message Date
Björn Steinhagen 5f763344e0 chore: docs 2025-09-25 09:05:19 +02:00
Björn Steinhagen 22badcbe35 chore: delete unused methods 2025-09-25 08:50:17 +02:00
Björn Steinhagen 2bba0dde3a refactor: simplifies things 2025-09-25 08:42:04 +02:00
Björn Steinhagen 0ff6992ced Merge branch 'dev' into bjorn/cnx-2253-deeply-nested-revit-models 2025-09-25 06:48:43 +02:00
Björn Steinhagen 8e653890e6 chore: cleanup 2025-09-25 06:47:31 +02:00
Björn 4c76ea393a fix: transform inverse 2025-09-16 14:07:20 +02:00
Björn 851ce6ab74 feat: scaling service 2025-09-16 14:05:20 +02:00
Björn 45a0e09c8a fix: redundant using statement 2025-09-16 11:07:19 +02:00
Björn d43d5b8102 chore: cleanup 2025-09-16 11:04:43 +02:00
Björn 484d95cab1 fix: document transforms 2025-09-16 09:56:58 +02:00
Björn 09ec1a8986 refactor: cleans up root object builder 2025-09-16 09:44:08 +02:00
Björn eeff41af20 fix: no more scaling needed 2025-09-15 23:35:03 +02:00
Björn 2a89b50cb3 feat: create InstanceProxy objects for each linked model instance placement 2025-09-15 22:40:36 +02:00
Björn d294842515 feat: create InstanceDefinitionProxy objects for unique linked models 2025-09-15 22:35:40 +02:00
Björn e49835140f feat: convert unique linked models once without instance transforms 2025-09-15 22:23:36 +02:00
Björn 56416584ff Merge remote-tracking branch 'origin/dev' into bjorn/cnx-2253-deeply-nested-revit-models 2025-09-15 22:06:42 +02:00
Björn aeeba4151b refactor: group linked models by document in LinkedModelHandler 2025-09-15 22:06:38 +02:00
Björn ded9135e80 Merge branch 'dev' into bjorn/cnx-2253-deeply-nested-revit-models 2025-09-13 13:02:54 +02:00
Björn b5a3a0a180 Merge branch 'dev' into bjorn/cnx-2253-deeply-nested-revit-models 2025-09-12 16:51:51 +02:00
15 changed files with 908 additions and 369 deletions
@@ -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)
@@ -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;
}
}
}
@@ -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);
}
@@ -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<
@@ -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" />
@@ -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,
@@ -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,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;