Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c7260c603 | |||
| bae9e3e0f1 | |||
| 26b0394613 | |||
| 689ef0bcbe | |||
| 461585b782 | |||
| ea33f35a7d | |||
| 7427f1a2f3 | |||
| b7984bf97e | |||
| 9b24a45b6e | |||
| 4ace81a422 | |||
| a60790c92c | |||
| fd0d00cac3 | |||
| 498396e611 | |||
| 5444377398 | |||
| 9d981f9800 | |||
| 14e17fb67d | |||
| 0ffa7685fd | |||
| dc7d4671e4 | |||
| 10cb5cd66f | |||
| cb15d9f77a | |||
| da74faef9b | |||
| 4368833c7e | |||
| a20df41316 | |||
| ccf48dbad1 | |||
| 6700aa27bc | |||
| df525eab63 | |||
| 275901626f | |||
| fac0dc31b2 | |||
| 8696eca1f0 | |||
| d647c71cf5 | |||
| 9b218dd808 | |||
| 9f39dc521d | |||
| 112093f914 |
@@ -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",
|
||||
|
||||
+296
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
-226
@@ -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
|
||||
) { }
|
||||
}
|
||||
|
||||
+1
@@ -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" />
|
||||
|
||||
+4
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -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;
|
||||
|
||||
|
||||
+43
-15
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-5
@@ -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()
|
||||
|
||||
+18
-8
@@ -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;
|
||||
|
||||
+2
-28
@@ -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>
|
||||
|
||||
+146
-8
@@ -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>();
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+31
-20
@@ -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);
|
||||
|
||||
+3
-6
@@ -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
-1
@@ -1,8 +1,8 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.Revit2023.ToSpeckle.Properties;
|
||||
using Speckle.Converters.RevitShared.Services;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.Converters.RevitShared.ToSpeckle.Properties;
|
||||
using Speckle.Sdk.Common.Exceptions;
|
||||
using ApplicationException = Autodesk.Revit.Exceptions.ApplicationException;
|
||||
|
||||
|
||||
+137
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user