Compare commits

...

27 Commits

Author SHA1 Message Date
Björn b3f0c3f138 chore: integrating two branches 2025-10-09 18:12:12 +02:00
Björn 7b41a016da Merge branch 'bjorn/clean-up' into bjorn/integrate-linked-models-to-poc 2025-10-09 18:09:26 +02:00
Björn fd5f6a3f29 chore: clean-up 2025-10-09 17:09:34 +02:00
Jedd Morgan 361195f1f1 feat(revit): Use Symbol geometry when it's safe to, and fix linked documents (#1144)
* Hash function

* Use v2 style transform

* extra comments

* experiment1

* correct transform logic and disposal

* corrected transform logic

* simplify (maybe) the transform combination
2025-10-09 15:00:50 +01:00
Jedd Morgan dc5467de88 Merge branch 'oguzhan/display-value-proxies' into jrm/experimental-linked-document 2025-10-09 12:21:42 +01:00
Jedd Morgan 48441a3630 simplify (maybe) the transform combination 2025-10-09 12:20:26 +01:00
Jedd Morgan c08a3e6129 corrected transform logic 2025-10-09 11:32:06 +01:00
Jedd Morgan 43bbe4efd0 correct transform logic and disposal 2025-10-09 11:11:36 +01:00
Jedd Morgan 1b94fd9901 experiment1 2025-10-08 17:33:01 +01:00
Jedd Morgan cfed10e10f extra comments 2025-10-08 16:01:43 +01:00
Jedd Morgan 723c3dc25d Use v2 style transform 2025-10-08 15:54:10 +01:00
Jedd Morgan a5ccad03c2 Merge pull request #1142 from specklesystems/jedd/cnx-2657-hashing-the-meshes
feat(revit)!: Use Hash function for mesh geometry instance ids
2025-10-08 17:13:06 +03:00
Jedd Morgan c06067bfc4 Hash function 2025-10-08 12:50:42 +01:00
Björn aaec369de8 fix: doc context for cached elements 2025-10-08 13:49:31 +02:00
Björn de5129a128 fix: update cache 2025-10-07 22:31:21 +02:00
Björn 01d25c3e6d feat: implement linked model element caching in converter 2025-10-07 22:23:42 +02:00
Björn d7a16998bc feat: register LinkedModelElementCache 2025-10-07 21:39:05 +02:00
Björn cd3a3d2d3b chore: campsite 2025-10-07 21:28:57 +02:00
Björn 04ab4d5f1e feat: add LinkedModelElementCache for linked models 2025-10-07 21:28:41 +02:00
Björn 8ed18ac1d5 Merge branch 'oguzhan/display-value-proxies' into bjorn/integrate-linked-models-to-poc 2025-10-07 18:22:35 +02:00
Björn b22c92919d fix: custom mesh id logic 2025-10-07 15:57:15 +02:00
oguzhankoral 96ba945f51 Fix transform issues 2025-10-02 20:32:21 +03:00
Björn 938f8926b3 docs: notes from chatty-chat 2025-10-02 15:22:45 +02:00
oguzhankoral 90ecf20582 Handle revit receive 2025-10-02 07:31:54 +03:00
oguzhankoral a1d9c0093d Handle autocad, rhino and sketchup receives 2025-10-01 22:30:33 +03:00
oguzhankoral 26c0558a14 some fixes 2025-09-30 11:04:03 +03:00
oguzhankoral 025a99da22 POC 2025-09-29 23:52:21 +03:00
17 changed files with 548 additions and 74 deletions
@@ -1,7 +1,7 @@
using Autodesk.Revit.DB;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.Common.Caching;
// using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Extensions;
using Speckle.Connectors.Common.Operations;
@@ -21,7 +21,7 @@ namespace Speckle.Connectors.Revit.Operations.Send;
public class RevitRootObjectBuilder(
IRootToSpeckleConverter converter,
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ISendConversionCache sendConversionCache,
//ISendConversionCache sendConversionCache,
ElementUnpacker elementUnpacker,
LevelUnpacker levelUnpacker,
IThreadContext threadContext,
@@ -48,6 +48,7 @@ public class RevitRootObjectBuilder(
CancellationToken cancellationToken
)
{
Console.WriteLine(projectId);
var doc = converterSettings.Current.Document;
if (doc.IsFamilyDocument)
@@ -130,7 +131,7 @@ public class RevitRootObjectBuilder(
}
var countProgress = 0;
var cacheHitCount = 0;
// var cacheHitCount = 0;
var skippedObjectCount = 0;
foreach (var atomicObjectByDocumentAndTransform in atomicObjectsByDocumentAndTransform)
@@ -183,26 +184,17 @@ 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
if (!hasTransform && sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
if (hasTransform)
{
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;
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,
@@ -248,6 +240,15 @@ public class RevitRootObjectBuilder(
var levelProxies = levelUnpacker.Unpack(flatElements);
rootObject[ProxyKeys.LEVEL] = levelProxies;
rootObject[ProxyKeys.INSTANCE_DEFINITION] = revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.Values;
rootObject.elements.Add(
new Collection()
{
elements = revitToSpeckleCacheSingleton.InstancedObjects.Values.ToList(),
name = "revitInstancedObjects"
}
);
// NOTE: these are currently not used anywhere, we'll skip them until someone calls for it back
// rootObject[ProxyKeys.PARAMETER_DEFINITIONS] = _parameterDefinitionHandler.Definitions;
@@ -1,8 +1,9 @@
using Microsoft.Extensions.Logging;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects;
using Speckle.Sdk;
using Speckle.Sdk.Common;
@@ -18,11 +19,11 @@ public sealed class DisplayValueExtractor
List<SOG.Mesh>
> _meshByMaterialConverter;
private readonly IScalingServiceToSpeckle _toSpeckleScalingService;
private readonly ITypedConverter<DB.Curve, ICurve> _curveConverter;
private readonly ITypedConverter<DB.PolyLine, SOG.Polyline> _polylineConverter;
private readonly ITypedConverter<DB.Point, SOG.Point> _pointConverter;
private readonly ITypedConverter<DB.PointCloudInstance, SOG.Pointcloud> _pointcloudConverter;
private readonly ILogger<DisplayValueExtractor> _logger;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
public DisplayValueExtractor(
@@ -34,8 +35,8 @@ public sealed class DisplayValueExtractor
ITypedConverter<DB.PolyLine, SOG.Polyline> polylineConverter,
ITypedConverter<DB.Point, SOG.Point> pointConverter,
ITypedConverter<DB.PointCloudInstance, SOG.Pointcloud> pointcloudConverter,
ILogger<DisplayValueExtractor> logger,
IConverterSettingsStore<RevitConversionSettings> converterSettings
IConverterSettingsStore<RevitConversionSettings> converterSettings,
IScalingServiceToSpeckle toSpeckleScalingService
)
{
_meshByMaterialConverter = meshByMaterialConverter;
@@ -43,30 +44,30 @@ public sealed class DisplayValueExtractor
_polylineConverter = polylineConverter;
_pointConverter = pointConverter;
_pointcloudConverter = pointcloudConverter;
_logger = logger;
_converterSettings = converterSettings;
_toSpeckleScalingService = toSpeckleScalingService;
}
public List<Base> GetDisplayValue(DB.Element element)
public List<DisplayValueResult> GetDisplayValue(DB.Element element)
{
switch (element)
{
// get custom (anything not using element.get_geometry) display values
case DB.PointCloudInstance pointcloud:
return new() { _pointcloudConverter.Convert(pointcloud) };
return [DisplayValueResult.WithoutTransform(_pointcloudConverter.Convert(pointcloud))];
case DB.ModelCurve modelCurve:
return new() { GetCurveDisplayValue(modelCurve.GeometryCurve) };
return [DisplayValueResult.WithoutTransform(GetCurveDisplayValue(modelCurve.GeometryCurve))];
case DB.Grid grid:
return new() { GetCurveDisplayValue(grid.Curve) };
return [DisplayValueResult.WithoutTransform(GetCurveDisplayValue(grid.Curve))];
case DB.Area area:
List<Base> areaDisplay = new();
List<DisplayValueResult> areaDisplay = new();
using (var options = new DB.SpatialElementBoundaryOptions())
{
foreach (IList<DB.BoundarySegment> boundarySegmentGroup in area.GetBoundarySegments(options))
{
foreach (DB.BoundarySegment boundarySegment in boundarySegmentGroup)
{
areaDisplay.Add(GetCurveDisplayValue(boundarySegment.GetCurve()));
areaDisplay.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(boundarySegment.GetCurve())));
}
}
}
@@ -87,7 +88,7 @@ public sealed class DisplayValueExtractor
return wall.CurtainGrid is not null || wall.IsStackedWall ? new() : GetGeometryDisplayValue(element);
// railings should also include toprail which need to be retrieved separately
case DBA.Railing railing:
List<Base> railingDisplay = GetGeometryDisplayValue(railing);
List<DisplayValueResult> railingDisplay = GetGeometryDisplayValue(railing);
if (railing.TopRail != DB.ElementId.InvalidElementId)
{
var topRail = _converterSettings.Current.Document.GetElement(railing.TopRail);
@@ -105,10 +106,21 @@ public sealed class DisplayValueExtractor
private Base GetCurveDisplayValue(DB.Curve curve) => (Base)_curveConverter.Convert(curve);
private List<Base> GetGeometryDisplayValue(DB.Element element, DB.Options? options = null)
private List<DisplayValueResult> GetGeometryDisplayValue(DB.Element element, DB.Options? options = null)
{
var collections = GetSortedGeometryFromElement(element, options);
return ProcessGeometryCollections(element, collections);
using DB.Transform? localToDocument = GetTransform(element);
using DB.Transform? documentToLocal = localToDocument?.Inverse;
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
using DB.Transform? compoundTransform =
localToDocument is not null && documentToWorld is not null
? documentToWorld.Multiply(localToDocument)
: localToDocument; // don't want to accidentally dispose of the ReferencePointTransform
DB.Transform? localToWorld = compoundTransform ?? documentToWorld;
var collections = GetSortedGeometryFromElement(element, options, documentToLocal);
return ProcessGeometryCollections(element, collections, localToWorld);
}
/// <summary>
@@ -119,7 +131,15 @@ public sealed class DisplayValueExtractor
/// Note: Some special element types (like Rebar) cannot use this method as their
/// get_Geometry() returns null, requiring specialized extraction methods.
/// </remarks>
private GeometryCollections GetSortedGeometryFromElement(DB.Element element, DB.Options? options)
/// <param name="element"></param>
/// <param name="options"></param>
/// <param name="worldToLocal"></param>
/// <returns></returns>
private GeometryCollections GetSortedGeometryFromElement(
DB.Element element,
DB.Options? options,
DB.Transform? worldToLocal
)
{
//options = ViewSpecificOptions ?? options ?? new Options() { DetailLevel = DetailLevelSetting };
options ??= new DB.Options { DetailLevel = _detailLevelMap[_converterSettings.Current.DetailLevel] };
@@ -142,7 +162,7 @@ public sealed class DisplayValueExtractor
if (geom != null && geom.Any())
{
// retrieves all meshes and solids from a geometry element
SortGeometry(element, collections, geom);
SortGeometry(element, collections, geom, worldToLocal);
}
return collections;
@@ -155,36 +175,83 @@ public sealed class DisplayValueExtractor
/// <remarks>
/// Essentially all the ensuing steps after the common get_Geometry element method
/// </remarks>
private List<Base> ProcessGeometryCollections(DB.Element element, GeometryCollections collections)
private List<DisplayValueResult> ProcessGeometryCollections(
DB.Element element,
GeometryCollections collections,
DB.Transform? localToWorld
)
{
List<Base> displayValue = new();
// handle all solids and meshes by their material
var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids);
List<SOG.Mesh> displayMeshes = _meshByMaterialConverter.Convert(
(meshesByMaterial, element.Id, ShouldSetElementDisplayToTransparent(element))
);
displayValue.AddRange(displayMeshes);
// add rest of geometry
List<DisplayValueResult> displayValue = new(collections.TotalCount);
Matrix4x4? matrix = localToWorld is not null ? TransformToMatrix(localToWorld) : null;
foreach (SOG.Mesh mesh in displayMeshes)
{
displayValue.Add(
matrix.HasValue
? DisplayValueResult.WithTransform(mesh, matrix.Value)
: DisplayValueResult.WithoutTransform(mesh)
);
}
// add rest of geometry (always without transform)
foreach (var curve in collections.Curves)
{
displayValue.Add(GetCurveDisplayValue(curve));
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
foreach (var polyline in collections.Polylines)
{
displayValue.Add(_polylineConverter.Convert(polyline));
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(polyline)));
}
foreach (var point in collections.Points)
{
displayValue.Add(_pointConverter.Convert(point));
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(point)));
}
return displayValue;
}
private Matrix4x4 TransformToMatrix(DB.Transform transform) =>
new()
{
M11 = transform.BasisX.X,
M21 = transform.BasisX.Y,
M31 = transform.BasisX.Z,
M41 = 0,
M12 = transform.BasisY.X,
M22 = transform.BasisY.Y,
M32 = transform.BasisY.Z,
M42 = 0,
M13 = transform.BasisZ.X,
M23 = transform.BasisZ.Y,
M33 = transform.BasisZ.Z,
M43 = 0,
M14 = _toSpeckleScalingService.ScaleLength(transform.Origin.X),
M24 = _toSpeckleScalingService.ScaleLength(transform.Origin.Y),
M34 = _toSpeckleScalingService.ScaleLength(transform.Origin.Z),
M44 = 1
};
private static DB.Transform? GetTransform(DB.Element element)
{
if (element is DB.Instance i)
{
return i.GetTotalTransform();
}
return null;
}
private static Dictionary<DB.ElementId, List<DB.Mesh>> GetMeshesByMaterial(
List<DB.Mesh> meshes,
List<DB.Solid> solids
@@ -249,7 +316,12 @@ public sealed class DisplayValueExtractor
///
/// Note: this is basically a geometry unpacker for all types of geometry
/// </summary>
private void SortGeometry(DB.Element element, GeometryCollections collections, DB.GeometryElement geom)
private void SortGeometry(
DB.Element element,
GeometryCollections collections,
DB.GeometryElement geom,
DB.Transform? worldToLocal
)
{
foreach (DB.GeometryObject geomObj in geom)
{
@@ -267,13 +339,22 @@ public sealed class DisplayValueExtractor
continue;
}
if (worldToLocal is not null)
{
solid = DB.SolidUtils.CreateTransformed(solid, worldToLocal);
}
collections.Solids.Add(solid);
break;
case DB.Mesh mesh:
if (worldToLocal is not null)
{
mesh = mesh.get_Transformed(worldToLocal);
}
collections.Meshes.Add(mesh);
break;
//Note, we're not applying transforms to curves/polylines/points because ProcessGeometryCollections expects them in world coordinates
case DB.Curve curve:
collections.Curves.Add(curve);
break;
@@ -288,12 +369,19 @@ public sealed class DisplayValueExtractor
case DB.GeometryInstance instance:
// element transforms should not be carried down into nested geometryInstances.
// Nested geomInstances should have their geom retreived with GetInstanceGeom, not GetSymbolGeom
SortGeometry(element, collections, instance.GetInstanceGeometry());
// 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:
SortGeometry(element, collections, geometryElement);
SortGeometry(element, collections, geometryElement, null);
break;
}
}
@@ -424,25 +512,23 @@ public sealed class DisplayValueExtractor
/// Instead, we use GetFullGeometryForView() to obtain the geometry and then process it
/// using the standard geometry sorting and conversion.
/// </remarks>
private List<Base> GetRebarVolumetricDisplayValue(DB.Structure.Rebar rebar)
private List<DisplayValueResult> GetRebarVolumetricDisplayValue(DB.Structure.Rebar rebar)
{
var collections = new GeometryCollections();
// Regular get_Geometry() returns null for rebar, so we need to use GetFullGeometryForView
// ❗NOTE: view detail level needs to be fine in order for this to work
// Same behaviour as sending structural frame though - consistent and therefore okay.
DB.GeometryElement geometryElements = rebar.GetFullGeometryForView(_converterSettings.Current.Document.ActiveView);
SortGeometry(rebar, collections, geometryElements);
DB.GeometryElement? geometryElements = rebar.GetFullGeometryForView(_converterSettings.Current.Document.ActiveView);
if (geometryElements != null)
{
SortGeometry(rebar, collections, geometryElements);
return ProcessGeometryCollections(rebar, collections);
SortGeometry(rebar, collections, geometryElements, null);
return ProcessGeometryCollections(rebar, collections, null);
}
// Return empty list if no geometry is found - imo not critical
return new List<Base>();
return new List<DisplayValueResult>();
}
/// <summary>
@@ -451,7 +537,7 @@ public sealed class DisplayValueExtractor
/// <remarks>
/// This method extracts the centerlines of rebar elements when a simplified representation is preferred.
/// </remarks>
private List<Base> GetRebarCenterlineDisplayValue(DB.Structure.Rebar rebar)
private List<DisplayValueResult> GetRebarCenterlineDisplayValue(DB.Structure.Rebar rebar)
{
bool isSingleLayout = rebar.LayoutRule == DB.Structure.RebarLayoutRule.Single;
int numberOfBarPositions = rebar.NumberOfBarPositions;
@@ -480,10 +566,10 @@ public sealed class DisplayValueExtractor
);
}
List<Base> displayValue = new();
List<DisplayValueResult> displayValue = new();
foreach (var curve in curves)
{
displayValue.Add(GetCurveDisplayValue(curve));
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
return displayValue;
@@ -494,6 +580,10 @@ public sealed class DisplayValueExtractor
/// Used to pass multiple geometry collections as a single parameter to improve code readability
/// and reduce the risk of parameter ordering errors.
/// </summary>
/// <remarks>
/// <see cref="Solids"/> and <see cref="Meshes"/> potentially in local coordinate space.
/// For now, <see cref="Curves"/>, <see cref="Polylines"/>, <see cref="Points"/> will always be in world space
/// </remarks>
private sealed record GeometryCollections
{
public List<DB.Solid> Solids { get; } = new();
@@ -501,5 +591,7 @@ public sealed class DisplayValueExtractor
public List<DB.Curve> Curves { get; } = new();
public List<DB.PolyLine> Polylines { get; } = new();
public List<DB.Point> Points { get; } = new();
public int TotalCount => Solids.Count + Meshes.Count + Curves.Count + Polylines.Count + Points.Count;
}
}
@@ -0,0 +1,22 @@
using Speckle.DoubleNumerics;
using Speckle.Sdk.Models;
namespace Speckle.Converters.RevitShared.Helpers;
/// <summary>
/// Represents a display value extracted from a Revit element, optionally with a transform matrix for instancing.
/// </summary>
/// <param name="Geometry">The extracted geometry as a Speckle Base object</param>
/// <param name="Transform">Optional transform matrix for instanced geometry. Null for non-instanced geometry.</param>
public readonly record struct DisplayValueResult(Base Geometry, Matrix4x4? Transform)
{
/// <summary>
/// Creates a display value result without a transform (non-instanced geometry).
/// </summary>
public static DisplayValueResult WithoutTransform(Base geometry) => new(geometry, null);
/// <summary>
/// Creates a display value result with a transform (instanced geometry).
/// </summary>
public static DisplayValueResult WithTransform(Base geometry, Matrix4x4 transform) => new(geometry, transform);
}
@@ -0,0 +1,64 @@
using System.Diagnostics.CodeAnalysis;
using Speckle.Sdk.Models;
namespace Speckle.Converters.RevitShared.Helpers;
/// <summary>
/// Caches converted elements from linked models to avoid redundant conversions when the same linked model is instanced
/// multiple times.
/// </summary>
/// <remarks>
/// Scoped per send operation. Don't think we can reliably have change tracking on linked model elements, so we clear
/// the cache per operation rather than risk stale data. Also, aim of this cache is to avoid re-converting same elements
/// across multiple instances of same linked model. So, not a cache in the RevitToSpeckleCacheSingleton sense.
/// </remarks>
public sealed class LinkedModelElementCacheScoped
{
private readonly Dictionary<string, Base> _cache = [];
// TODO: delete - these are just for dev logging
public int CacheMisses { get; private set; }
public int CacheHits { get; private set; }
public double CacheHitRate => (CacheHits + CacheMisses) == 0 ? 0 : (CacheHits * 100) / (CacheHits + CacheMisses);
/// <summary>
/// Attempts to retrieve a cached element from a linked model.
/// </summary>
public bool TryGetCachedElement(
string documentPath,
string elementUniqueId,
[NotNullWhen(true)] out Base? cachedElement
)
{
string key = CreateCacheKey(documentPath, elementUniqueId);
if (_cache.TryGetValue(key, out cachedElement))
{
CacheHits++;
return true;
}
CacheMisses++;
return false;
}
/// <summary>
/// Stores a converted element in the cache.
/// </summary>
public void StoreCachedElement(string documentPath, string elementUniqueId, Base convertedElement)
{
string key = CreateCacheKey(documentPath, elementUniqueId);
_cache[key] = convertedElement;
}
public void Clear() => _cache.Clear();
/// <summary>
/// Creates a unique cache key by combining document path and element ID.
/// </summary>
/// <remarks>
/// Defensively adding document path as key suffix for the (unlikely) occurence of same element ID across different models.
/// </remarks>
private static string CreateCacheKey(string documentPath, string elementUniqueId) =>
$"{documentPath}_{elementUniqueId}";
}
@@ -1,4 +1,5 @@
using Autodesk.Revit.DB;
using Speckle.DoubleNumerics;
namespace Speckle.Converters.RevitShared.Helpers;
@@ -43,6 +44,30 @@ public static class ReferencePointHelper
};
}
public static Matrix4x4 TransformToMatrix(Transform transform) =>
new()
{
M11 = transform.BasisX.X,
M21 = transform.BasisX.Y,
M31 = transform.BasisX.Z,
M41 = 0,
M12 = transform.BasisY.X,
M22 = transform.BasisY.Y,
M32 = transform.BasisY.Z,
M42 = 0,
M13 = transform.BasisZ.X,
M23 = transform.BasisZ.Y,
M33 = transform.BasisZ.Z,
M43 = 0,
M14 = transform.Origin.X,
M24 = transform.Origin.Y,
M34 = transform.Origin.Z,
M44 = 1
};
/// <summary>
/// Extracts and reconstructs a transform from the matrix data stored on root object
/// </summary>
@@ -1,4 +1,6 @@
using Speckle.Objects.Other;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.RevitShared.Helpers;
@@ -24,6 +26,10 @@ public class RevitToSpeckleCacheSingleton
/// </summary>
public Dictionary<string, Dictionary<string, RenderMaterialProxy>> ObjectRenderMaterialProxiesMap { get; } = new();
public Dictionary<string, InstanceDefinitionProxy> InstanceDefinitionProxiesMap { get; } = new();
public Dictionary<string, Base> InstancedObjects { get; } = new();
/// <summary>
/// Returns the merged material proxy list for the given object ids. Use this to get post conversion a correct list of material proxies for setting on the root commit object.
/// </summary>
@@ -2,7 +2,6 @@ using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Converters.Common;
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;
@@ -19,7 +18,8 @@ public static class ServiceRegistration
var converterAssembly = Assembly.GetExecutingAssembly();
//register types by default
serviceCollection.AddMatchingInterfacesAsTransient(converterAssembly);
// Register single root
// register single root
serviceCollection.AddRootCommon<RevitRootToSpeckleConverter>(converterAssembly);
// register all application converters
@@ -30,6 +30,7 @@ public static class ServiceRegistration
serviceCollection.AddSingleton(new RevitToHostCacheSingleton());
serviceCollection.AddSingleton(new RevitToSpeckleCacheSingleton());
serviceCollection.AddScoped<LinkedModelElementCacheScoped>();
// POC: do we need ToSpeckleScalingService as is, do we need to interface it out?
serviceCollection.AddScoped<ScalingServiceToSpeckle>();
@@ -9,7 +9,9 @@
<Import_RootNamespace>Speckle.Converters.RevitShared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Helpers\DisplayValueResult.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\LevelExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\LinkedModelElementCacheScoped.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ReferencePointHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\RevitToHostCacheSingleton.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\RevitToSpeckleCacheSingleton.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.RevitShared.ToSpeckle;
public readonly struct StructuralAssetProperties(
string name,
@@ -36,12 +36,9 @@ public class MeshListConversionToSpeckle : ITypedConverter<List<DB.Mesh>, SOG.Me
foreach (DB.XYZ vert in mesh.Vertices)
{
// We need this method to take into account reference point transforms
DB.XYZ extVert = _referencePointConverter.ConvertToExternalCoordinates(vert, true);
vertices.Add(_toSpeckleScalingService.ScaleLength(extVert.X));
vertices.Add(_toSpeckleScalingService.ScaleLength(extVert.Y));
vertices.Add(_toSpeckleScalingService.ScaleLength(extVert.Z));
vertices.Add(_toSpeckleScalingService.ScaleLength(vert.X));
vertices.Add(_toSpeckleScalingService.ScaleLength(vert.Y));
vertices.Add(_toSpeckleScalingService.ScaleLength(vert.Z));
}
for (int i = 0; i < mesh.NumTriangles; i++)
@@ -1,6 +1,5 @@
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.Sdk.Common.Exceptions;
@@ -1,12 +1,16 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.Common.ToSpeckle;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Converters.RevitShared.ToSpeckle.Properties;
using Speckle.DoubleNumerics;
using Speckle.Objects.Data;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.RevitShared.ToSpeckle;
@@ -18,9 +22,13 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
private readonly ITypedConverter<DB.Location, Base> _locationConverter;
private readonly LevelExtractor _levelExtractor;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly RevitToSpeckleCacheSingleton _revitToSpeckleCacheSingleton;
private readonly LinkedModelElementCacheScoped _linkedModelElementCacheScoped;
public ElementTopLevelConverterToSpeckle(
DisplayValueExtractor displayValueExtractor,
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
LinkedModelElementCacheScoped linkedModelElementCacheScoped,
PropertiesExtractor propertiesExtractor,
LevelExtractor levelExtractor,
ITypedConverter<DB.Location, Base> locationConverter,
@@ -28,6 +36,8 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
)
{
_displayValueExtractor = displayValueExtractor;
_revitToSpeckleCacheSingleton = revitToSpeckleCacheSingleton;
_linkedModelElementCacheScoped = linkedModelElementCacheScoped;
_propertiesExtractor = propertiesExtractor;
_levelExtractor = levelExtractor;
_locationConverter = locationConverter;
@@ -36,8 +46,44 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
public Base Convert(object target) => Convert((DB.Element)target);
public RevitObject Convert(DB.Element target)
private RevitObject Convert(DB.Element target)
{
// for linked model elements, check the cache. an "early exit" with cached properties saves expensive re-extraction
// this happens when we have multiple instances of the same linked model.
if (target.Document.IsLinked)
{
if (
_linkedModelElementCacheScoped.TryGetCachedElement(
target.Document.PathName,
target.UniqueId,
out Base? cachedElement
)
)
{
var cachedRevitObject = (RevitObject)cachedElement;
// Re-extract display values (different per instance due to transforms)
// but reuse everything else (properties, location, level, etc.)
List<DisplayValueResult> freshDisplayValues = _displayValueExtractor.GetDisplayValue(target);
List<Base> freshProxifiedDisplayValues = ProcessDisplayValues(freshDisplayValues);
// Create new RevitObject with cached properties but fresh display values
return new RevitObject
{
name = cachedRevitObject.name,
type = cachedRevitObject.type,
family = cachedRevitObject.family,
level = cachedRevitObject.level,
category = cachedRevitObject.category,
location = cachedRevitObject.location,
elements = cachedRevitObject.elements,
displayValue = freshProxifiedDisplayValues, // ← only this is fresh
properties = cachedRevitObject.properties,
units = cachedRevitObject.units
};
}
}
string category = target.Category?.Name ?? "none";
// special case for direct shapes: use builtin category instead
@@ -95,7 +141,10 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
}
// get the display value
List<Base> displayValue = _displayValueExtractor.GetDisplayValue(target);
List<DisplayValueResult> displayValuesWithTransforms = _displayValueExtractor.GetDisplayValue(target);
// process display values and create instance proxies where applicable
List<Base> proxifiedDisplayValues = ProcessDisplayValues(displayValuesWithTransforms);
// get level
string? level = _levelExtractor.GetLevelName(target);
@@ -117,11 +166,17 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
category = category,
location = convertedLocation,
elements = children,
displayValue = displayValue.Cast<Base>().ToList(),
displayValue = proxifiedDisplayValues,
properties = properties,
units = _converterSettings.Current.SpeckleUnits
};
// store in cache if linked model element
if (target.Document.IsLinked)
{
_linkedModelElementCacheScoped.StoreCachedElement(target.Document.PathName, target.UniqueId, revitObject);
}
return revitObject;
}
@@ -184,4 +239,69 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
yield return Convert(_converterSettings.Current.Document.GetElement(childId));
}
}
/// <summary>
/// Processes display values with transforms and creates instance proxies for meshes that can be instanced.
/// </summary>
/// <returns>List of processed display values, with meshes replaced by instance proxies where applicable</returns>
private List<Base> ProcessDisplayValues(List<DisplayValueResult> displayValues)
{
List<Base> proxifiedDisplayValues = new();
foreach (var displayValue in displayValues)
{
// check if this is a mesh with a transform - potential instance scenario
// assumption here is that if we have matrix for corresponding base it is instance-able
if (displayValue.Geometry is SOG.Mesh mesh && displayValue.Transform is not null)
{
var instanceProxy = CreateOrGetInstanceProxy(mesh, displayValue.Transform.Value);
proxifiedDisplayValues.Add(instanceProxy);
}
else
{
proxifiedDisplayValues.Add(displayValue.Geometry);
}
}
return proxifiedDisplayValues;
}
/// <summary>
/// Creates or retrieves an instance proxy for a mesh, managing instance definitions and caching.
/// </summary>
private InstanceProxy CreateOrGetInstanceProxy(SOG.Mesh mesh, Matrix4x4 transform)
{
var instanceDefinitionId = MeshInstanceIdGenerator.GenerateUntransformedMeshId(mesh);
// ensure instance definition exists
if (!_revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.ContainsKey(instanceDefinitionId))
{
var newInstanceDefinition = new InstanceDefinitionProxy
{
applicationId = instanceDefinitionId,
objects = new List<string> { mesh.applicationId.NotNull() },
maxDepth = 1,
name = instanceDefinitionId,
};
_revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.Add(instanceDefinitionId, newInstanceDefinition);
}
// cache the untransformed mesh object if not already cached
if (!_revitToSpeckleCacheSingleton.InstancedObjects.ContainsKey(instanceDefinitionId))
{
_revitToSpeckleCacheSingleton.InstancedObjects.Add(instanceDefinitionId, mesh);
}
// create and return instance proxy with transform
var instanceProxy = new InstanceProxy
{
applicationId = Guid.NewGuid().ToString(),
definitionId = instanceDefinitionId,
transform = transform,
maxDepth = 1,
units = mesh.units
};
return instanceProxy;
}
}
@@ -1,5 +1,6 @@
using Speckle.DoubleNumerics;
using Speckle.InterfaceGenerator;
using Speckle.Objects.Data;
using Speckle.Sdk.Dependencies;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.GraphTraversal;
@@ -36,6 +37,19 @@ public class LocalToGlobalUnpacker : ILocalToGlobalUnpacker
{
atomicObjects.Add((objectToUnpack, objectToUnpack.Current));
}
if (objectToUnpack.Current is DataObject dataObject)
{
foreach (Base displayValue in dataObject.displayValue)
{
if (displayValue is InstanceProxy instanceProxyInDisplayValue)
{
instanceProxies.Add(
(new TraversalContext(instanceProxyInDisplayValue, parent: objectToUnpack), instanceProxyInDisplayValue)
);
}
}
}
}
var objectsAtAbsolute = new HashSet<(TraversalContext tc, Base obj)>();
@@ -1,4 +1,5 @@
using Speckle.Objects.Other;
using Speckle.Objects.Data;
using Speckle.Objects.Other;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.GraphTraversal;
@@ -64,6 +65,17 @@ public class RootObjectUnpacker
{
atomicObjects.Add(tc);
}
if (tc.Current is DataObject dataObject)
{
foreach (var displayValue in dataObject.displayValue)
{
if (displayValue is IInstanceComponent)
{
instanceComponents.Add(new TraversalContext(displayValue, parent: tc));
}
}
}
}
return (atomicObjects, instanceComponents);
}
@@ -0,0 +1,29 @@
using NUnit.Framework;
using Speckle.Converters.Common.ToSpeckle;
namespace Speckle.Converters.Common.Tests.ToSpeckle;
public class MeshInstanceIdGeneratorTests
{
private static IEnumerable<List<double>> TestCases()
{
int[] testCases = [0, 1, 100, 1_000_000];
foreach (int testLength in testCases)
{
yield return Enumerable
.Range(0, testLength)
.Select(_ => TestContext.CurrentContext.Random.NextDouble(float.MinValue, float.MaxValue))
.ToList();
}
}
[Test]
[TestCaseSource(nameof(TestCases))]
public void TestEquivalentImplementations(List<double> vertices)
{
var result = MeshInstanceIdGenerator.GenerateUntransformedMeshId(vertices);
var resultSpan = MeshInstanceIdGenerator.GenerateUntransformedMeshId_Span(vertices);
Assert.That(result, Is.EqualTo(resultSpan));
}
}
@@ -4,6 +4,10 @@
<TargetFrameworks>net48;net8.0</TargetFrameworks>
<Configurations>Debug;Release;Local</Configurations>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Speckle.Converters.Common.Tests"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
@@ -0,0 +1,86 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Speckle.InterfaceGenerator;
using Speckle.Objects.Geometry;
using Speckle.Sdk.Common;
#if NET6_0_OR_GREATER
using System.Runtime.InteropServices;
#endif
namespace Speckle.Converters.Common.ToSpeckle;
[GenerateAutoInterface]
public static class MeshInstanceIdGenerator
{
/// <summary>
/// Generate a unique hash from the vertex data of a mesh.
/// This is a "good enough" way to compare the equality of meshes.
/// Note, does not consider other mesh data, only <see cref="Mesh.vertices"/>
/// </summary>
/// <remarks>
/// There are two implementations of this function because NET Framework lacks some of the Marshall and Span based functions.
/// However, their external behaviour is the same.
/// </remarks>
/// <param name="mesh"></param>
/// <returns></returns>
[Pure]
public static string GenerateUntransformedMeshId(Mesh mesh)
{
#if NET6_0_OR_GREATER
return GenerateUntransformedMeshId_Span(mesh.vertices);
#else
return GenerateUntransformedMeshId(mesh.vertices);
#endif
}
#if NET6_0_OR_GREATER
[Pure]
internal static string GenerateUntransformedMeshId_Span(List<double> vertices)
{
ReadOnlySpan<double> span = CollectionsMarshal.AsSpan(vertices);
ReadOnlySpan<byte> inputBytes = MemoryMarshal.AsBytes(span);
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(inputBytes, hash);
return Convert.ToHexString(hash);
}
#endif
[Pure]
[SuppressMessage(
"Performance",
"CA1850:Prefer static \'HashData\' method over \'ComputeHash\'",
Justification = "Already another overload for .NET Core"
)]
internal static string GenerateUntransformedMeshId(List<double> vertices)
{
double[] verts = (double[])s_listItemsField.GetValue(vertices).NotNull();
int byteCount = verts.Length * sizeof(double);
byte[] inputBytes = new byte[byteCount];
Buffer.BlockCopy(verts, 0, inputBytes, 0, byteCount);
// Compute the SHA256 hash
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(inputBytes);
// Convert hash to hex string (uppercase, similar to Convert.ToHexString)
StringBuilder sb = new(hashBytes.Length * 2);
foreach (byte b in hashBytes)
{
sb.AppendFormat("{0:X2}", b);
}
return sb.ToString();
}
}
private static readonly FieldInfo s_listItemsField = typeof(List<double>)
.GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)
.NotNull();
}