Compare commits

...

21 Commits

Author SHA1 Message Date
oguzhankoral 9e16a6cd85 properties poc 2025-10-14 13:00:17 +03:00
Björn Steinhagen 95070bfc65 fix: material assignment on revit receive (#1146) 2025-10-14 12:59:44 +03:00
oguzhankoral 7c5ddf1553 Clear instance proxies per conversion 2025-10-14 11:54:59 +03:00
Björn Steinhagen e746548c81 refactor(revit): replace tuples with DisplayValueResult record for display values (#1145) 2025-10-10 14:52:14 +08:00
Jedd Morgan e52ed561e2 Merge remote-tracking branch 'origin/dev' into oguzhan/display-value-proxies 2025-10-09 16:24:36 +01: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 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
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
19 changed files with 501 additions and 102 deletions
@@ -50,7 +50,7 @@ public class LevelUnpacker
{
name = level.Name,
displayValue = [],
properties = _propertiesExtractor.GetProperties(level)
properties = new Dictionary<string, object?>() { }
};
var unitSettings = _converterSettings.Current.Document.GetUnits();
var lengthUnitType = unitSettings.GetFormatOptions(Autodesk.Revit.DB.SpecTypeId.Length).GetUnitTypeId();
@@ -110,7 +110,8 @@ public sealed class RevitHostObjectBuilder(
// TODO: TransformTo and material baking needs to be fixed in Revit!!
// create a mapping from original to modified IDs <- so that we can actually map ids in the proxies to the objects
Dictionary<string, string> originalToModifiedIds = new();
// as part of CNX-2677, we have a one-to-many problem. many instances share the same reference, so we use a list
Dictionary<string, List<string>> originalToModifiedIds = new();
// modify application IDs BEFORE material baking
foreach (LocalToGlobalMap localToGlobalMap in localToGlobalMaps)
@@ -139,7 +140,13 @@ public sealed class RevitHostObjectBuilder(
string modifiedAppId = $"{originalAppId}_{Guid.NewGuid().ToString("N")[..8]}";
if (originalAppId != null)
{
originalToModifiedIds[originalAppId] = modifiedAppId;
if (!originalToModifiedIds.TryGetValue(originalAppId, out List<string>? modifiedIds))
{
modifiedIds = new List<string>();
originalToModifiedIds[originalAppId] = modifiedIds;
}
modifiedIds.Add(modifiedAppId);
}
localToGlobalMap.AtomicObject.applicationId = modifiedAppId;
@@ -152,14 +159,20 @@ public sealed class RevitHostObjectBuilder(
{
foreach (var proxy in unpackedRoot.RenderMaterialProxies)
{
var updatedObjects = new List<string>();
var objectIdsToUse = new List<string>();
foreach (var objectId in proxy.objects)
{
// Use the modified ID if it exists, otherwise keep the original <- this SUCKS and we need to change
string idToUse = originalToModifiedIds.TryGetValue(objectId, out var modifiedId) ? modifiedId : objectId;
updatedObjects.Add(idToUse);
if (originalToModifiedIds.TryGetValue(objectId, out var modifiedIds))
{
objectIdsToUse.AddRange(modifiedIds);
}
else
{
objectIdsToUse.Add(objectId);
}
}
proxy.objects = updatedObjects;
proxy.objects = objectIdsToUse;
}
}
@@ -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,18 @@ public class RevitRootObjectBuilder(
var levelProxies = levelUnpacker.Unpack(flatElements);
rootObject[ProxyKeys.LEVEL] = levelProxies;
rootObject[ProxyKeys.INSTANCE_DEFINITION] =
revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.Values.ToList();
rootObject.elements.Add(
new Collection()
{
elements = revitToSpeckleCacheSingleton.InstancedObjects.Values.ToList(),
name = "revitInstancedObjects"
}
);
revitToSpeckleCacheSingleton.ClearInstanceProxies();
// 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,10 @@
using Microsoft.Extensions.Logging;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.Common.ToSpeckle;
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 +20,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 +36,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 +45,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 +89,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 +107,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 +132,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 +163,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 +176,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 +317,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 +340,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 +370,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 +513,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 +538,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 +567,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 +581,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 +592,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;
}
}
@@ -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>
@@ -55,4 +61,10 @@ public class RevitToSpeckleCacheSingleton
}
return mergeTarget.Values.ToList();
}
public void ClearInstanceProxies()
{
InstanceDefinitionProxiesMap.Clear();
InstancedObjects.Clear();
}
}
@@ -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
@@ -3,6 +3,7 @@ using Speckle.Converters.Common;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Sdk;
using Speckle.Sdk.Models;
namespace Speckle.Converters.RevitShared.ToSpeckle;
@@ -42,15 +43,24 @@ public class ParameterExtractor
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public Dictionary<string, object?> GetParameters(DB.Element element)
public Base GetParameters(DB.Element element)
{
var instanceParams = new Base();
instanceParams["instanceParameters"] = ParseParameterSet(element.Parameters);
var typeParams = new Base();
typeParams["typeParameters"] = GetTypeParameterDictionary(element);
// NOTE: Woe and despair, I'm really abusing dictionaries here. See note at the top of class.
return new Dictionary<string, object?>()
{
["Instance Parameters"] = ParseParameterSet(element.Parameters),
["Type Parameters"] = GetTypeParameterDictionary(element),
["System Type Parameters"] = GetSystemTypeParameterDictionary(element)
};
var props = new Base();
props["@Instance Parameters"] = instanceParams;
props["@Type Parameters"] = typeParams;
props["System Type Parameters"] = GetSystemTypeParameterDictionary(element);
// return new Dictionary<string, object?>()
// {
// ["@Instance Parameters"] = instanceParams,
// ["@Type Parameters"] = typeParams,
// ["System Type Parameters"] = GetSystemTypeParameterDictionary(element)
// };
return props;
}
private Dictionary<string, Dictionary<string, object?>>? GetTypeParameterDictionary(DB.Element element)
@@ -1,4 +1,5 @@
using Speckle.Converters.Common.Objects;
using Speckle.Sdk.Models;
namespace Speckle.Converters.RevitShared.ToSpeckle.Properties;
@@ -19,25 +20,23 @@ public class PropertiesExtractor
_materialQuantityConverter = materialQuantityConverter;
}
public Dictionary<string, object?> GetProperties(DB.Element element)
public Base GetProperties(DB.Element element)
{
var props = new Base();
// by default, always get class properties first
Dictionary<string, object?> properties = _classPropertiesExtractor.GetClassProperties(element);
props["@classProps"] = _classPropertiesExtractor.GetClassProperties(element);
// add material quantities
Dictionary<string, object> matQuantities = _materialQuantityConverter.Convert(element);
if (matQuantities.Count > 0)
{
properties.Add("Material Quantities", matQuantities);
}
props["@matProps"] = _materialQuantityConverter.Convert(element);
// if (matQuantities.Count > 0)
// {
// properties.Add("Material Quantities", matQuantities);
// }
// add parameters
Dictionary<string, object?> parameters = _parameterExtractor.GetParameters(element);
if (parameters.Count > 0)
{
properties.Add("Parameters", parameters);
}
Base parameters = _parameterExtractor.GetParameters(element);
props["@parameters"] = parameters;
return properties;
return parameters;
}
}
@@ -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.Properties;
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,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.RevitShared.ToSpeckle.Properties;
using Speckle.Sdk.Common.Exceptions;
using ApplicationException = Autodesk.Revit.Exceptions.ApplicationException;
@@ -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,11 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
private readonly ITypedConverter<DB.Location, Base> _locationConverter;
private readonly LevelExtractor _levelExtractor;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly RevitToSpeckleCacheSingleton _revitToSpeckleCacheSingleton;
public ElementTopLevelConverterToSpeckle(
DisplayValueExtractor displayValueExtractor,
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
PropertiesExtractor propertiesExtractor,
LevelExtractor levelExtractor,
ITypedConverter<DB.Location, Base> locationConverter,
@@ -28,6 +34,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
)
{
_displayValueExtractor = displayValueExtractor;
_revitToSpeckleCacheSingleton = revitToSpeckleCacheSingleton;
_propertiesExtractor = propertiesExtractor;
_levelExtractor = levelExtractor;
_locationConverter = locationConverter;
@@ -95,7 +102,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);
@@ -105,7 +115,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
var children = GetElementChildren(target).ToList();
// get properties
Dictionary<string, object?> properties = _propertiesExtractor.GetProperties(target);
Base properties = _propertiesExtractor.GetProperties(target);
RevitObject revitObject =
new()
@@ -117,10 +127,11 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
category = category,
location = convertedLocation,
elements = children,
displayValue = displayValue.Cast<Base>().ToList(),
properties = properties,
displayValue = proxifiedDisplayValues,
properties = new Dictionary<string, object?>() { },
units = _converterSettings.Current.SpeckleUnits
};
revitObject["@serializedProperties"] = properties;
return revitObject;
}
@@ -184,4 +195,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 = 0,
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 = 0,
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,25 @@
using Speckle.DoubleNumerics;
using Speckle.Sdk.Models;
namespace Speckle.Converters.Common.ToSpeckle;
/// <summary>
/// Represents a display value extracted from a host app 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>
/// <remarks>
/// Seems unnecessary, but reads nicely (self-documenting) in usage in my opinion (clear intent).
/// </remarks>
public static DisplayValueResult WithTransform(Base geometry, Matrix4x4 transform) => new(geometry, transform);
}
@@ -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();
}