Compare commits

..

9 Commits

Author SHA1 Message Date
Jonathon Broughton 4c42138732 updates category mapping for Civil 3D interoperability
Modifies send settings for better compatibility with Civil 3D mappings.

Enhances logging for category mapping to assist in debugging and monitoring.

Adds additional category mappings to improve conversion accuracy.
2026-01-12 18:10:18 +00:00
Jonathon Broughton 8c84f673df Refactors constructor 2025-12-20 10:25:12 +00:00
Jonathon Broughton 6157cc9d40 Updates method visibility for send settings
Changes the visibility of the send settings method to virtual, enabling overrides in derived classes.
2025-12-19 22:54:44 +00:00
Jonathon Broughton c7e35f6776 Enhance PropertiesExtractor for Revit category mapping 2025-12-19 22:45:04 +00:00
Jonathon Broughton 73c426057a Implement Civil3DBuiltInCategoryExtractor
Update projitems for Civil3DBuiltInCategoryExtractor
2025-12-19 22:45:03 +00:00
Jonathon Broughton 3245bb2be2 Update Civil3dConversionSettings to include Revit category mapping
Update Civil3dConversionSettingsFactory to support Revit category mapping
2025-12-19 22:45:00 +00:00
Jonathon Broughton aeec0976af Implement ToSpeckleSettingsManagerCivil3d
Update projitems for new Civil3D send settings
2025-12-19 22:44:58 +00:00
Jonathon Broughton 18005abc3d Implement Revit category mapping setting for Civil3D 2025-12-19 22:44:57 +00:00
Jonathon Broughton a98e1ccfaa Add new settings and dependencies for Civil3D
Add Civil3D settings to Dependency Injection
2025-12-19 22:44:56 +00:00
19 changed files with 254 additions and 174 deletions
@@ -125,7 +125,7 @@ public abstract class AutocadSendBaseBinding : ISendBinding
public List<ISendFilter> GetSendFilters() => _sendFilters;
public List<ICardSetting> GetSendSettings() => [];
public virtual List<ICardSetting> GetSendSettings() => [];
public async Task Send(string modelCardId) =>
await _threadContext.RunOnMainAsync(async () => await SendInternal(modelCardId));
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Speckle.Connectors.Autocad.Bindings;
using Speckle.Connectors.Civil3dShared.Operations.Send.Settings;
using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.Common.Cancellation;
using Speckle.Connectors.Common.Threading;
@@ -7,56 +8,63 @@ using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.DUI.Models.Card.SendFilter;
using Speckle.Connectors.DUI.Settings;
using Speckle.Converters.Autocad;
using Speckle.Converters.Civil3dShared;
using Speckle.Converters.Common;
namespace Speckle.Connectors.Civil3dShared.Bindings;
public sealed class Civil3dSendBinding : AutocadSendBaseBinding
{
private readonly ICivil3dConversionSettingsFactory _civil3dConversionSettingsFactory;
private readonly IAutocadConversionSettingsFactory _autocadConversionSettingsFactory;
public Civil3dSendBinding(
DocumentModelStore store,
IBrowserBridge parent,
IEnumerable<ISendFilter> sendFilters,
ICancellationManager cancellationManager,
ISendConversionCache sendConversionCache,
ICivil3dConversionSettingsFactory civil3dConversionSettingsFactory,
IAutocadConversionSettingsFactory autocadConversionSettingsFactory,
IThreadContext threadContext,
ITopLevelExceptionHandler topLevelExceptionHandler,
IAppIdleManager appIdleManager,
ISendOperationManagerFactory sendOperationManagerFactory
public sealed class Civil3dSendBinding(
DocumentModelStore store,
IBrowserBridge parent,
IEnumerable<ISendFilter> sendFilters,
ICancellationManager cancellationManager,
ISendConversionCache sendConversionCache,
ICivil3dConversionSettingsFactory civil3dConversionSettingsFactory,
IAutocadConversionSettingsFactory autocadConversionSettingsFactory,
IToSpeckleSettingsManagerCivil3d toSpeckleSettingsManagerCivil3d,
IThreadContext threadContext,
ITopLevelExceptionHandler topLevelExceptionHandler,
IAppIdleManager appIdleManager,
ISendOperationManagerFactory sendOperationManagerFactory
)
: AutocadSendBaseBinding(
store,
parent,
sendFilters,
cancellationManager,
sendConversionCache,
threadContext,
topLevelExceptionHandler,
appIdleManager,
sendOperationManagerFactory
)
: base(
store,
parent,
sendFilters,
cancellationManager,
sendConversionCache,
threadContext,
topLevelExceptionHandler,
appIdleManager,
sendOperationManagerFactory
)
{
_civil3dConversionSettingsFactory = civil3dConversionSettingsFactory;
_autocadConversionSettingsFactory = autocadConversionSettingsFactory;
}
{
private readonly DocumentModelStore _store = store;
public override List<ICardSetting> GetSendSettings() => [new RevitCategoryMappingSetting(true)];
// POC: we're registering the conversion settings for autocad here because we need the autocad conversion settings to be able to use the autocad typed converters.
// POC: We need a separate send binding for civil3d due to using a different unit converter (needed for conversion settings construction)
// POC: We need a separate a send binding for Civil3d due to using a different unit converter (needed for conversion settings construction)
protected override void InitializeSettings(IServiceProvider serviceProvider)
{
// Get the model card from the store to access user settings
var modelCard = _store.GetSenders().FirstOrDefault();
bool mappingToRevitCategories = false;
if (modelCard != null)
{
mappingToRevitCategories = toSpeckleSettingsManagerCivil3d.GetMappingToRevitCategories(modelCard);
}
serviceProvider
.GetRequiredService<IConverterSettingsStore<Civil3dConversionSettings>>()
.Initialize(_civil3dConversionSettingsFactory.Create(Application.DocumentManager.CurrentDocument));
.Initialize(
civil3dConversionSettingsFactory.Create(Application.DocumentManager.CurrentDocument, mappingToRevitCategories)
);
serviceProvider
.GetRequiredService<IConverterSettingsStore<AutocadConversionSettings>>()
.Initialize(_autocadConversionSettingsFactory.Create(Application.DocumentManager.CurrentDocument));
.Initialize(autocadConversionSettingsFactory.Create(Application.DocumentManager.CurrentDocument));
}
}
@@ -6,6 +6,7 @@ using Speckle.Connectors.Civil3dShared.Bindings;
using Speckle.Connectors.Civil3dShared.HostApp;
using Speckle.Connectors.Civil3dShared.Operations.Receive;
using Speckle.Connectors.Civil3dShared.Operations.Send;
using Speckle.Connectors.Civil3dShared.Operations.Send.Settings;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Converters.Civil3dShared.ToSpeckle;
@@ -24,6 +25,9 @@ public static class Civil3dConnectorModule
serviceCollection.AddScoped<IRootObjectBuilder<AutocadRootObject>, Civil3dRootObjectBuilder>();
serviceCollection.AddSingleton<IBinding, Civil3dSendBinding>();
// send settings
serviceCollection.AddSingleton<ToSpeckleSettingsManagerCivil3d>();
// add receive
serviceCollection.LoadReceive();
serviceCollection.AddScoped<IHostObjectBuilder, Civil3dHostObjectBuilder>();
@@ -0,0 +1,12 @@
using Speckle.Connectors.DUI.Settings;
namespace Speckle.Connectors.Civil3dShared.Operations.Send.Settings;
public class RevitCategoryMappingSetting(bool value) : ICardSetting
{
public string? Id { get; set; } = "mappingToRevitCategories";
public string? Title { get; set; } = "Map to Revit Categories";
public string? Type { get; set; } = "boolean";
public List<string>? Enum { get; set; }
public object? Value { get; set; } = value;
}
@@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.DUI.Models.Card;
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Common;
namespace Speckle.Connectors.Civil3dShared.Operations.Send.Settings;
[GenerateAutoInterface]
public class ToSpeckleSettingsManagerCivil3d : IToSpeckleSettingsManagerCivil3d
{
private readonly ISendConversionCache _sendConversionCache;
private readonly Dictionary<string, bool?> _revitCategoryMappingCache = [];
public ToSpeckleSettingsManagerCivil3d(ISendConversionCache sendConversionCache)
{
_sendConversionCache = sendConversionCache;
}
public bool GetMappingToRevitCategories([NotNull] SenderModelCard modelCard)
{
var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "mappingToRevitCategories")?.Value as bool?;
var returnValue = value != null && value.NotNull();
if (_revitCategoryMappingCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue))
{
if (previousValue != returnValue)
{
EvictCacheForModelCard(modelCard);
}
}
_revitCategoryMappingCache[modelCard.ModelCardId] = returnValue;
return returnValue;
}
private void EvictCacheForModelCard(SenderModelCard modelCard)
{
var objectIds = modelCard.SendFilter != null ? modelCard.SendFilter.NotNull().SelectedObjectIds : [];
_sendConversionCache.EvictObjects(objectIds);
}
}
@@ -14,6 +14,8 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\PropertySetBaker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\Civil3dHostObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Civil3dRootObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\RevitCategoryMappingSetting.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\ToSpeckleSettingsManagerCivil3d.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bindings\Civil3dSendBinding.cs" />
</ItemGroup>
<ItemGroup>
@@ -21,5 +23,6 @@
<Folder Include="$(MSBuildThisFileDirectory)HostApp\" />
<Folder Include="$(MSBuildThisFileDirectory)Operations\Receive\" />
<Folder Include="$(MSBuildThisFileDirectory)Operations\Send\" />
<Folder Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\" />
</ItemGroup>
</Project>
</Project>
@@ -42,11 +42,7 @@ public class RevitRootObjectBuilder(
() => Task.FromResult(BuildSync(documentElementContexts, projectId, onOperationProgressed, ct))
);
#pragma warning disable CA1506
#pragma warning disable CA1502
private RootObjectBuilderResult BuildSync(
#pragma warning restore CA1506
#pragma warning restore CA1502
IReadOnlyList<DocumentToConvert> documentElementContexts,
string projectId,
IProgress<CardProgress> onOperationProgressed,
@@ -60,9 +56,6 @@ public class RevitRootObjectBuilder(
throw new SpeckleException("Family Environment documents are not supported.");
}
// create a new send pipeline
using var sendPipeline = new Speckle.Sdk.Pipeline.Send();
// init the root
Collection rootObject =
new() { name = converterSettings.Current.Document.PathName.Split('\\').Last().Split('.').First() };
@@ -191,12 +184,10 @@ public class RevitRootObjectBuilder(
// 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
bool wasCached = false;
if (!hasTransform && sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
{
// TODO: cahce hit
converted = value;
wasCached = true;
cacheHitCount++;
}
// not in cache means we convert
@@ -215,12 +206,6 @@ public class RevitRootObjectBuilder(
converted.applicationId = applicationId;
}
var reference = sendPipeline.Process(converted).Result; // .Wait(cancellationToken);//.ConfigureAwait(false);
if (!wasCached)
{
sendConversionCache.AppendSendResult(projectId, applicationId, reference);
}
var collection = sendCollectionManager.GetAndCreateObjectHostCollection(
revitElement,
rootObject,
@@ -228,7 +213,7 @@ public class RevitRootObjectBuilder(
modelDisplayName
);
collection.elements.Add(reference);
collection.elements.Add(converted);
results.Add(new(Status.SUCCESS, applicationId, sourceType, converted));
}
catch (Exception ex) when (!ex.IsFatal())
@@ -269,20 +254,13 @@ public class RevitRootObjectBuilder(
rootObject[ProxyKeys.INSTANCE_DEFINITION] = revitToSpeckleCacheSingleton.GetInstanceDefinitionProxiesForObjects(
idsAndSubElementIds
);
// NOTE: i might be overdoing things in here, but tldr:
// - all instance objects (meshes) are processed individually
// - process their collection individually, and then attach it to the root collection
// we could, theoretically, just process the collection as a whole (but it can be big?)
// note/ask: do these need to go in the conversion cache? or not?
var instanceObjects = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds);
var instanceReferences = new Collection("revitInstancedObjects");
foreach (var instanceObject in instanceObjects)
{
var referenceInstanceObject = sendPipeline.Process(instanceObject).Result;
instanceReferences.elements.Add(referenceInstanceObject);
}
var instanceReferenceCollection = sendPipeline.Process(instanceReferences).Result;
rootObject.elements.Add(instanceReferenceCollection);
rootObject.elements.Add(
new Collection()
{
elements = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds),
name = "revitInstancedObjects"
}
);
// STEP 6: Unpack all other objects to attach to root collection
List<Objects.Other.Camera> views = viewUnpacker.Unpack(converterSettings.Current.Document);
@@ -301,10 +279,6 @@ public class RevitRootObjectBuilder(
rootObject[RootKeys.REFERENCE_POINT_TRANSFORM] = transformMatrix;
}
// NOTE: could be
sendPipeline.Process(rootObject).Wait(cancellationToken);
sendPipeline.WaitForUpload().Wait(cancellationToken);
return new RootObjectBuilderResult(new Collection() { name = "ignore" }, results);
return new RootObjectBuilderResult(rootObject, results);
}
}
@@ -75,8 +75,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
)
{
using var activity = _activityFactory.Start("Build");
using var sendPipeline = new Speckle.Sdk.Pipeline.Send();
// 0 - Init the root
Collection rootObjectCollection = new() { name = _converterSettings.Current.Document.Name ?? "Unnamed document" };
rootObjectCollection["units"] = _converterSettings.Current.SpeckleUnits;
@@ -99,7 +97,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
// 3 - Convert atomic objects
List<SendConversionResult> results = new(atomicObjects.Count);
int count = 0;
using (var _ = _activityFactory.Start("Convert all"))
{
foreach (RhinoObject rhinoObject in atomicObjects)
@@ -111,8 +108,9 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
Layer layer = _converterSettings.Current.Document.Layers[rhinoObject.Attributes.LayerIndex];
Collection collectionHost = _layerUnpacker.GetHostObjectCollection(layer, rootObjectCollection);
var result = await ConvertRhinoObject(rhinoObject, collectionHost, instanceProxies, projectId, sendPipeline);
var result = ConvertRhinoObject(rhinoObject, collectionHost, instanceProxies, projectId);
results.Add(result);
++count;
onOperationProgressed.Report(new("Converting", (double)count / atomicObjects.Count));
await Task.Yield();
@@ -151,23 +149,18 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
}
}
await sendPipeline.Process(rootObjectCollection);
await sendPipeline.WaitForUpload();
return new RootObjectBuilderResult(new Collection() { name = "ignore" }, results);
return new RootObjectBuilderResult(rootObjectCollection, results);
}
private async Task<SendConversionResult> ConvertRhinoObject(
private SendConversionResult ConvertRhinoObject(
RhinoObject rhinoObject,
Collection collectionHost,
IReadOnlyDictionary<string, InstanceProxy> instanceProxies,
string projectId,
Sdk.Pipeline.Send sendPipeline
string projectId
)
{
string applicationId = rhinoObject.Id.ToString();
string sourceType = rhinoObject.ObjectType.ToString();
bool wasCached = false;
try
{
// get from cache or convert:
@@ -181,7 +174,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
else if (_sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
{
converted = value;
wasCached = true;
}
else
{
@@ -202,17 +194,10 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
converted["properties"] = properties;
}
// process in pipeline
var reference = await sendPipeline.Process(converted).ConfigureAwait(false);
if (!wasCached)
{
_sendConversionCache.AppendSendResult(projectId, applicationId, reference);
}
// add to host
collectionHost.elements.Add(reference);
collectionHost.elements.Add(converted);
return new(Status.SUCCESS, applicationId, sourceType, reference);
return new(Status.SUCCESS, applicationId, sourceType, converted);
}
catch (Exception ex) when (!ex.IsFatal())
{
@@ -6,7 +6,7 @@ using Speckle.Connectors.Rhino.DependencyInjection;
using Speckle.Converters.Rhino;
using Speckle.Sdk;
using Speckle.Sdk.Models.Extensions;
namespace Speckle.Connectors.Rhino.Plugin;
///<summary>
@@ -1,3 +1,3 @@
namespace Speckle.Converters.Civil3dShared;
public record Civil3dConversionSettings(Document Document, string SpeckleUnits);
public record Civil3dConversionSettings(Document Document, string SpeckleUnits, bool MappingToRevitCategories = false);
@@ -7,8 +7,8 @@ namespace Speckle.Converters.Civil3dShared;
public class Civil3dConversionSettingsFactory(IHostToSpeckleUnitConverter<AAEC.BuiltInUnit> unitsConverter)
: ICivil3dConversionSettingsFactory
{
public Civil3dConversionSettings Create(Document document) =>
new(document, unitsConverter.ConvertOrThrow(GetDocBuiltInUnit(document)));
public Civil3dConversionSettings Create(Document document, bool mappingToRevitCategories = false) =>
new(document, unitsConverter.ConvertOrThrow(GetDocBuiltInUnit(document)), mappingToRevitCategories);
private static AAEC.BuiltInUnit GetDocBuiltInUnit(Document doc)
{
@@ -0,0 +1,47 @@
using Speckle.InterfaceGenerator;
namespace Speckle.Converters.Civil3dShared.Helpers;
[GenerateAutoInterface]
public class Civil3DBuiltInCategoryExtractor : ICivil3DBuiltInCategoryExtractor
{
internal const string DEFAULT_DICT_KEY = "builtInCategory";
public bool TryGetBuiltInCategory(ADB.Entity entity, out string mapped)
{
mapped = string.Empty;
var rx = entity.GetRXClass();
var name = rx?.Name;
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
var builtInCategory = Civil3DClassToRevitBuiltInCategory(name!);
if (string.Equals(builtInCategory, name, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"{name}: {builtInCategory}");
return false; // no mapping
}
mapped = builtInCategory;
Console.WriteLine($"{name}: {builtInCategory} -> {mapped}");
return true;
}
private static readonly Dictionary<string, string> s_civil3dClassMap =
new(StringComparer.OrdinalIgnoreCase)
{
["AeccDbSurfaceTin"] = "OST_Topography",
["AeccDbPipe"] = "OST_PlaceHolderPipes",
["AeccDbStructure"] = "OST_GenericModel"
// ["Structure"] = "OST_PipeFitting"
};
private static string Civil3DClassToRevitBuiltInCategory(string className) =>
s_civil3dClassMap.TryGetValue(className, out var ost) ? ost : className;
}
@@ -15,6 +15,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Civil3dToSpeckleUnitConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SpeckleApplicationIdExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsings.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\Civil3DBuiltInCategoryExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\CorridorDisplayValueExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\CorridorHandler.cs" />
@@ -1,3 +1,6 @@
using Speckle.Converters.Civil3dShared.Helpers;
using Speckle.Converters.Common;
namespace Speckle.Converters.Civil3dShared.ToSpeckle;
/// <summary>
@@ -9,18 +12,24 @@ public class PropertiesExtractor : Speckle.Converters.AutocadShared.ToSpeckle.IP
private readonly PartDataExtractor _partDataExtractor;
private readonly PropertySetExtractor _propertySetExtractor;
private readonly ExtensionDictionaryExtractor _extensionDictionaryExtractor;
private readonly ICivil3DBuiltInCategoryExtractor _builtInCategoryExtractor;
private readonly IConverterSettingsStore<Civil3dConversionSettings> _settingsStore;
public PropertiesExtractor(
ClassPropertiesExtractor classPropertiesExtractor,
PartDataExtractor partDataExtractor,
PropertySetExtractor propertySetExtractor,
ExtensionDictionaryExtractor extensionDictionaryExtractor
ExtensionDictionaryExtractor extensionDictionaryExtractor,
ICivil3DBuiltInCategoryExtractor builtInCategoryExtractor,
IConverterSettingsStore<Civil3dConversionSettings> settingsStore
)
{
_classPropertiesExtractor = classPropertiesExtractor;
_partDataExtractor = partDataExtractor;
_propertySetExtractor = propertySetExtractor;
_extensionDictionaryExtractor = extensionDictionaryExtractor;
_builtInCategoryExtractor = builtInCategoryExtractor;
_settingsStore = settingsStore;
}
public Dictionary<string, object?> GetProperties(ADB.Entity entity)
@@ -28,6 +37,15 @@ public class PropertiesExtractor : Speckle.Converters.AutocadShared.ToSpeckle.IP
// first get all class properties, which will be at the root level of props dictionary
Dictionary<string, object?> properties = _classPropertiesExtractor.GetClassProperties(entity);
// add built-in category if setting enabled and mapping available
if (
_settingsStore.Current.MappingToRevitCategories
&& _builtInCategoryExtractor.TryGetBuiltInCategory(entity, out string builtInCategory)
)
{
properties[Civil3DBuiltInCategoryExtractor.DEFAULT_DICT_KEY] = builtInCategory;
}
// add part data, property sets, and extension dictionaries to the properties dict
AddDictionaryToPropertyDictionary(_partDataExtractor.GetPartData(entity), "Part Data", properties);
AddDictionaryToPropertyDictionary(_propertySetExtractor.GetPropertySets(entity), "Property Sets", properties);
@@ -4,7 +4,6 @@ using Speckle.Converters.Common.ToSpeckle;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Converters.RevitShared.ToSpeckle;
using Speckle.DoubleNumerics;
using Speckle.Objects;
using Speckle.Sdk;
@@ -165,11 +164,11 @@ public sealed class DisplayValueExtractor
}
/// <summary>
/// Converts sorted geometry into DisplayValueResults <see cref="ElementTopLevelConverterToSpeckle"/>.
/// Processes collections of different geometry types and converts them to display values.
/// Extracted as a common method to reduce code duplication between regular geometry processing and special cases like rebar.
/// </summary>
/// <remarks>
/// Applies localToWorld only to curves, points, polylines.
/// Meshes remain in symbol space to generate correct instance proxies and avoid duplicates.
/// Essentially all the ensuing steps after the common get_Geometry element method
/// </remarks>
private List<DisplayValueResult> ProcessGeometryCollections(
DB.Element element,
@@ -177,35 +176,50 @@ public sealed class DisplayValueExtractor
DB.Transform? localToWorld
)
{
// handle all solids and meshes by their material
var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids);
var displayMeshes = _meshByMaterialConverter.Convert(
List<SOG.Mesh> displayMeshes = _meshByMaterialConverter.Convert(
(meshesByMaterial, element.Id, ShouldSetElementDisplayToTransparent(element))
);
List<DisplayValueResult> displayValue = new(collections.TotalCount);
Matrix4x4? matrix = localToWorld is not null ? TransformToMatrix(localToWorld) : null;
foreach (var mesh in displayMeshes)
foreach (SOG.Mesh mesh in displayMeshes)
{
// if we have a transform, keep mesh in symbol space and attach transform
displayValue.Add(
localToWorld != null
? DisplayValueResult.WithTransform(mesh, TransformToMatrix(localToWorld))
matrix.HasValue
? DisplayValueResult.WithTransform(mesh, matrix.Value)
: DisplayValueResult.WithoutTransform(mesh)
);
}
// transform curves, polylines, and points to world coordinates before conversion.
// Unlike meshes/solids which are proxified with transform matrices, these geometry
// types must have their final world coordinates baked directly into their geometry.
foreach (var curve in collections.Curves)
{
var transformedCurve = localToWorld != null ? curve.CreateTransformed(localToWorld) : curve;
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
if (localToWorld is not null)
{
using var transformedCurve = curve.CreateTransformed(localToWorld);
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
}
else
{
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
}
// Note: Creating new polyline/point instances for transformation isn't ideal for perf,
// but Revit API doesn't provide in-place transform methods. Trade-off is acceptable since
// family instances typically don't have massive numbers of raw polylines/points in their geometry.
foreach (var polyline in collections.Polylines)
{
if (localToWorld != null)
if (localToWorld is not null)
{
var coords = polyline.GetCoordinates().Select(p => localToWorld.OfPoint(p)).ToList();
using var transformedPolyline = DB.PolyLine.Create(coords);
var coords = polyline.GetCoordinates();
var transformedCoords = coords.Select(coord => localToWorld.OfPoint(coord)).ToList();
using var transformedPolyline = DB.PolyLine.Create(transformedCoords);
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(transformedPolyline)));
}
else
@@ -216,7 +230,7 @@ public sealed class DisplayValueExtractor
foreach (var point in collections.Points)
{
if (localToWorld != null)
if (localToWorld is not null)
{
using var transformedPoint = DB.Point.Create(localToWorld.OfPoint(point.Coord));
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(transformedPoint)));
@@ -316,17 +330,23 @@ public sealed class DisplayValueExtractor
};
/// <summary>
/// Sorts element geometry into solids, meshes, curves, polylines, points.
/// According to the remarks on the GeometryInstance class in the RevitAPIDocs,
/// https://www.revitapidocs.com/2024/fe25b14f-5866-ca0f-a660-c157484c3a56.htm,
/// a family instance geometryElement should have a top-level geometry instance when the symbol
/// does not have modified geometry (the docs say that modified geometry will not have a geom instance,
/// however in my experience, all family instances have a top-level geom instance, but if the family instance
/// is modified, then the geom instance won't contain any geometry.)
///
/// This remark also leads me to think that a family instance will not have top-level solids and geom instances.
/// We are logging cases where this is not true.
///
/// Note: this is basically a geometry unpacker for all types of geometry
/// </summary>
/// <remarks>
/// GeometryInstances are processed via GetSymbolGeometry() with accumulated transforms,
/// keeping meshes in symbol space and avoiding double transforms.
/// </remarks>
private void SortGeometry(
DB.Element element,
GeometryCollections collections,
DB.GeometryElement geom,
DB.Transform? accumulatedTransform
DB.Transform? worldToLocal
)
{
foreach (DB.GeometryObject geomObj in geom)
@@ -339,62 +359,56 @@ public sealed class DisplayValueExtractor
switch (geomObj)
{
case DB.Solid solid:
// skip invalid solid
if (solid.Faces.Size == 0)
{
continue;
}
if (accumulatedTransform != null)
if (worldToLocal is not null)
{
// apply transform to bring solid into document/world space
// only apply once to avoid double-transform bugs
solid = DB.SolidUtils.CreateTransformed(solid, accumulatedTransform);
solid = DB.SolidUtils.CreateTransformed(solid, worldToLocal);
}
collections.Solids.Add(solid);
break;
case DB.Mesh mesh:
if (accumulatedTransform != null)
if (worldToLocal is not null)
{
// apply accumulated transform to mesh
// prevents geometry from being incorrectly transformed later [Ref: CNX-2875]
mesh = mesh.get_Transformed(accumulatedTransform);
mesh = mesh.get_Transformed(worldToLocal);
}
collections.Meshes.Add(mesh);
break;
// curves, polylines, and points are transformed to world space in ProcessGeometryCollections,
// not here, because they cannot be proxified like meshes.
case DB.Curve curve:
// curves are stored as-is; transforms are applied later in ProcessGeometryCollections
collections.Curves.Add(curve);
break;
case DB.PolyLine polyline:
// polylines also handled later during display value processing
collections.Polylines.Add(polyline);
break;
case DB.Point point:
// points remain in local space; transformed later if needed
collections.Points.Add(point);
break;
case DB.GeometryInstance instance:
// GeometryInstance.Transform: symbol → parent coordinate system
// multiply with accumulatedTransform to handle nested instances
var instanceTransform = instance.Transform;
var nextTransform =
accumulatedTransform != null ? accumulatedTransform.Multiply(instanceTransform) : instanceTransform;
// always use symbol geometry, never GetInstanceGeometry() [Ref: CNX-2875]
SortGeometry(element, collections, instance.GetSymbolGeometry(), nextTransform);
// element transforms should not be carried down into nested geometryInstances.
// Nested geomInstances should have their geom retrieved with GetInstanceGeom, not GetSymbolGeom
if (worldToLocal == null) //see remark on method for why this is safe to do...
{
SortGeometry(element, collections, instance.GetInstanceGeometry(), null);
}
else
{
SortGeometry(element, collections, instance.GetSymbolGeometry(), null);
}
break;
case DB.GeometryElement geometryElement:
// raw GeometryElement: it has no transform of its own
// pass accumulatedTransform from parent if present
SortGeometry(element, collections, geometryElement, accumulatedTransform);
SortGeometry(element, collections, geometryElement, null);
break;
}
}
@@ -486,26 +500,6 @@ public sealed class DisplayValueExtractor
return currentOptions;
}
// cable trays (and fittings) are MEP system families whose geometry detail is effectively view-driven.
// So, we've seen that, Options.DetailLevel is ignored by get_Geometry() for these categories unless a View is
// explicitly supplied, and Revit will always return a medium-detail representation otherwise [Ref: CNX-2735]
// We force extraction through the active view here (if there is one!)
if (
elementBuiltInCategory == DB.BuiltInCategory.OST_CableTray
|| elementBuiltInCategory == DB.BuiltInCategory.OST_CableTrayFitting
)
{
try
{
return new DB.Options { View = _converterSettings.Current.Document.NotNull().ActiveView };
}
catch (Exception ex) when (!ex.IsFatal())
{
// linked docs or invalid view context fall back to non-view-specific options
return currentOptions;
}
}
// NOTE: On steel elements. This is an incomplete solution.
// If steel element proxies will be sucked in via category selection, and they are not visible in the current view, they will not be extracted out.
// I'm inclined to go with this as a semi-permanent limitation. See:
@@ -42,7 +42,7 @@ internal sealed class RhinoJobHandler(
projectId: job.Payload.ProjectId,
progressMessage: "Starting Up Importer",
sourceData: new(
handlerApplication.Slug,
application.Slug,
application.HostApplicationVersion,
job.Payload.FileName,
file.FileInfo.Length
@@ -14,7 +14,6 @@ namespace Speckle.Connectors.Common.Caching;
public interface ISendConversionCache
{
void StoreSendResult(string projectId, IReadOnlyDictionary<Id, ObjectReference> convertedReferences);
void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference);
/// <summary>
/// <para>Call this method whenever you need to invalidate a set of objects that have changed in the host app.</para>
@@ -11,8 +11,6 @@ public class NullSendConversionCache : ISendConversionCache
{
public void StoreSendResult(string projectId, IReadOnlyDictionary<Id, ObjectReference> convertedReferences) { }
public void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference) { }
public void EvictObjects(IEnumerable<string> objectIds) { }
public void ClearCache() { }
@@ -17,11 +17,6 @@ public class SendConversionCache : ISendConversionCache
}
}
public void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference)
{
Cache[(applicationId, projectId)] = convertedReference;
}
/// <inheritdoc/>
public void EvictObjects(IEnumerable<string> objectIds) =>
Cache = Cache