Compare commits

...

33 Commits

Author SHA1 Message Date
Jedd Morgan 7c7260c603 Merge pull request #1157 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Update dev into main
2025-10-20 15:39:33 +01:00
Oğuzhan Koral bae9e3e0f1 Prevent crashes on unnamed files (#1154)
Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2025-10-20 17:33:00 +03:00
Oğuzhan Koral 26b0394613 feat(revit): display value proxies (#1140)
* POC

* some fixes

* Handle autocad, rhino and sketchup receives

* Handle revit receive

* Fix transform issues

* fix: custom mesh id logic

* Hash function

* Merge pull request #1142 from specklesystems/jedd/cnx-2657-hashing-the-meshes

feat(revit)!: Use Hash function for mesh geometry instance ids

* Use v2 style transform

* extra comments

* experiment1

* correct transform logic and disposal

* corrected transform logic

* simplify (maybe) the transform combination

* refactor(revit): replace tuples with DisplayValueResult record for display values (#1145)

* Clear instance proxies per conversion

* fix: material assignment on revit receive (#1146)

* Fix: enable send caching (#1148)

* Enable caching without definition proxy noise

* Log element id relationship while creating cache to filter after conversion to attach root

* Update RevitRootObjectBuilder.cs

* Clear cache on document swap

* More clean up

* fix(revit): defer material proxy population to prevent duplicate instance mesh IDs (#1155)

* fix(revit): defer material proxy population to prevent duplicate instance mesh IDs

* chore: formatting

* chore: campsite

* refactor: throwing on cache error

* refactor: move material proxy population into cache singleton

* fix: di

* fix(rhino): match cleaned block names when purging instance definitions (#1156)

* fix(rhino): match cleaned block names when purging instance definitions

* refactor: simplification

* chore: comments

* refactor: use .Contains

---------

Co-authored-by: Björn <steinhagen.bjoern@gmail.com>
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
2025-10-20 15:26:56 +01:00
Dogukan Karatas 689ef0bcbe Merge pull request #1149 from specklesystems/dogukan/cnx-2490-receive-property-sets-in-civil-3d
feat (civil): receive property sets
2025-10-17 18:09:25 +02:00
Dogukan Karatas 461585b782 adds additional cast 2025-10-17 17:58:01 +02:00
Claire Kuang ea33f35a7d removes unnecessary casting on send 2025-10-17 16:57:38 +01:00
Claire Kuang 7427f1a2f3 adds constants and better host object builder methods 2025-10-17 16:08:40 +01:00
Dogukan Karatas b7984bf97e add additional cleaning 2025-10-17 15:25:35 +02:00
Dogukan Karatas 9b24a45b6e property set pruge 2025-10-17 12:50:58 +02:00
Claire Kuang 4ace81a422 Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-17 10:02:40 +01:00
Björn Steinhagen a60790c92c fix(grasshopper): account selection not respected when url input connected (#1150)
* fix: account switching on urlInput
* chore: good ol' comments
* chore: adding server url to exception message
2025-10-17 10:02:12 +01:00
Dogukan Karatas fd0d00cac3 Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-16 18:48:01 +02:00
Dogukan Karatas 498396e611 Merge pull request #1153 from specklesystems/dogukan/build-package-version-bump
chore(build): bump Microsoft.Build version
2025-10-16 18:47:18 +02:00
Dogukan Karatas 5444377398 bump version 2025-10-16 18:29:20 +02:00
Dogukan Karatas 9d981f9800 ci check 2025-10-16 17:59:13 +02:00
Dogukan Karatas 14e17fb67d prefix and purge 2025-10-16 17:52:30 +02:00
Dogukan Karatas 0ffa7685fd rather cast than convert 2025-10-16 17:16:20 +02:00
Dogukan Karatas dc7d4671e4 default value simplification 2025-10-16 16:40:37 +02:00
Dogukan Karatas 10cb5cd66f removed the parser 2025-10-16 16:24:53 +02:00
Jedd Morgan cb15d9f77a Merge pull request #1152 from specklesystems/jrm/trigger-ci
Remove arcgis from readme
2025-10-16 15:05:53 +01:00
Jedd Morgan da74faef9b read me change to trigger ci 2025-10-16 15:02:58 +01:00
Claire Kuang 4368833c7e Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-16 09:56:57 +01:00
Dogukan Karatas a20df41316 readonly dict 2025-10-15 22:56:50 +02:00
Dogukan Karatas ccf48dbad1 process property set definitions from root object 2025-10-15 22:42:33 +02:00
Björn Steinhagen 6700aa27bc fix(revit): handle level extraction for face-based family instances (#1151)
* fix: get levels for face-based instances

* fix: level unpacker
2025-10-15 22:06:01 +02:00
Dogukan Karatas df525eab63 moved the baker to the connector 2025-10-15 21:25:12 +02:00
Dogukan Karatas 275901626f created an abstract class 2025-10-15 16:35:19 +02:00
Claire Kuang fac0dc31b2 Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-15 12:48:26 +01:00
Dogukan Karatas 8696eca1f0 use active transaction 2025-10-15 13:12:41 +02:00
Dogukan Karatas d647c71cf5 baker added 2025-10-15 12:25:01 +02:00
Dogukan Karatas 9b218dd808 Revert "created PropertySetConverter"
This reverts commit 112093f914.
2025-10-15 12:06:04 +02:00
Björn Steinhagen 9f39dc521d fix(revit): clamp PBR material properties to avoid receive failures (#1147)
* fix: clamping roughness vaues

* chore: extending to other 0 - 1 values
2025-10-14 16:15:52 +02:00
Dogukan Karatas 112093f914 created PropertySetConverter 2025-10-13 14:08:35 +02:00
38 changed files with 1699 additions and 407 deletions
+9 -9
View File
@@ -16,12 +16,12 @@
},
"Microsoft.Build": {
"type": "Direct",
"requested": "[17.11.4, )",
"resolved": "17.11.4",
"contentHash": "UMC7DfeFEHY2GGHHaghybUuUlLaByFHEFudR2PehMgDBuRuLAUePp1iaa4eFtVzepRzMtIbeSCVJCzzX3NV2Gg==",
"requested": "[17.11.48, )",
"resolved": "17.11.48",
"contentHash": "g8Kn575mNAKcuFotV3C7xvF+IbxuHennl67LH2shL2au1U6UqwReTDygCHyU04+koc2Yn7fHIbVQaC08HqEWow==",
"dependencies": {
"Microsoft.Build.Framework": "17.11.4",
"Microsoft.NET.StringTools": "17.11.4",
"Microsoft.Build.Framework": "17.11.48",
"Microsoft.NET.StringTools": "17.11.48",
"System.Collections.Immutable": "8.0.0",
"System.Configuration.ConfigurationManager": "8.0.0",
"System.Reflection.Metadata": "8.0.0",
@@ -82,8 +82,8 @@
},
"Microsoft.Build.Framework": {
"type": "Transitive",
"resolved": "17.11.4",
"contentHash": "u28uDihlqxtt8h2dL1ZJOZ7TRkxBK+HGr+3FgQpILVo7Q7gErkw8mYW9R+RM5PtxvZTdYb/4MWDL66vdIsANBQ=="
"resolved": "17.11.48",
"contentHash": "C3WIMt2wBl4++NX3jSEpTq5KXBhvAV154R4JrYHkfy9JSBcXWiL0mkgpspk5xSdOj+fS/uz7zluIy6bMM1fkkQ=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
@@ -97,8 +97,8 @@
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
"resolved": "17.11.48",
"contentHash": "0IQo089IGBEC4jgtishauZMVr9ZxOWNiGKeDvyzZlvw7p2r253lJh6IJCLLFWXvZnVrVO5mnsYIPamxFPzM08w=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
@@ -0,0 +1,296 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Extensions;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Converters.Common;
using Speckle.Sdk.Common;
using Speckle.Sdk.Dependencies;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Instances;
using AutocadColor = Autodesk.AutoCAD.Colors.Color;
namespace Speckle.Connectors.Autocad.Operations.Receive;
/// <summary>
/// <para>Base class for AutoCAD host object builders. Expects to be a scoped dependency per receive operation.</para>
/// </summary>
public abstract class AutocadHostObjectBaseBuilder : IHostObjectBuilder
{
private readonly IRootToHostConverter _converter;
private readonly AutocadLayerBaker _layerBaker;
private readonly AutocadGroupBaker _groupBaker;
private readonly AutocadInstanceBaker _instanceBaker;
private readonly IAutocadMaterialBaker _materialBaker;
private readonly IAutocadColorBaker _colorBaker;
private readonly AutocadContext _autocadContext;
private readonly RootObjectUnpacker _rootObjectUnpacker;
private readonly IReceiveConversionHandler _conversionHandler;
protected AutocadHostObjectBaseBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler
)
{
_converter = converter;
_layerBaker = layerBaker;
_groupBaker = groupBaker;
_instanceBaker = instanceBaker;
_materialBaker = materialBaker;
_colorBaker = colorBaker;
_autocadContext = autocadContext;
_rootObjectUnpacker = rootObjectUnpacker;
_conversionHandler = conversionHandler;
}
public Task<HostObjectBuilderResult> Build(
Base rootObject,
string projectName,
string modelName,
IProgress<CardProgress> onOperationProgressed,
CancellationToken cancellationToken
)
{
// Prompt the UI conversion started. Progress bar will swoosh.
onOperationProgressed.Report(new("Converting", null));
// Layer filter for received commit with project and model name
_layerBaker.CreateLayerFilter(projectName, modelName);
// 0 - Clean then Rock n Roll!
string baseLayerPrefix = _autocadContext.RemoveInvalidChars($"SPK-{projectName}-{modelName}-");
PreReceiveDeepClean(baseLayerPrefix);
// 1 - Unpack objects and proxies from root commit object
var unpackedRoot = _rootObjectUnpacker.Unpack(rootObject);
// 2 - Split atomic objects and instance components with their path
var (atomicObjects, instanceComponents) = _rootObjectUnpacker.SplitAtomicObjectsAndInstances(
unpackedRoot.ObjectsToConvert
);
var atomicObjectsWithPath = _layerBaker.GetAtomicObjectsWithPath(atomicObjects);
var instanceComponentsWithPath = _layerBaker.GetInstanceComponentsWithPath(instanceComponents);
// POC: these are not captured by traversal, so we need to re-add them here
if (unpackedRoot.DefinitionProxies != null && unpackedRoot.DefinitionProxies.Count > 0)
{
var transformed = unpackedRoot.DefinitionProxies.Select(proxy =>
(Array.Empty<Collection>(), proxy as IInstanceComponent)
);
instanceComponentsWithPath.AddRange(transformed);
}
// 3 - Parse and bake proxies (materials and colors), as they are used later down the line by layers and objects
if (unpackedRoot.RenderMaterialProxies != null)
{
_materialBaker.ParseAndBakeRenderMaterials(
unpackedRoot.RenderMaterialProxies,
baseLayerPrefix,
onOperationProgressed
);
}
if (unpackedRoot.ColorProxies != null)
{
_colorBaker.ParseColors(unpackedRoot.ColorProxies, onOperationProgressed);
}
// 3.5 - Parse and bake additional proxies that are needed for conversion
ParseAndBakeAdditionalProxies(rootObject, baseLayerPrefix);
// 4 - Convert atomic objects
HashSet<ReceiveConversionResult> results = new();
HashSet<string> bakedObjectIds = new();
Dictionary<string, IReadOnlyCollection<Entity>> applicationIdMap = new();
var count = 0;
foreach (var (layerPath, atomicObject) in atomicObjectsWithPath)
{
onOperationProgressed.Report(new("Converting objects", (double)++count / atomicObjects.Count));
var ex = _conversionHandler.TryConvert(() =>
{
cancellationToken.ThrowIfCancellationRequested();
string objectId = atomicObject.applicationId ?? atomicObject.id.NotNull();
IReadOnlyCollection<Entity> convertedObjects = ConvertObject(atomicObject, layerPath, baseLayerPrefix);
applicationIdMap[objectId] = convertedObjects;
results.UnionWith(
convertedObjects.Select(e => new ReceiveConversionResult(
Status.SUCCESS,
atomicObject,
e.GetSpeckleApplicationId(),
e.GetType().ToString()
))
);
bakedObjectIds.UnionWith(convertedObjects.Select(e => e.GetSpeckleApplicationId()));
});
if (ex != null)
{
results.Add(new(Status.ERROR, atomicObject, null, null, ex));
}
}
// 5 - Convert instances
var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = _instanceBaker.BakeInstances(
instanceComponentsWithPath,
applicationIdMap,
baseLayerPrefix,
onOperationProgressed
);
bakedObjectIds.RemoveWhere(id => consumedObjectIds.Contains(id));
bakedObjectIds.UnionWith(createdInstanceIds);
results.RemoveWhere(result => result.ResultId != null && consumedObjectIds.Contains(result.ResultId));
results.UnionWith(instanceConversionResults);
// 6 - Create groups
if (unpackedRoot.GroupProxies != null)
{
IReadOnlyCollection<ReceiveConversionResult> groupResults = _groupBaker.CreateGroups(
unpackedRoot.GroupProxies,
applicationIdMap
);
results.UnionWith(groupResults);
}
return Task.FromResult(new HostObjectBuilderResult(bakedObjectIds, results));
}
protected void PreReceiveDeepClean(string baseLayerPrefix)
{
_layerBaker.DeleteAllLayersByPrefix(baseLayerPrefix);
_instanceBaker.PurgeInstances(baseLayerPrefix);
_materialBaker.PurgeMaterials(baseLayerPrefix);
PreReceiveAdditionalDeepClean(baseLayerPrefix);
}
/// <summary>
/// Method for adding app-specific additional deep clean of the document prior to receiving.
/// </summary>
protected virtual void PreReceiveAdditionalDeepClean(string baseLayerPrefix) { }
/// <summary>
/// Method for parsing and baking additional app-specific proxies on the root prior to converting and baking objects
/// </summary>
protected virtual void ParseAndBakeAdditionalProxies(Base rootObject, string baseLayerPrefix) { }
private IReadOnlyCollection<Entity> ConvertObject(Base obj, Collection[] layerPath, string baseLayerNamePrefix)
{
string layerName = _layerBaker.CreateLayerForReceive(layerPath, baseLayerNamePrefix);
var convertedEntities = new HashSet<Entity>();
using var tr = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction();
// 1: convert
var converted = _converter.Convert(obj);
// 2: handle result
switch (converted)
{
case Entity entity:
var bakedEntity = BakeObject(entity, obj, layerName, tr);
convertedEntities.Add(bakedEntity);
break;
case List<(Entity, Base)> listConversionResult: // this is from fallback conversion for brep/brepx/subdx/extrusionx/polycurve
var bakedFallbackEntities = BakeObjectsAsGroup(listConversionResult, obj, layerName, baseLayerNamePrefix, tr);
convertedEntities.UnionWith(bakedFallbackEntities);
break;
default:
// TODO: capture defualt case with report object here? Same as in Rhino
break;
}
tr.Commit();
return convertedEntities.Freeze();
}
private Entity BakeObject(
Entity entity,
Base originalObject,
string layerName,
Transaction tr,
Base? parentObject = null
)
{
var objId = originalObject.applicationId ?? originalObject.id.NotNull();
if (_colorBaker.ObjectColorsIdMap.TryGetValue(objId, out AutocadColor? color))
{
entity.Color = color;
}
if (_materialBaker.TryGetMaterialId(originalObject, parentObject, out ObjectId matId))
{
entity.MaterialId = matId;
}
entity.AppendToDb(layerName);
// Hook for derived classes to perform additional operations after entity is added to database
PostBakeEntity(entity, originalObject, tr);
return entity;
}
/// <summary>
/// Method for additional app-specific operations on entities after the entity has been added to the document database.
/// Called after the entity is added to the database in an open transaction
/// </summary>
/// <param name="entity"></param>
/// <param name="originalObject"></param>
/// <param name="tr"></param>
protected virtual void PostBakeEntity(Entity entity, Base originalObject, Transaction tr)
{
// Default implementation does nothing - override in derived classes
}
private List<Entity> BakeObjectsAsGroup(
List<(Entity, Base)> fallbackConversionResult,
Base parentObject,
string layerName,
string baseLayerName,
Transaction tr
)
{
var ids = new ObjectIdCollection();
var entities = new List<Entity>();
foreach (var (conversionResult, originalObject) in fallbackConversionResult)
{
BakeObject(conversionResult, originalObject, layerName, tr, parentObject);
ids.Add(conversionResult.ObjectId);
entities.Add(conversionResult);
}
if (entities.Count <= 1) // return if empty list or only one, because we don't want to create empty or single item groups.
{
return entities;
}
var groupDictionary = (DBDictionary)
tr.GetObject(Application.DocumentManager.CurrentDocument.Database.GroupDictionaryId, OpenMode.ForWrite);
var groupName = _autocadContext.RemoveInvalidChars(
$@"{parentObject.speckle_type.Split('.').Last()} - {parentObject.applicationId ?? parentObject.id} ({baseLayerName})"
);
var newGroup = new Group(groupName, true);
newGroup.Append(ids);
groupDictionary.UpgradeOpen();
groupDictionary.SetAt(groupName, newGroup);
tr.AddNewlyCreatedDBObject(newGroup, true);
return entities;
}
}
@@ -1,238 +1,35 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Extensions;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Converters.Common;
using Speckle.Sdk.Common;
using Speckle.Sdk.Dependencies;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Instances;
using AutocadColor = Autodesk.AutoCAD.Colors.Color;
namespace Speckle.Connectors.Autocad.Operations.Receive;
/// <summary>
/// <para>Expects to be a scoped dependency per receive operation.</para>
/// <para>AutoCAD-specific host object builder. Expects to be a scoped dependency per receive operation.</para>
/// </summary>
public class AutocadHostObjectBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler
) : IHostObjectBuilder
public sealed class AutocadHostObjectBuilder : AutocadHostObjectBaseBuilder
{
public Task<HostObjectBuilderResult> Build(
Base rootObject,
string projectName,
string modelName,
IProgress<CardProgress> onOperationProgressed,
CancellationToken cancellationToken
public AutocadHostObjectBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler
)
{
// Prompt the UI conversion started. Progress bar will swoosh.
onOperationProgressed.Report(new("Converting", null));
// Layer filter for received commit with project and model name
layerBaker.CreateLayerFilter(projectName, modelName);
// 0 - Clean then Rock n Roll!
string baseLayerPrefix = autocadContext.RemoveInvalidChars($"SPK-{projectName}-{modelName}-");
PreReceiveDeepClean(baseLayerPrefix);
// 1 - Unpack objects and proxies from root commit object
var unpackedRoot = rootObjectUnpacker.Unpack(rootObject);
// 2 - Split atomic objects and instance components with their path
var (atomicObjects, instanceComponents) = rootObjectUnpacker.SplitAtomicObjectsAndInstances(
unpackedRoot.ObjectsToConvert
);
var atomicObjectsWithPath = layerBaker.GetAtomicObjectsWithPath(atomicObjects);
var instanceComponentsWithPath = layerBaker.GetInstanceComponentsWithPath(instanceComponents);
// POC: these are not captured by traversal, so we need to re-add them here
if (unpackedRoot.DefinitionProxies != null && unpackedRoot.DefinitionProxies.Count > 0)
{
var transformed = unpackedRoot.DefinitionProxies.Select(proxy =>
(Array.Empty<Collection>(), proxy as IInstanceComponent)
);
instanceComponentsWithPath.AddRange(transformed);
}
// 3 - Bake materials and colors, as they are used later down the line by layers and objects
if (unpackedRoot.RenderMaterialProxies != null)
{
materialBaker.ParseAndBakeRenderMaterials(
unpackedRoot.RenderMaterialProxies,
baseLayerPrefix,
onOperationProgressed
);
}
if (unpackedRoot.ColorProxies != null)
{
colorBaker.ParseColors(unpackedRoot.ColorProxies, onOperationProgressed);
}
// 4 - Convert atomic objects
HashSet<ReceiveConversionResult> results = new();
HashSet<string> bakedObjectIds = new();
Dictionary<string, IReadOnlyCollection<Entity>> applicationIdMap = new();
var count = 0;
foreach (var (layerPath, atomicObject) in atomicObjectsWithPath)
{
onOperationProgressed.Report(new("Converting objects", (double)++count / atomicObjects.Count));
var ex = conversionHandler.TryConvert(() =>
{
cancellationToken.ThrowIfCancellationRequested();
string objectId = atomicObject.applicationId ?? atomicObject.id.NotNull();
IReadOnlyCollection<Entity> convertedObjects = ConvertObject(atomicObject, layerPath, baseLayerPrefix);
applicationIdMap[objectId] = convertedObjects;
results.UnionWith(
convertedObjects.Select(e => new ReceiveConversionResult(
Status.SUCCESS,
atomicObject,
e.GetSpeckleApplicationId(),
e.GetType().ToString()
))
);
bakedObjectIds.UnionWith(convertedObjects.Select(e => e.GetSpeckleApplicationId()));
});
if (ex != null)
{
results.Add(new(Status.ERROR, atomicObject, null, null, ex));
}
}
// 5 - Convert instances
var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = instanceBaker.BakeInstances(
instanceComponentsWithPath,
applicationIdMap,
baseLayerPrefix,
onOperationProgressed
);
bakedObjectIds.RemoveWhere(id => consumedObjectIds.Contains(id));
bakedObjectIds.UnionWith(createdInstanceIds);
results.RemoveWhere(result => result.ResultId != null && consumedObjectIds.Contains(result.ResultId));
results.UnionWith(instanceConversionResults);
// 6 - Create groups
if (unpackedRoot.GroupProxies != null)
{
IReadOnlyCollection<ReceiveConversionResult> groupResults = groupBaker.CreateGroups(
unpackedRoot.GroupProxies,
applicationIdMap
);
results.UnionWith(groupResults);
}
return Task.FromResult(new HostObjectBuilderResult(bakedObjectIds, results));
}
private void PreReceiveDeepClean(string baseLayerPrefix)
{
layerBaker.DeleteAllLayersByPrefix(baseLayerPrefix);
instanceBaker.PurgeInstances(baseLayerPrefix);
materialBaker.PurgeMaterials(baseLayerPrefix);
}
private IReadOnlyCollection<Entity> ConvertObject(Base obj, Collection[] layerPath, string baseLayerNamePrefix)
{
string layerName = layerBaker.CreateLayerForReceive(layerPath, baseLayerNamePrefix);
var convertedEntities = new HashSet<Entity>();
using var tr = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction();
// 1: convert
var converted = converter.Convert(obj);
// 2: handle result
switch (converted)
{
case Entity entity:
var bakedEntity = BakeObject(entity, obj, layerName);
convertedEntities.Add(bakedEntity);
break;
case List<(Entity, Base)> listConversionResult: // this is from fallback conversion for brep/brepx/subdx/extrusionx/polycurve
var bakedFallbackEntities = BakeObjectsAsGroup(listConversionResult, obj, layerName, baseLayerNamePrefix);
convertedEntities.UnionWith(bakedFallbackEntities);
break;
default:
// TODO: capture defualt case with report object here? Same as in Rhino
break;
}
tr.Commit();
return convertedEntities.Freeze();
}
private Entity BakeObject(Entity entity, Base originalObject, string layerName, Base? parentObject = null)
{
var objId = originalObject.applicationId ?? originalObject.id.NotNull();
if (colorBaker.ObjectColorsIdMap.TryGetValue(objId, out AutocadColor? color))
{
entity.Color = color;
}
if (materialBaker.TryGetMaterialId(originalObject, parentObject, out ObjectId matId))
{
entity.MaterialId = matId;
}
entity.AppendToDb(layerName);
return entity;
}
private List<Entity> BakeObjectsAsGroup(
List<(Entity, Base)> fallbackConversionResult,
Base parentObject,
string layerName,
string baseLayerName
)
{
var ids = new ObjectIdCollection();
var entities = new List<Entity>();
foreach (var (conversionResult, originalObject) in fallbackConversionResult)
{
BakeObject(conversionResult, originalObject, layerName, parentObject);
ids.Add(conversionResult.ObjectId);
entities.Add(conversionResult);
}
if (entities.Count <= 1) // return if empty list or only one, because we don't want to create empty or single item groups.
{
return entities;
}
var tr = Application.DocumentManager.CurrentDocument.Database.TransactionManager.TopTransaction;
var groupDictionary = (DBDictionary)
tr.GetObject(Application.DocumentManager.CurrentDocument.Database.GroupDictionaryId, OpenMode.ForWrite);
var groupName = autocadContext.RemoveInvalidChars(
$@"{parentObject.speckle_type.Split('.').Last()} - {parentObject.applicationId ?? parentObject.id} ({baseLayerName})"
);
var newGroup = new Group(groupName, true);
newGroup.Append(ids);
groupDictionary.UpgradeOpen();
groupDictionary.SetAt(groupName, newGroup);
tr.AddNewlyCreatedDBObject(newGroup, true);
return entities;
}
: base(
converter,
layerBaker,
groupBaker,
instanceBaker,
materialBaker,
colorBaker,
autocadContext,
rootObjectUnpacker,
conversionHandler
) { }
}
@@ -38,6 +38,7 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\EntityExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\SpeckleApplicationIdExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\TransactionContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\AutocadHostObjectBaseBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\AutocadHostObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\AutocadRootObject.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\AutocadRootObjectBaseBuilder.cs" />
@@ -3,6 +3,8 @@ using Microsoft.Extensions.DependencyInjection;
using Speckle.Connectors.Autocad.DependencyInjection;
using Speckle.Connectors.Autocad.Operations.Send;
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.Common.Builders;
using Speckle.Connectors.DUI.Bindings;
@@ -24,10 +26,12 @@ public static class Civil3dConnectorModule
// add receive
serviceCollection.LoadReceive();
serviceCollection.AddScoped<IHostObjectBuilder, Civil3dHostObjectBuilder>();
serviceCollection.AddSingleton<IBinding, Civil3dReceiveBinding>();
// additional classes
serviceCollection.AddScoped<PropertySetDefinitionHandler>();
serviceCollection.AddScoped<PropertySetBaker>();
// automatically detects the Class:IClass interface pattern to register all generated interfaces
serviceCollection.AddMatchingInterfacesAsTransient(Assembly.GetExecutingAssembly());
@@ -0,0 +1,404 @@
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Operations;
using Speckle.Converters.Civil3dShared;
using Speckle.Converters.Civil3dShared.Helpers;
using Speckle.Converters.Civil3dShared.ToSpeckle;
using Speckle.Converters.Common;
using Speckle.Sdk;
using Speckle.Sdk.Models;
using AAEC = Autodesk.Aec;
using AAECPDB = Autodesk.Aec.PropertyData.DatabaseServices;
using ADB = Autodesk.AutoCAD.DatabaseServices;
namespace Speckle.Connectors.Civil3dShared.HostApp;
/// <summary>
/// Helper class to bake property sets to entities on receive.
/// </summary>
public class PropertySetBaker
{
private const string PROP_SET_DEF_DICT_NAME = "AecPropertySetDefs";
private readonly IConverterSettingsStore<Civil3dConversionSettings> _settingsStore;
private readonly ILogger<PropertySetBaker> _logger;
private readonly PropertyHandler _propertyHandler;
/// <summary>
/// Map of property set definition name to its ObjectId. Populated during ParsePropertySetDefinitions.
/// </summary>
private readonly Dictionary<string, ADB.ObjectId> _propertySetDefinitionMap = new();
public PropertySetBaker(
IConverterSettingsStore<Civil3dConversionSettings> settingsStore,
ILogger<PropertySetBaker> logger
)
{
_settingsStore = settingsStore;
_logger = logger;
_propertyHandler = new PropertyHandler();
}
/// <summary>
/// Removes all property set definitions with a prefix before receive operation.
/// </summary>
public void PurgePropertySets(string namePrefix)
{
ADB.Database db = _settingsStore.Current.Document.Database;
using var tr = db.TransactionManager.StartTransaction();
List<ADB.ObjectId> definitionsToDelete = new();
// Access the property set definition dictionary from the named object dictionary
var nod = (ADB.DBDictionary)tr.GetObject(db.NamedObjectsDictionaryId, ADB.OpenMode.ForRead);
if (nod.Contains(PROP_SET_DEF_DICT_NAME))
{
ADB.ObjectId propSetDefsDictId = nod.GetAt(PROP_SET_DEF_DICT_NAME);
var propSetDefsDict = (ADB.DBDictionary)tr.GetObject(propSetDefsDictId, ADB.OpenMode.ForRead);
// Iterate through all property set definitions in the dictionary
foreach (ADB.DBDictionaryEntry entry in propSetDefsDict)
{
if (entry.Key.Contains(namePrefix))
{
definitionsToDelete.Add(entry.Value);
}
}
}
// Delete the matching definitions
foreach (ADB.ObjectId defId in definitionsToDelete)
{
try
{
var propSetDef = (AAECPDB.PropertySetDefinition)tr.GetObject(defId, ADB.OpenMode.ForWrite);
propSetDef.Erase();
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to purge property set definition");
}
}
tr.Commit();
}
/// <summary>
/// Parse and bake all property set definitions from the root object.
/// Should be called after purging and after materials/colors are parsed.
/// </summary>
public void ParseAndBakePropertySetDefinitions(Base rootObject, string namePrefix)
{
_propertySetDefinitionMap.Clear();
if (rootObject[ProxyKeys.PROPERTYSET_DEFINITIONS] is not Dictionary<string, object?> definitions)
{
return;
}
if (definitions.Count == 0)
{
return;
}
using var tr = _settingsStore.Current.Document.Database.TransactionManager.StartTransaction();
foreach (var definition in definitions)
{
string setName = definition.Key;
object? setDefObj = definition.Value;
if (setDefObj is not Dictionary<string, object?> setDefData)
{
_logger.LogWarning("Property set definition {SetName} has invalid data format", setName);
continue;
}
if (!setDefData.TryGetValue(PropertySetDefinitionHandler.PROP_SET_PROP_DEFS_KEY, out var propDefsObj))
{
_logger.LogWarning("Property set definition {SetName} missing propertyDefinitions", setName);
continue;
}
if (propDefsObj is not Dictionary<string, object?> propertyDefinitions)
{
_logger.LogWarning("Property set definition {SetName} propertyDefinitions has invalid format", setName);
continue;
}
ADB.ObjectId defId = CreatePropertySetDefinition(setName, propertyDefinitions, namePrefix, tr);
if (!defId.IsNull)
{
_propertySetDefinitionMap[setName] = defId;
}
}
tr.Commit();
}
/// <summary>
/// Try to bake property sets from a Speckle object to a Civil3D entity.
/// </summary>
public bool TryBakePropertySets(ADB.Entity entity, Base sourceObject, ADB.Transaction tr)
{
if (
sourceObject["properties"] is not Dictionary<string, object?> properties
|| !properties.TryGetValue("Property Sets", out var propertySetsObj)
|| propertySetsObj is not Dictionary<string, object?> propertySets
|| propertySets.Count == 0
)
{
return false;
}
try
{
foreach (var propertySet in propertySets)
{
string setName = propertySet.Key;
object? setDataObj = propertySet.Value;
if (setDataObj is not Dictionary<string, object?> setData)
{
_logger.LogWarning("Property set {SetName} has invalid data format", setName);
continue;
}
if (!TryBakePropertySet(entity, setName, setData, tr))
{
_logger.LogWarning("Failed to bake property set {SetName} onto entity", setName);
}
}
return true;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to bake property sets onto entity {Handle}", entity.Handle);
return false;
}
}
private bool TryBakePropertySet(
ADB.Entity entity,
string setName,
Dictionary<string, object?> setData,
ADB.Transaction tr
)
{
try
{
if (!_propertySetDefinitionMap.TryGetValue(setName, out ADB.ObjectId propertySetDefId))
{
_logger.LogWarning("Property set definition {SetName} not found in definition map", setName);
return false;
}
if (propertySetDefId.IsNull)
{
return false;
}
if (ObjectHasPropertySet(entity, propertySetDefId))
{
throw new SpeckleException($"Property set '{setName}' already exists on entity.");
}
return AddPropertySetToEntity(entity, propertySetDefId, setData, tr);
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to process property set {SetName}", setName);
return false;
}
}
private ADB.ObjectId CreatePropertySetDefinition(
string setName,
Dictionary<string, object?> propertyDefinitions,
string namePrefix,
ADB.Transaction tr
)
{
var db = _settingsStore.Current.Document.Database;
using AAECPDB.DictionaryPropertySetDefinitions propSetDefs = new(db);
string prefixedName = $"{setName}-{namePrefix}";
AAECPDB.PropertySetDefinition propSetDef = new();
propSetDef.SetToStandard(db);
propSetDef.SubSetDatabaseDefaults(db);
//propSetDef.Description = "Property Set Definition added by Speckle"; // POC: should use the description that was published. can this back in if needed
propSetDef.AppliesToAll = true;
foreach (var propertyDefinition in propertyDefinitions)
{
string propertyName = propertyDefinition.Key;
object? propertyDefObj = propertyDefinition.Value;
if (propertyDefObj is not Dictionary<string, object?> propertyDefDict)
{
continue;
}
if (
!propertyDefDict.TryGetValue(PropertySetDefinitionHandler.PROP_DEF_TYPE_KEY, out var dataTypeStr)
|| dataTypeStr is not string dataTypeString
)
{
_logger.LogError(
"Property set definition {SetName} is invalid: property {PropertyName} missing or invalid dataType",
setName,
propertyName
);
return ADB.ObjectId.Null;
}
if (!Enum.TryParse(dataTypeString, out AAEC.PropertyData.DataType dataType))
{
_logger.LogError(
"Property set definition {SetName} is invalid: unsupported data type {DataType} for property {PropertyName}",
setName,
dataTypeString,
propertyName
);
return ADB.ObjectId.Null;
}
AAECPDB.PropertyDefinition propDef = new() { DataType = dataType, Name = propertyName };
propDef.SetToStandard(db);
propDef.SubSetDatabaseDefaults(db);
if (
propertyDefDict.TryGetValue(PropertySetDefinitionHandler.PROP_DEF_DEFAULT_VALUE_KEY, out object? defaultValue)
&& defaultValue != null
)
{
try
{
// Cast numeric types to avoid bad numeric value errors
var convertedValue = dataType switch
{
AAEC.PropertyData.DataType.Integer => (int)(long)defaultValue,
AAEC.PropertyData.DataType.AutoIncrement => (int)(long)defaultValue,
_ => defaultValue
};
propDef.DefaultData = convertedValue;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(
ex,
"Failed to set default value for property {PropertyName}, continuing without default",
propertyName
);
}
}
propSetDef.Definitions.Add(propDef);
}
propSetDefs.AddNewRecord(prefixedName, propSetDef);
tr.AddNewlyCreatedDBObject(propSetDef, true);
return propSetDef.ObjectId;
}
private bool ObjectHasPropertySet(ADB.DBObject obj, ADB.ObjectId propertySetId)
{
try
{
ADB.ObjectId tempId = AAECPDB.PropertyDataServices.GetPropertySet(obj, propertySetId);
return !tempId.IsNull;
}
catch (Autodesk.AutoCAD.Runtime.Exception ex) when (!ex.IsFatal())
{
return false;
}
}
private bool AddPropertySetToEntity(
ADB.Entity entity,
ADB.ObjectId propertySetDefId,
Dictionary<string, object?> setData,
ADB.Transaction tr
)
{
try
{
if (!entity.IsWriteEnabled)
{
entity.UpgradeOpen();
}
AAECPDB.PropertyDataServices.AddPropertySet(entity, propertySetDefId);
return TrySetPropertyValues(entity, propertySetDefId, setData, tr);
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to add property set to entity");
return false;
}
}
private bool TrySetPropertyValues(
ADB.Entity entity,
ADB.ObjectId propertySetDefId,
Dictionary<string, object?> setData,
ADB.Transaction tr
)
{
try
{
ADB.ObjectId propertySetId = AAECPDB.PropertyDataServices.GetPropertySet(entity, propertySetDefId);
var propertySet = (AAECPDB.PropertySet)tr.GetObject(propertySetId, ADB.OpenMode.ForWrite);
var setDefinition = (AAECPDB.PropertySetDefinition)tr.GetObject(propertySetDefId, ADB.OpenMode.ForRead);
// Build a map of property names to definition IDs
Dictionary<string, int> propertyNameToId = new();
foreach (AAECPDB.PropertyDefinition propDef in setDefinition.Definitions)
{
propertyNameToId[propDef.Name] = propDef.Id;
}
foreach (var propertyEntry in setData)
{
string propertyName = propertyEntry.Key;
object? propertyDataObj = propertyEntry.Value;
if (propertyDataObj is not Dictionary<string, object?> propertyDataDict)
{
continue;
}
if (!propertyDataDict.TryGetValue("value", out var value) || value == null)
{
continue;
}
if (!propertyNameToId.TryGetValue(propertyName, out int propertyId))
{
continue;
}
_propertyHandler.TryGetValue(
() =>
{
propertySet.SetAt(propertyId, value);
return true;
},
out _
);
}
return true;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to update property set values");
return false;
}
}
}
@@ -0,0 +1,60 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Autocad.Operations.Receive;
using Speckle.Connectors.Civil3dShared.HostApp;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Converters.Common;
using Speckle.Sdk.Models;
namespace Speckle.Connectors.Civil3dShared.Operations.Receive;
/// <summary>
/// <para>Civil3D specific host object builder with property set support. Expects to be a scoped dependency per receive operation.</para>
/// </summary>
public sealed class Civil3dHostObjectBuilder : AutocadHostObjectBaseBuilder
{
private readonly PropertySetBaker _propertySetBaker;
public Civil3dHostObjectBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler,
PropertySetBaker propertySetBaker
)
: base(
converter,
layerBaker,
groupBaker,
instanceBaker,
materialBaker,
colorBaker,
autocadContext,
rootObjectUnpacker,
conversionHandler
)
{
_propertySetBaker = propertySetBaker;
}
protected override void PreReceiveAdditionalDeepClean(string baseLayerPrefix)
{
_propertySetBaker.PurgePropertySets(baseLayerPrefix);
}
protected override void ParseAndBakeAdditionalProxies(Base rootObject, string baseLayerPrefix)
{
_propertySetBaker.ParseAndBakePropertySetDefinitions(rootObject, baseLayerPrefix);
}
protected override void PostBakeEntity(Entity entity, Base originalObject, Transaction tr)
{
_propertySetBaker.TryBakePropertySets(entity, originalObject, tr);
}
}
@@ -11,11 +11,15 @@
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Bindings\Civil3dReceiveBinding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\Civil3dConnectorModule.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\PropertySetBaker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\Civil3dHostObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Civil3dRootObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bindings\Civil3dSendBinding.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)DependencyInjection\" />
<Folder Include="$(MSBuildThisFileDirectory)HostApp\" />
<Folder Include="$(MSBuildThisFileDirectory)Operations\Receive\" />
<Folder Include="$(MSBuildThisFileDirectory)Operations\Send\" />
</ItemGroup>
</Project>
@@ -29,9 +29,11 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private readonly DocumentModelStore _store;
private readonly ICancellationManager _cancellationManager;
private readonly ISendConversionCache _sendConversionCache;
private readonly ToSpeckleSettingsManager _toSpeckleSettingsManager;
private readonly ElementUnpacker _elementUnpacker;
private readonly IRevitConversionSettingsFactory _revitConversionSettingsFactory;
private readonly RevitToSpeckleCacheSingleton _revitToSpeckleCacheSingleton;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly LinkedModelHandler _linkedModelHandler;
private readonly IThreadContext _threadContext;
@@ -55,6 +57,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
ToSpeckleSettingsManager toSpeckleSettingsManager,
ElementUnpacker elementUnpacker,
IRevitConversionSettingsFactory revitConversionSettingsFactory,
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
ITopLevelExceptionHandler topLevelExceptionHandler,
LinkedModelHandler linkedModelHandler,
IThreadContext threadContext,
@@ -71,6 +74,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
_toSpeckleSettingsManager = toSpeckleSettingsManager;
_elementUnpacker = elementUnpacker;
_revitConversionSettingsFactory = revitConversionSettingsFactory;
_revitToSpeckleCacheSingleton = revitToSpeckleCacheSingleton;
_topLevelExceptionHandler = topLevelExceptionHandler;
_linkedModelHandler = linkedModelHandler;
_threadContext = threadContext;
@@ -449,6 +453,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private async Task OnDocumentChanged()
{
_sendConversionCache.ClearCache();
_revitToSpeckleCacheSingleton.ClearCache();
if (_cancellationManager.NumberOfOperations > 0)
{
@@ -34,18 +34,22 @@ public class LevelUnpacker
Dictionary<string, LevelProxy> levelProxies = new();
foreach (var element in elements)
{
if (levelProxies.TryGetValue(element.LevelId.ToString(), out LevelProxy? levelProxy))
// NOTE: Use level.UniqueId (not element.LevelId) as key
// face-based instances don't have a valid element.LevelId, hence all the changes in the LevelExtractor
var level = _levelExtractor.GetLevel(element);
if (level is null)
{
continue;
}
string levelKey = level.UniqueId;
if (levelProxies.TryGetValue(levelKey, out LevelProxy? levelProxy))
{
levelProxy.objects.Add(element.UniqueId);
}
else
{
var level = _levelExtractor.GetLevel(element);
if (level is null)
{
continue;
}
var levelDataObject = new DataObject()
{
name = level.Name,
@@ -53,11 +57,11 @@ public class LevelUnpacker
properties = _propertiesExtractor.GetProperties(level)
};
var unitSettings = _converterSettings.Current.Document.GetUnits();
var lengthUnitType = unitSettings.GetFormatOptions(Autodesk.Revit.DB.SpecTypeId.Length).GetUnitTypeId();
var lengthUnitType = unitSettings.GetFormatOptions(SpecTypeId.Length).GetUnitTypeId();
levelDataObject["elevation"] = UnitUtils.ConvertFromInternalUnits(level.Elevation, lengthUnitType);
levelDataObject["units"] = _converterSettings.Current.SpeckleUnits;
levelProxies[element.LevelId.ToString()] = new LevelProxy()
levelProxies[levelKey] = new LevelProxy()
{
applicationId = level.UniqueId,
objects = [element.UniqueId],
@@ -120,7 +120,7 @@ internal sealed class RevitDocumentStore : DocumentModelStore
var x = doc.PathName;
if (string.IsNullOrEmpty(x))
{
return null;
return doc.Title;
}
return x;
#endif
@@ -120,9 +120,14 @@ public class RevitMaterialBaker
try
{
// all values assumed to be on the 0 - 1 scale need to pass through this validation and logging (if assumption wrong)
double roughness = ClampToUnitRange(speckleRenderMaterial.roughness, "roughness", speckleRenderMaterial.name);
double opacity = ClampToUnitRange(speckleRenderMaterial.opacity, "opacity", speckleRenderMaterial.name);
double metalness = ClampToUnitRange(speckleRenderMaterial.metalness, "metalness", speckleRenderMaterial.name);
var diffuse = System.Drawing.Color.FromArgb(speckleRenderMaterial.diffuse);
double transparency = 1 - speckleRenderMaterial.opacity;
double smoothness = 1 - speckleRenderMaterial.roughness;
double transparency = 1 - opacity;
double smoothness = 1 - roughness;
string materialId = speckleRenderMaterial.applicationId ?? speckleRenderMaterial.id.NotNull();
string matName = _revitUtils.RemoveInvalidChars($"{speckleRenderMaterial.name}-({materialId})-{baseLayerName}");
@@ -130,7 +135,7 @@ public class RevitMaterialBaker
var revitMaterial = (Material)_converterSettings.Current.Document.GetElement(newMaterialId);
revitMaterial.Color = new Color(diffuse.R, diffuse.G, diffuse.B);
revitMaterial.Transparency = (int)(transparency * 100);
revitMaterial.Shininess = (int)(speckleRenderMaterial.metalness * 128);
revitMaterial.Shininess = (int)(metalness * 128);
revitMaterial.Smoothness = (int)(smoothness * 128);
foreach (var objectId in proxy.objects)
@@ -163,4 +168,30 @@ public class RevitMaterialBaker
document.Delete(materialIds);
}
}
/// <summary>
/// After CNX-2661, we've seen some edge cases contradicting the expected 0 - 1 range for PRB properties.
/// Defensively, we'd rather clamp these values than throw.
/// </summary>
/// <remarks>
/// Created a method so that we can extend the checks to any numerical value potentially leading to a negative value,
/// which would throw an exception. Generalised method since Math.Clamp() only available since C# 8.0 and this method
/// handles logging (in the hope that we can get a better feel for these "weird" models, e.g. 0 - 100 scale??)
/// </remarks>
private double ClampToUnitRange(double value, string propertyName, string materialName)
{
if (value is < 0 or > 1)
{
_logger.LogWarning(
"Material '{MaterialName}' has an invalid {PropertyName} value of {Value} and was clamped to 0 - 1 range",
materialName,
propertyName,
value
);
value = Math.Min(Math.Max(0, value), 1);
}
return value;
}
}
@@ -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;
}
}
@@ -183,6 +183,7 @@ 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))
{
converted = value;
@@ -248,6 +249,17 @@ public class RevitRootObjectBuilder(
var levelProxies = levelUnpacker.Unpack(flatElements);
rootObject[ProxyKeys.LEVEL] = levelProxies;
rootObject[ProxyKeys.INSTANCE_DEFINITION] = revitToSpeckleCacheSingleton.GetInstanceDefinitionProxiesForObjects(
idsAndSubElementIds
);
rootObject.elements.Add(
new Collection()
{
elements = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds),
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;
@@ -87,6 +87,7 @@ public class SpeckleSelectModelComponent : GH_Component
string? urlInput = null;
// SCENARIO 1: Component has input wire connected
if (da.GetData(0, ref urlInput))
{
UrlInput = urlInput;
@@ -99,6 +100,11 @@ public class SpeckleSelectModelComponent : GH_Component
return;
}
if (_justPastedIn)
{
RestoreAccountFromStoredState();
}
try
{
// NOTE: once we split the logic in Sender and Receiver components, we need to set flag correctly
@@ -132,22 +138,9 @@ public class SpeckleSelectModelComponent : GH_Component
_storedUserId = SpeckleOperationWizard.SelectedAccount?.id;
}
if (_justPastedIn && _storedUserId != null && !string.IsNullOrEmpty(_storedUserId))
if (_justPastedIn)
{
try
{
SpeckleOperationWizard.SetAccountFromId(_storedUserId);
}
catch (SpeckleAccountManagerException e)
{
// Swallow and move onto checking server.
Console.WriteLine(e);
}
if (_storedServer != null && SpeckleOperationWizard.SelectedAccount == null)
{
SpeckleOperationWizard.SetAccountFromIdAndUrl(_storedUserId, _storedServer);
}
RestoreAccountFromStoredState();
}
// Validate backing data
@@ -396,4 +389,39 @@ public class SpeckleSelectModelComponent : GH_Component
VersionContextMenuButton.ExpirePreview(redraw);
base.ExpirePreview(redraw);
}
/// <summary>
/// Restores the account from stored state when the component is pasted or loaded from file.
/// </summary>
/// <remarks>
/// Attempts to restore account in two stages:
/// <list type="number">
/// <item>First tries to get account by stored user ID</item>
/// <item>If that fails and server url is available, falls back to getting any account matching the server</item>
/// </list>
/// Only executes when <see cref="_justPastedIn"/> is true and <see cref="_storedUserId"/> is not empty.
/// </remarks>
private void RestoreAccountFromStoredState()
{
if (_storedUserId is null || string.IsNullOrEmpty(_storedUserId))
{
return;
}
try
{
SpeckleOperationWizard.SetAccountFromId(_storedUserId);
}
catch (SpeckleAccountManagerException e)
{
Console.WriteLine(e);
}
// Fallback: if account wasn't found by ID but we have a server URL,
// try to find any account matching that server
if (_storedServer != null && SpeckleOperationWizard.SelectedAccount == null)
{
SpeckleOperationWizard.SetAccountFromIdAndUrl(_storedUserId, _storedServer);
}
}
}
@@ -79,22 +79,40 @@ public class SpeckleOperationWizard
var resources = SpeckleResourceBuilder.FromUrlString(input, token);
if (resources.Length == 0)
{
throw new SpeckleException($"Input url string was empty");
throw new SpeckleException("Input url string was empty");
}
if (resources.Length > 1)
{
throw new SpeckleException($"Input multi-model url is not supported");
throw new SpeckleException("Input multi-model url is not supported");
}
var resource = resources.First();
using var scope = PriorityLoader.CreateScopeForActiveDocument();
var account = resource.Account.GetAccount(scope);
SetAccount(account, false);
var urlDerivedAccount = resource.Account.GetAccount(scope);
// if no account is selected, happily go through the url derived account approach
if (SelectedAccount == null)
{
throw new SpeckleException("No account found for server URL");
SetAccount(urlDerivedAccount, false);
}
// if we have an account from right-click context-menu, we rely on that and just validate that it's actually applicable to that server
else if (urlDerivedAccount != null && SelectedAccount.serverInfo.url != urlDerivedAccount.serverInfo.url)
{
throw new SpeckleException(
$"Selected account is for '{SelectedAccount.serverInfo.url}' "
+ $"but URL requires '{urlDerivedAccount.serverInfo.url}'"
);
}
// we have both scenarios covered
// Scenario #1 - default account from url
// Scenario #2 - triggered by account switch on right-click context (and validated)
if (SelectedAccount == null)
{
throw new SpeckleException(
$"No appropriate account found for the given '{urlDerivedAccount?.serverInfo.url}' server"
);
}
IClient client = _clientFactory.Create(SelectedAccount);
@@ -104,10 +104,10 @@ public class RhinoInstanceBaker : IInstanceBaker<IReadOnlyCollection<string>>
attributes
);
// POC: check on defIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
// POC: check on defIndex -1, means we haven't created anything - this is most likely an unrecoverable error at this stage
if (defIndex == -1)
{
throw new ConversionException("Failed to create an instance defintion object.");
throw new ConversionException("Failed to create an instance definition object.");
}
if (definitionProxy.applicationId != null)
@@ -170,9 +170,13 @@ public class RhinoInstanceBaker : IInstanceBaker<IReadOnlyCollection<string>>
public void PurgeInstances(string namePrefix)
{
var currentDoc = RhinoDoc.ActiveDoc; // POC: too much right now to interface around
// clean name prefix to match how block names are created
var cleanedPrefix = RhinoUtils.CleanBlockDefinitionName(namePrefix);
foreach (var definition in currentDoc.InstanceDefinitions)
{
if (!definition.IsDeleted && definition.Name.Contains(namePrefix))
if (!definition.IsDeleted && definition.Name.Contains(cleanedPrefix))
{
currentDoc.InstanceDefinitions.Delete(definition.Index, true, false);
}
@@ -218,13 +218,23 @@ public class RhinoMaterialUnpacker
? pbRenderMaterial.Material.EmissionColor
: pbRenderMaterial.Emission.AsSystemColor(); // pbRenderMaterial.emission gives wrong color for emission materials, and material.emissioncolor gives the wrong value for most others *shrug*
// NOTE: added after CNX-2661, without having file that caused issue hard to say what the issue is
// api bug / funny model (custom textures) / upgrade from old model (e.g. Rhino 6)? who knows.
// PBR standard is 0-1. Clamping to valid range. This may indicate texture data is in wrong scale.
double roughness = pbRenderMaterial.Roughness;
if (roughness < 0 || roughness > 1)
{
_logger.LogWarning("Material '{Name}' has invalid roughness value of {Value}", renderMaterial.Name, roughness);
roughness = Math.Min(Math.Max(0, roughness), 1); // Math.Clamp() only from C# 8.0
}
SpeckleRenderMaterial speckleRenderMaterial =
new()
{
name = renderMaterialName,
opacity = opacity,
metalness = pbRenderMaterial.Metallic,
roughness = pbRenderMaterial.Roughness,
roughness = roughness,
diffuse = diffuse.ToArgb(),
emissive = emissive.ToArgb(),
applicationId = renderMaterial.Id.ToString()
@@ -14,6 +14,17 @@ public class PropertySetDefinitionHandler
/// POC: We're storing these by property set def name atm. There is a decent change different property sets can have the same name, need to validate this.
public Dictionary<string, Dictionary<string, object?>> Definitions { get; } = new();
// Keys used for the dictionary representing a single property set definition
public const string PROP_SET_DEF_NAME_KEY = "name"; // name of the property set definition
public const string PROP_SET_PROP_DEFS_KEY = "propertyDefinitions"; // property definitions in this property set definition
// Keys used for inidividual property definitions within a single property set definition
public const string PROP_DEF_NAME_KEY = "name";
public const string PROP_DEF_DESCRIPTION_KEY = "description";
public const string PROP_DEF_ID_KEY = "id";
public const string PROP_DEF_TYPE_KEY = "dataType";
public const string PROP_DEF_DEFAULT_VALUE_KEY = "defaultValue";
/// <summary>
/// Extracts out and stores in <see cref="Definitions"/> the property set definition.
/// </summary>
@@ -29,12 +40,11 @@ public class PropertySetDefinitionHandler
propertyDefinitionNames[propertyDefinition.Id] = propertyName;
var propertyDict = new Dictionary<string, object?>()
{
["name"] = propertyName,
["description"] = propertyDefinition.Description,
["id"] = propertyDefinition.Id,
["isReadOnly"] = propertyDefinition.IsReadOnly,
["dataType"] = propertyDefinition.DataType.ToString(),
["defaultValue"] = propertyDefinition.DefaultData
[PROP_DEF_NAME_KEY] = propertyName,
[PROP_DEF_DESCRIPTION_KEY] = propertyDefinition.Description,
[PROP_DEF_ID_KEY] = propertyDefinition.Id,
[PROP_DEF_TYPE_KEY] = propertyDefinition.DataType.ToString(),
[PROP_DEF_DEFAULT_VALUE_KEY] = propertyDefinition.DefaultData
};
// accessing unit type prop can be expected to throw if it's not applicable to the definition
@@ -53,8 +63,8 @@ public class PropertySetDefinitionHandler
Definitions[name] = new Dictionary<string, object?>()
{
["name"] = name,
["propertyDefinitions"] = propertyDefinitionsDict
[PROP_SET_DEF_NAME_KEY] = name,
[PROP_SET_PROP_DEFS_KEY] = propertyDefinitionsDict
};
return propertyDefinitionNames;
@@ -91,7 +91,8 @@ public class PropertySetExtractor
? propertyDefinitionName
: data.FieldBucketId;
var value = GetValue(data);
// POC: not sure how to support graphic types atm
var value = data.DataType is AAEC.PropertyData.DataType.Graphic ? null : data.GetData(data.UnitType);
Dictionary<string, object?> propertyValueDict = new() { ["value"] = value, ["name"] = dataName };
PropertyHandler propHandler = new();
@@ -109,31 +110,4 @@ public class PropertySetExtractor
return null;
}
private object? GetValue(AAECPDB.PropertySetData data)
{
object fieldData = data.GetData(data.UnitType);
switch (data.DataType)
{
case AAEC.PropertyData.DataType.Integer:
return fieldData as int?;
case AAEC.PropertyData.DataType.Real:
return fieldData as double?;
case AAEC.PropertyData.DataType.TrueFalse:
return fieldData as bool?;
case AAEC.PropertyData.DataType.Graphic: // POC: not sure how to support atm
return null;
case AAEC.PropertyData.DataType.List:
return fieldData as List<object>;
case AAEC.PropertyData.DataType.AutoIncrement:
return fieldData as int?;
case AAEC.PropertyData.DataType.AlphaIncrement: // POC: not sure what this is
return fieldData;
case AAEC.PropertyData.DataType.Text:
return fieldData as string;
default:
return fieldData;
}
}
}
@@ -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;
}
}
@@ -18,23 +18,74 @@ public sealed class LevelExtractor
return level.Name;
}
/// <summary>
/// Gets the level associated with an element. Handles face-based family instances and hosted elements.
/// </summary>
public DB.Level? GetLevel(DB.Element element)
{
// get level, if any
DB.ElementId? levelId = null;
// try direct LevelId first
if (element.LevelId != DB.ElementId.InvalidElementId)
{
if (_levelCache.TryGetValue(element.LevelId, out DB.Level? cachedLevel))
{
return cachedLevel;
}
levelId = element.LevelId;
}
// otherwise try FamilyInstance-specific sources
else if (element is DB.FamilyInstance familyInstance)
{
levelId = TryGetFamilyInstanceLevelId(familyInstance);
if (element.Document.GetElement(element.LevelId) is DB.Level level)
// couldn't find a direct level ID - recurse to host
if (levelId == null && familyInstance.Host != null)
{
_levelCache[element.LevelId] = level;
return level;
return GetLevel(familyInstance.Host);
}
}
// okay, no valid LevelId found and we've tried A LOT!
if (levelId == null || levelId == DB.ElementId.InvalidElementId)
{
return null;
}
// check if cache has seen this Level before
if (_levelCache.TryGetValue(levelId, out DB.Level? cached))
{
return cached;
}
// add to the cache if firs occurence of this level
if (element.Document.GetElement(levelId) is DB.Level level)
{
_levelCache[levelId] = level;
return level;
}
return null;
}
/// <summary>
/// Tries to get a level ID from a FamilyInstance via parameter or host.
/// Face-based instances store their level in INSTANCE_SCHEDULE_ONLY_LEVEL_PARAM.
/// </summary>
/// <remarks>
/// See: https://forums.autodesk.com/t5/revit-api-forum/newfamilyinstance-not-setting-level-of-family-instance/td-p/11405934
/// </remarks>
private DB.ElementId? TryGetFamilyInstanceLevelId(DB.FamilyInstance familyInstance)
{
// try parameter-based level first (face-based families)
var levelId = familyInstance.get_Parameter(DB.BuiltInParameter.INSTANCE_SCHEDULE_ONLY_LEVEL_PARAM)?.AsElementId();
if (levelId != null && levelId != DB.ElementId.InvalidElementId)
{
return levelId;
}
// try host if it's directly a level
if (familyInstance.Host is DB.Level hostLevel)
{
return hostLevel.Id;
}
return null;
}
}
@@ -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,9 @@
using Microsoft.Extensions.Logging;
using Speckle.Converters.Common.ToSpeckle;
using Speckle.Objects.Other;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.RevitShared.Helpers;
@@ -11,7 +16,7 @@ namespace Speckle.Converters.RevitShared.Helpers;
/// Ask dim for more and he might start crying.
/// </para>
/// </summary>
public class RevitToSpeckleCacheSingleton
public class RevitToSpeckleCacheSingleton(ILogger<RevitToSpeckleCacheSingleton> logger)
{
/// <summary>
/// (DB.Material id, RenderMaterial). This can be generated from converting render materials or material quantities.
@@ -24,11 +29,31 @@ public class RevitToSpeckleCacheSingleton
/// </summary>
public Dictionary<string, Dictionary<string, RenderMaterialProxy>> ObjectRenderMaterialProxiesMap { get; } = new();
public Dictionary<
string,
(List<string> elementIds, InstanceDefinitionProxy definitionProxy)
> InstanceDefinitionProxiesMap { get; } = new();
public Dictionary<string, (List<string> elementIds, Base baseObj)> 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.
/// Maps mesh application IDs to their material IDs for later proxy population.
/// Dictionary: elementId -> (meshAppId -> materialId)
/// </summary>
/// <param name="elementIds"></param>
/// <returns></returns>
public Dictionary<string, Dictionary<string, string>> MeshToMaterialMap { get; } = new();
/// <summary>
/// Returns the merged material proxy list for the given object IDs.
/// Use this post-conversion to get a correct list of material proxies for the root commit object.
/// </summary>
/// <returns>A deduplicated list of <see cref="RenderMaterialProxy"/> objects for all specified elements.</returns>
/// <remarks>
/// <para>
/// Material proxy objects lists should already be correctly populated at this point (with definition mesh IDs for instances
/// and individual mesh IDs for non-instances), so the merging primarily handles cross-element scenarios rather than
/// fixing incorrect data.
/// </para>
/// </remarks>
public List<RenderMaterialProxy> GetRenderMaterialProxyListForObjects(List<string> elementIds)
{
var proxiesToMerge = ObjectRenderMaterialProxiesMap
@@ -42,17 +67,130 @@ public class RevitToSpeckleCacheSingleton
{
if (!mergeTarget.TryGetValue(kvp.Key, out RenderMaterialProxy? value))
{
value = kvp.Value;
mergeTarget[kvp.Key] = value;
continue;
// first time seeing this material - add it
mergeTarget[kvp.Key] = kvp.Value;
}
else
{
// merge objects lists (should already be mostly correct now)
value.objects.AddRange(kvp.Value.objects);
}
value.objects.AddRange(kvp.Value.objects);
}
}
// final deduplication (should be minimal now)
foreach (var renderMaterialProxy in mergeTarget.Values)
{
renderMaterialProxy.objects = renderMaterialProxy.objects.Distinct().ToList();
}
return mergeTarget.Values.ToList();
}
/// <summary>
/// Gets instance definition proxies from session cache for the given element ids.
/// This is necessary because send caching only check against DB.Element since it is the managed object in Revit UI.
/// We need to filter already existant definition proxies from cache with their element id relationship.
/// Otherwise, we will end up with incomplete data in root.
/// </summary>
/// <param name="elementIds">Ids to get corresponding definition proxies that cached before.</param>
public List<InstanceDefinitionProxy> GetInstanceDefinitionProxiesForObjects(List<string> elementIds) =>
InstanceDefinitionProxiesMap
.Values.Where(v => v.elementIds.Any(id => elementIds.Contains(id)))
.Select(v => v.definitionProxy)
.ToList();
/// <summary>
/// Gets atomic objects (Base) that extracted out from display value of RevitDataObject.
/// We need to filter already existant atomic objects from cache with their element id relationship.
/// Otherwise, we will end up with incomplete data in root.
/// </summary>
/// <param name="elementIds">Element ids to get corresponding atomic objects (Base) that cached before.</param>
/// <returns></returns>
public List<Base> GetBaseObjectsForObjects(List<string> elementIds) =>
InstancedObjects.Values.Where(v => v.elementIds.Any(id => elementIds.Contains(id))).Select(v => v.baseObj).ToList();
/// <summary>
/// Adds a mesh ID to the appropriate material proxy.
/// For instances: adds the definition mesh ID.
/// For non-instances: adds the mesh's own ID.
/// </summary>
/// <remarks>
/// Cache navigation logic is encapsulated here. Failures are logged but do not throw exceptions,
/// allowing conversion to continue even if material assignment fails.
/// </remarks>
public void AddMeshToMaterialProxy(string elementId, SOG.Mesh mesh, bool isInstance)
{
// get mesh-to-material mapping
if (!MeshToMaterialMap.TryGetValue(elementId, out var meshMatMap))
{
logger.LogWarning("No mesh-to-material mapping found for element {ElementId}", elementId);
return;
}
// get material ID for this mesh
if (!meshMatMap.TryGetValue(mesh.applicationId.NotNull(), out var materialId))
{
logger.LogError(
"Cache inconsistency: Mesh {MeshId} not found in material mapping for element {ElementId}",
mesh.applicationId,
elementId
);
return;
}
// get material proxy map
if (!ObjectRenderMaterialProxiesMap.TryGetValue(elementId, out var proxyMap))
{
logger.LogError("Cache inconsistency: Material proxy map not found for element {ElementId}", elementId);
return;
}
// get specific material proxy
if (!proxyMap.TryGetValue(materialId, out var materialProxy))
{
logger.LogError(
"Cache inconsistency: Material proxy not found for material {MaterialId} in element {ElementId}",
materialId,
elementId
);
return;
}
// determine which mesh ID to add
string meshIdToAdd;
if (isInstance)
{
var instanceDefinitionId = MeshInstanceIdGenerator.GenerateUntransformedMeshId(mesh);
if (!InstancedObjects.TryGetValue(instanceDefinitionId, out var instancedObject))
{
throw new InvalidOperationException(
$"Instance definition '{instanceDefinitionId}' not found in cache for mesh '{mesh.applicationId}'"
);
}
meshIdToAdd = instancedObject.baseObj.applicationId.NotNull();
}
else
{
meshIdToAdd = mesh.applicationId.NotNull();
}
// add to proxy if not already present
if (!materialProxy.objects.Contains(meshIdToAdd))
{
materialProxy.objects.Add(meshIdToAdd);
}
}
public void ClearCache()
{
ObjectRenderMaterialProxiesMap.Clear();
SpeckleRenderMaterialCache.Clear();
InstanceDefinitionProxiesMap.Clear();
InstancedObjects.Clear();
MeshToMaterialMap.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
@@ -29,7 +29,7 @@ public static class ServiceRegistration
serviceCollection.AddSingleton(new RevitContext());
serviceCollection.AddSingleton(new RevitToHostCacheSingleton());
serviceCollection.AddSingleton(new RevitToSpeckleCacheSingleton());
serviceCollection.AddSingleton<RevitToSpeckleCacheSingleton>();
// POC: do we need ToSpeckleScalingService as is, do we need to interface it out?
serviceCollection.AddScoped<ScalingServiceToSpeckle>();
@@ -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,
@@ -43,16 +43,22 @@ public class MeshByMaterialDictionaryToSpeckle
/// <summary>
/// Converts a dictionary of Revit meshes, where key is MaterialId, into a list of Speckle meshes.
/// </summary>
/// <param name="args">A tuple consisting of (1) a dictionary with DB.ElementId keys and List of DB.Mesh values and (2) the root element id (the one generating all the meshes).</param>
/// <returns>
/// Returns a list of <see cref="SOG.Mesh"/> objects where each mesh represents one unique material in the input dictionary.
/// </returns>
/// <remarks>
/// Be aware that this method internally creates a new instance of <see cref="SOG.Mesh"/> for each unique material in the input dictionary.
/// These meshes are created with an initial capacity based on the size of the vertex and face arrays to avoid unnecessary resizing.
/// Also note that, for each unique material, the method tries to retrieve the related DB.Material from the current document and convert it. If the conversion is successful,
/// the material is added to the corresponding Speckle mesh. If the conversion fails, the operation simply continues without the material.
/// TODO: update description
/// <para>
/// This method creates a new instance of <see cref="SOG.Mesh"/> for each unique material in the input dictionary.
/// </para>
/// <para>
/// For each unique material, the method retrieves the related DB.Material from the current document and converts it to a <see cref="RenderMaterial"/>.
/// Material proxies are created but their objects lists are NOT populated at this stage. The mesh-to-material relationship is stored
/// in <see cref="RevitToSpeckleCacheSingleton.MeshToMaterialMap"/> for later population during display value processing.
/// </para>
/// <para>
/// Deferred population of the object list to ensure that instance geometry references the definition mesh ID in material proxies,
/// rather than individual instance mesh IDs. We can only do this later, because proxification hasn't happened yet.
/// </para>
/// </remarks>
public List<SOG.Mesh> Convert(
(Dictionary<DB.ElementId, List<DB.Mesh>> target, DB.ElementId parentElementId, bool makeTransparent) args
@@ -62,7 +68,7 @@ public class MeshByMaterialDictionaryToSpeckle
var objectRenderMaterialProxiesMap = _revitToSpeckleCacheSingleton.ObjectRenderMaterialProxiesMap;
var materialProxyMap = new Dictionary<string, RenderMaterialProxy>();
var key = args.parentElementId.ToString().NotNull();
// ids are same in copy pasted linked models, otherwise we reset the materialProxyMap in cache and only one of the linked model is having the render materials
if (objectRenderMaterialProxiesMap.TryGetValue(key, out var cachedMaterialProxy))
{
materialProxyMap = cachedMaterialProxy;
@@ -85,32 +91,37 @@ public class MeshByMaterialDictionaryToSpeckle
: materialId.ToString().NotNull();
List<DB.Mesh> meshes = keyValuePair.Value;
// use the meshlist converter to convert the mesh values into a single speckle mesh
SOG.Mesh speckleMesh = _meshListConverter.Convert(meshes);
speckleMesh.applicationId = Guid.NewGuid().ToString(); // NOTE: as we are composing meshes out of multiple ones for the same material, we need to generate our own application id. c'est la vie.
speckleMesh.applicationId = Guid.NewGuid().ToString();
// store mesh-to-material mapping
if (!_revitToSpeckleCacheSingleton.MeshToMaterialMap.TryGetValue(key, out var meshMatMap))
{
meshMatMap = new Dictionary<string, string>();
_revitToSpeckleCacheSingleton.MeshToMaterialMap[key] = meshMatMap;
}
meshMatMap[speckleMesh.applicationId.NotNull()] = materialIdString;
// get the speckle render material
RenderMaterial? renderMaterial = args.makeTransparent
? _transparentMaterial
: _converterSettings.Current.Document.GetElement(materialId) is DB.Material material
? _speckleRenderMaterialConverter.Convert(material)
: null;
// get the render material if any
// Create proxy but DON'T populate objects list yet
if (renderMaterial is not null)
{
if (!materialProxyMap.TryGetValue(materialIdString, out RenderMaterialProxy? renderMaterialProxy))
if (!materialProxyMap.ContainsKey(materialIdString))
{
renderMaterialProxy = new RenderMaterialProxy()
{
value = renderMaterial,
applicationId = materialId.ToString(),
objects = []
};
RenderMaterialProxy? renderMaterialProxy =
new()
{
value = renderMaterial,
applicationId = materialId.ToString(),
objects = []
};
materialProxyMap[materialIdString] = renderMaterialProxy;
}
renderMaterialProxy.objects.Add(speckleMesh.applicationId);
}
result.Add(speckleMesh);
@@ -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;
@@ -36,7 +43,7 @@ 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)
{
string category = target.Category?.Name ?? "none";
@@ -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(target.Id.ToString(), displayValuesWithTransforms);
// get level
string? level = _levelExtractor.GetLevelName(target);
@@ -117,7 +127,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
category = category,
location = convertedLocation,
elements = children,
displayValue = displayValue.Cast<Base>().ToList(),
displayValue = proxifiedDisplayValues,
properties = properties,
units = _converterSettings.Current.SpeckleUnits
};
@@ -184,4 +194,128 @@ 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.
/// Also populates material proxy objects lists with the appropriate mesh IDs based on whether geometry is instanced or not.
/// </summary>
/// <returns>
/// List of processed display values, with meshes replaced by instance proxies where applicable.
/// Non-instance geometry is returned as-is.
/// </returns>
/// <remarks>
/// <para>
/// This is a bit of a code smell. This method is doing to much, "this ... AND this...".
/// </para>
/// <para>
/// But, given a mesh:
/// - if it has a transform, mesh is converted to instance proxy, and the definition mesh ID is added to material proxies
/// - if it doesn't have a transform, it remains as a regular mesh, and its own ID is added to material proxies
/// - other geometry types pass through unchanged
/// </para>
/// <para>
/// This is where material proxy population occurs (deferred from <see cref="MeshByMaterialDictionaryToSpeckle.Convert"/>)
/// to ensure we use definition mesh IDs for instances rather than individual instance mesh IDs.
/// </para>
/// </remarks>
private List<Base> ProcessDisplayValues(string elementId, List<DisplayValueResult> displayValues)
{
List<Base> proxifiedDisplayValues = new();
foreach (var displayValue in displayValues)
{
// check if this is a mesh with a transform - potential instance scenario
if (displayValue.Geometry is SOG.Mesh mesh && displayValue.Transform is not null)
{
var instanceProxy = CreateOrGetInstanceProxy(elementId, mesh, displayValue.Transform.Value);
proxifiedDisplayValues.Add(instanceProxy);
// add the definition mesh ID to material proxy, not the instance mesh
// method technically is a "Try" but logs internally, so we don't have a return to check
_revitToSpeckleCacheSingleton.AddMeshToMaterialProxy(elementId, mesh, isInstance: true);
}
else if (displayValue.Geometry is SOG.Mesh nonInstanceMesh)
{
// non-instance mesh - add its own ID to material proxy
// method technically is a "Try" but logs internally, so we don't have a return to check
_revitToSpeckleCacheSingleton.AddMeshToMaterialProxy(elementId, nonInstanceMesh, isInstance: false);
proxifiedDisplayValues.Add(nonInstanceMesh);
}
else
{
proxifiedDisplayValues.Add(displayValue.Geometry);
}
}
return proxifiedDisplayValues;
}
/// <summary>
/// Creates or retrieves an instance proxy for a mesh, managing instance definitions and caching.
/// </summary>
/// <remarks>
/// <para>
/// This method generates a deterministic instance definition ID based on the untransformed mesh geometry using
/// <see cref="MeshInstanceIdGenerator.GenerateUntransformedMeshId"/>. Multiple instances with identical geometry
/// will share the same definition.
/// </para>
/// <para>
/// The method manages two caches:
/// - <see cref="RevitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap"/>: Tracks instance definitions and which elements use them
/// - <see cref="RevitToSpeckleCacheSingleton.InstancedObjects"/>: Stores the actual definition meshes for later serialization
/// </para>
/// </remarks>
private InstanceProxy CreateOrGetInstanceProxy(string elementId, SOG.Mesh mesh, Matrix4x4 transform)
{
var instanceDefinitionId = MeshInstanceIdGenerator.GenerateUntransformedMeshId(mesh);
// We need to attach element id relationship to proxy singleton for send caching.
// Send caching skips whole DB.Element that turn into RevitDataObject. since we have instance proxies in RevitDataObject but
// its definitions outside of caching mechanism, this elementId helps us to filter which definition proxies should be attached to the root
if (
_revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.TryGetValue(
instanceDefinitionId,
out var instanceDefinition
)
)
{
instanceDefinition.elementIds.Add(elementId);
}
else
{
var newInstanceDefinition = new InstanceDefinitionProxy
{
applicationId = instanceDefinitionId,
objects = new List<string> { mesh.applicationId.NotNull() },
maxDepth = 0,
name = instanceDefinitionId,
};
_revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.Add(
instanceDefinitionId,
([elementId], newInstanceDefinition)
);
}
// some comment valid here as above if statement, since we store original meshes outside of RevitDataObject, we need to know which of them will be attached.
if (_revitToSpeckleCacheSingleton.InstancedObjects.TryGetValue(instanceDefinitionId, out var instancedObject))
{
instancedObject.elementIds.Add(elementId);
}
else
{
_revitToSpeckleCacheSingleton.InstancedObjects.Add(instanceDefinitionId, ([elementId], 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 -1
View File
@@ -16,7 +16,7 @@
<PackageVersion Include="LibTessDotNet" Version="1.1.15" />
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.52" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="Microsoft.Build" Version="17.11.4" />
<PackageVersion Include="Microsoft.Build" Version="17.11.48" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="Npgsql" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.9" />
-2
View File
@@ -23,14 +23,12 @@ This repo is the home of our next-generation Speckle .NET projects:
- [`Autocad Connector`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Connectors/Autocad): for Autodesk AutoCAD and Civil3D 2022+
- [`Rhino Connector`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Connectors/Rhino): for McNeel Rhino 7+
- [`Revit Connector`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Connectors/Revit): for Autodesk Revit 2022+
- [`ArcGIS Connector`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3): for Esri ArcGIS
- [`Tekla Connector`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Connectors/Tekla): for Trimble Tekla 2024
- **Speckle Converters**
- [`Autocad Converter`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Converters/Autocad): for Autodesk AutoCAD 2022+
- [`Civil3d Converter`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Converters/Civil3d): for Autodesk Civil3D 2022+
- [`Rhino Converter`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Converters/Rhino): for McNeel Rhino 7+
- [`Revit Converter`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Converters/Revit): for Autodesk Revit 2023+
- [`ArcGIS Converter`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Converters/ArcGIS/Speckle.Converters.ArcGIS3): for Esri ArcGIS
- [`Tekla Converter`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Converters/Tekla/Speckle.Converter.Tekla2024): for Trimble Tekla 2024
- **SDK**
- [`SDK`](https://github.com/specklesystems/speckle-sharp-connectors/tree/main/Sdk): Autofac module, connector utilities, and dependency injection.
@@ -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();
}