Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4ee1f2a55 | |||
| 4f960cc670 | |||
| 1f63c1f8b3 | |||
| 2ed9ffbca7 | |||
| d87b862e2b | |||
| 3ad3ad2f01 | |||
| 6db7e46401 | |||
| 13fc24c7c7 | |||
| cf86158b83 | |||
| 25eb955636 | |||
| 7862a858ae | |||
| bc18d3b494 | |||
| fd34f22028 | |||
| 958c9e5e94 | |||
| 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 |
@@ -35,7 +35,7 @@ jobs:
|
||||
run: ./build.ps1 zip
|
||||
|
||||
- name: ⬆️ Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: output-${{ env.SEMVER }}
|
||||
path: output/*.*
|
||||
|
||||
@@ -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>
|
||||
+32
-32
@@ -1,8 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connectors.CSiShared.HostApp.Helpers;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.CSiShared;
|
||||
using Speckle.Converters.CSiShared.Utils;
|
||||
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
|
||||
using Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
|
||||
|
||||
namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
|
||||
|
||||
@@ -11,54 +10,55 @@ namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
|
||||
/// </summary>
|
||||
public class EtabsShellSectionPropertyExtractor : IApplicationShellSectionPropertyExtractor
|
||||
{
|
||||
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
|
||||
private readonly ILogger<EtabsShellSectionPropertyExtractor> _logger;
|
||||
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
|
||||
private readonly EtabsShellSectionResolver _etabsShellSectionResolver;
|
||||
|
||||
public EtabsShellSectionPropertyExtractor(
|
||||
IConverterSettingsStore<CsiConversionSettings> settingsStore,
|
||||
ILogger<EtabsShellSectionPropertyExtractor> logger,
|
||||
EtabsShellSectionResolver etabsShellSectionResolver
|
||||
EtabsShellSectionResolver etabsShellSectionResolver,
|
||||
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_logger = logger;
|
||||
_etabsShellSectionResolver = etabsShellSectionResolver;
|
||||
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract shell section properties
|
||||
/// Extract shell section properties from cache.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// sectionName is unique across all types (Wall, Slab and Deck)
|
||||
/// There is no general query such as PropArea.GetShell() - rather we have to be specific on the type, for example
|
||||
/// PropArea.GetWall() or PropArea.GetDeck() BUT we can't get the building type given a SectionName.
|
||||
/// Hence the introduction of ResolveSection.
|
||||
/// By the time this method is called during section unpacking, all sections should already be
|
||||
/// resolved and cached by <see cref="EtabsShellPropertiesExtractor"/> during object conversion.
|
||||
/// </remarks>
|
||||
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties)
|
||||
{
|
||||
// Step 01: Finding the appropriate api query for the unknown section type (wall, deck or slab)
|
||||
Dictionary<string, object?> resolvedProperties = _etabsShellSectionResolver.ResolveSection(sectionName);
|
||||
var sectionProps = GetSectionProperties(sectionName);
|
||||
|
||||
// Step 02: Mutate properties dictionary with resolved properties
|
||||
foreach (var nestedDictionary in resolvedProperties)
|
||||
// shallow copy nested dictionaries into provided properties dict to mutate it (required by interface contract)
|
||||
foreach (var kvp in sectionProps)
|
||||
{
|
||||
if (nestedDictionary.Value is not Dictionary<string, object?> nestedValues)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unexpected value type for key {Key} in section {SectionName}. Expected Dictionary<string, object?>, got {ActualType}",
|
||||
nestedDictionary.Key,
|
||||
sectionName,
|
||||
nestedDictionary.Value?.GetType().Name ?? "null"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nestedProperties = properties.EnsureNested(nestedDictionary.Key);
|
||||
foreach (var kvp in nestedValues)
|
||||
{
|
||||
nestedProperties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, object?> GetSectionProperties(string sectionName)
|
||||
{
|
||||
// return cached properties directly
|
||||
if (_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache.TryGetValue(sectionName, out var cachedProperties))
|
||||
{
|
||||
return cachedProperties;
|
||||
}
|
||||
|
||||
// fallback - shouldn't happen because cached populated on the fly as sections appear in the extractor
|
||||
_logger.LogWarning(
|
||||
"Section {SectionName} not in cache during unpacking - resolving via API (expensive)",
|
||||
sectionName
|
||||
);
|
||||
|
||||
var resolved = _etabsShellSectionResolver.ResolveSection(sectionName);
|
||||
_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache[sectionName] = resolved;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ public static class ServiceRegistration
|
||||
services.AddScoped<IApplicationShellSectionPropertyExtractor, EtabsShellSectionPropertyExtractor>();
|
||||
services.AddScoped<EtabsSectionPropertyDefinitionService>();
|
||||
services.AddScoped<EtabsSectionPropertyExtractor>();
|
||||
services.AddScoped<EtabsShellSectionResolver>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyDefinitionService.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionPropertyExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionResolver.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\EtabsPluginBase.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\EtabsSpeckleFormBase.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ServiceRegistration.cs" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+55
@@ -64,6 +64,13 @@ public class CreateCollection : VariableParameterComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
// validate for duplicate application IDs across the entire collection hierarchy
|
||||
if (HasDuplicateApplicationIds(rootCollection))
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
|
||||
return; // error already added in validation method
|
||||
}
|
||||
|
||||
dataAccess.SetData(0, new SpeckleCollectionWrapperGoo(rootCollection));
|
||||
}
|
||||
|
||||
@@ -182,6 +189,54 @@ public class CreateCollection : VariableParameterComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all application IDs are unique across the entire collection hierarchy.
|
||||
/// Shows an error if duplicates are found, indicating objects appear in multiple collections.
|
||||
/// </summary>
|
||||
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
|
||||
private bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
|
||||
{
|
||||
// args to CheckForDuplicateApplicationIds passed in since the method can recursively check
|
||||
var seenIds = new HashSet<string>();
|
||||
var duplicateIds = new HashSet<string>();
|
||||
|
||||
// iterate, create hash set and check all application IDs
|
||||
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
|
||||
|
||||
return duplicateIds.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively collects application IDs from all in the collection hierarchy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
|
||||
/// </remarks>
|
||||
private void ProcessAndCheckForDuplicateApplicationIds(
|
||||
SpeckleCollectionWrapper collection,
|
||||
HashSet<string> seenIds,
|
||||
HashSet<string> duplicateIds
|
||||
)
|
||||
{
|
||||
foreach (var element in collection.Elements)
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case SpeckleCollectionWrapper childCollection:
|
||||
// recurse into child collections
|
||||
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
|
||||
break;
|
||||
|
||||
case SpeckleWrapper wrapper:
|
||||
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
|
||||
{
|
||||
duplicateIds.Add(wrapper.ApplicationId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IGH_VariableParameterComponent implementation
|
||||
public override bool CanInsertParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input;
|
||||
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
|
||||
Params.RegisterInputParam(param);
|
||||
}
|
||||
|
||||
Params.OnParametersChanged();
|
||||
ExpireSolution(true);
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -103,7 +103,7 @@ public class SpeckleDataObjectPassthrough()
|
||||
List<SpeckleGeometryWrapperGoo> inputGeometry = new();
|
||||
if (!da.GetDataList(1, inputGeometry) && result == null)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Pass in a Speckle DataObject or Geometries.");
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a Speckle DataObject or Geometries");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public class SpeckleDataObjectPassthrough()
|
||||
{
|
||||
if (inputGeo.Value is SpeckleBlockInstanceWrapper)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"DataObjects cannot contain Block Instances.");
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "DataObjects cannot contain Block Instances");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,10 @@ public class SpeckleDataObjectPassthrough()
|
||||
result.Properties = inputProperties;
|
||||
}
|
||||
|
||||
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
|
||||
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
|
||||
result.ApplicationId ??= Guid.NewGuid().ToString();
|
||||
|
||||
// get the path
|
||||
string? path =
|
||||
result.Path.Count > 1 ? string.Join(Constants.LAYER_PATH_DELIMITER, result.Path) : result.Path.FirstOrDefault();
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ public class SpeckleGeometryPassthrough()
|
||||
|
||||
if (result == null && inputGeometry == null)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Pass in a Speckle Geometry or Geometry.");
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a Speckle Geometry or Geometry");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+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);
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed class RhinoSendBinding : ISendBinding
|
||||
_sendOperationManagerFactory = sendOperationManagerFactory;
|
||||
_rhinoLayerHelper = rhinoLayerHelper;
|
||||
Commands = new SendBindingUICommands(parent); // POC: Commands are tightly coupled with their bindings, at least for now, saves us injecting a factory.
|
||||
PreviousUnitSystem = RhinoDoc.ActiveDoc.ModelUnitSystem;
|
||||
PreviousUnitSystem = RhinoDoc.ActiveDoc?.ModelUnitSystem ?? UnitSystem.None;
|
||||
SubscribeToRhinoEvents();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rhino;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Rhino;
|
||||
using Speckle.Objects.Other;
|
||||
@@ -44,35 +43,43 @@ public class RhinoMaterialBaker
|
||||
string materialId = speckleRenderMaterial.applicationId ?? speckleRenderMaterial.id.NotNull();
|
||||
string matName = $"{speckleRenderMaterial.name}-({materialId})-{baseLayerName}";
|
||||
matName = matName.Replace("[", "").Replace("]", ""); // "Material" doesn't like square brackets if we create from here. Once they created from Rhino UI, all good..
|
||||
Color diffuse = Color.FromArgb(speckleRenderMaterial.diffuse);
|
||||
Color emissive = Color.FromArgb(speckleRenderMaterial.emissive);
|
||||
double transparency = 1 - speckleRenderMaterial.opacity;
|
||||
|
||||
Material rhinoMaterial =
|
||||
new()
|
||||
{
|
||||
Name = matName,
|
||||
DiffuseColor = diffuse,
|
||||
EmissionColor = emissive,
|
||||
Transparency = transparency
|
||||
};
|
||||
// Check if material with this name already exists in the document
|
||||
int matIndex = doc.Materials.Find(matName, ignoreDeletedMaterials: true);
|
||||
|
||||
// try to get additional properties
|
||||
if (speckleRenderMaterial["ior"] is double ior)
|
||||
{
|
||||
rhinoMaterial.IndexOfRefraction = ior;
|
||||
}
|
||||
if (speckleRenderMaterial["shine"] is double shine)
|
||||
{
|
||||
rhinoMaterial.Shine = shine;
|
||||
}
|
||||
|
||||
int matIndex = doc.Materials.Add(rhinoMaterial);
|
||||
|
||||
// POC: check on matIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
|
||||
// If material doesn't exist, create it
|
||||
if (matIndex == -1)
|
||||
{
|
||||
throw new ConversionException("Failed to add a material to the document.");
|
||||
Color diffuse = Color.FromArgb(speckleRenderMaterial.diffuse);
|
||||
Color emissive = Color.FromArgb(speckleRenderMaterial.emissive);
|
||||
double transparency = 1 - speckleRenderMaterial.opacity;
|
||||
|
||||
Material rhinoMaterial =
|
||||
new()
|
||||
{
|
||||
Name = matName,
|
||||
DiffuseColor = diffuse,
|
||||
EmissionColor = emissive,
|
||||
Transparency = transparency
|
||||
};
|
||||
|
||||
// try to get additional properties
|
||||
if (speckleRenderMaterial["ior"] is double ior)
|
||||
{
|
||||
rhinoMaterial.IndexOfRefraction = ior;
|
||||
}
|
||||
if (speckleRenderMaterial["shine"] is double shine)
|
||||
{
|
||||
rhinoMaterial.Shine = shine;
|
||||
}
|
||||
|
||||
matIndex = doc.Materials.Add(rhinoMaterial);
|
||||
|
||||
// POC: check on matIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
|
||||
if (matIndex == -1)
|
||||
{
|
||||
throw new ConversionException($"Failed to add a material to the document: '{matName}' (ID: {materialId})");
|
||||
}
|
||||
}
|
||||
|
||||
// Create the object <> material index map
|
||||
@@ -87,27 +94,4 @@ public class RhinoMaterialBaker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all materials with a name starting with <paramref name="namePrefix"/> from the active document
|
||||
/// </summary>
|
||||
/// <param name="namePrefix"></param>
|
||||
public void PurgeMaterials(string namePrefix)
|
||||
{
|
||||
var currentDoc = RhinoDoc.ActiveDoc; // POC: too much right now to interface around
|
||||
foreach (Material material in currentDoc.Materials)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!material.IsDeleted && material.Name != null && material.Name.Contains(namePrefix))
|
||||
{
|
||||
currentDoc.Materials.Delete(material);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
_logger.LogError(ex, "Failed to purge a material from the document");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+2
-1
@@ -256,7 +256,8 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
|
||||
.RunOnMain(() =>
|
||||
{
|
||||
_instanceBaker.PurgeInstances(baseLayerName);
|
||||
_materialBaker.PurgeMaterials(baseLayerName);
|
||||
// Materials are now reused across receives instead of being purged
|
||||
// _materialBaker.PurgeMaterials(baseLayerName);
|
||||
|
||||
var doc = _converterSettings.Current.Document;
|
||||
// Cleans up any previously received objects
|
||||
|
||||
+74
-18
@@ -8,25 +8,11 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers;
|
||||
/// Extracts properties common to frame elements across CSi products (e.g., Etabs, Sap2000)
|
||||
/// using the FrameObj API calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Design Decisions:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Individual methods preferred over batched calls due to:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Independent API calls with no performance gain from batching (?)</description></item>
|
||||
/// <item><description>Easier debugging and error tracing</description></item>
|
||||
/// <item><description>Simpler maintenance as each method maps to one API concept</description></item>
|
||||
/// </list>
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class CsiFramePropertiesExtractor
|
||||
{
|
||||
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
|
||||
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
|
||||
private readonly DatabaseTableExtractor _databaseTableExtractor;
|
||||
|
||||
private static readonly string[] s_releaseKeys =
|
||||
[
|
||||
@@ -36,15 +22,17 @@ public sealed class CsiFramePropertiesExtractor
|
||||
"Torsion",
|
||||
"Moment 22 (Minor)",
|
||||
"Moment 33 (Major)"
|
||||
]; // Note: caching keys for better performance
|
||||
];
|
||||
|
||||
public CsiFramePropertiesExtractor(
|
||||
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton,
|
||||
IConverterSettingsStore<CsiConversionSettings> settingsStore
|
||||
IConverterSettingsStore<CsiConversionSettings> settingsStore,
|
||||
DatabaseTableExtractor databaseTableExtractor
|
||||
)
|
||||
{
|
||||
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
|
||||
_settingsStore = settingsStore;
|
||||
_databaseTableExtractor = databaseTableExtractor;
|
||||
}
|
||||
|
||||
public void ExtractProperties(CsiFrameWrapper frame, PropertyExtractionResult frameData)
|
||||
@@ -61,13 +49,29 @@ public sealed class CsiFramePropertiesExtractor
|
||||
assignments[CommonObjectProperty.PROPERTY_MODIFIERS] = GetModifiers(frame);
|
||||
assignments["End Releases"] = GetReleases(frame);
|
||||
|
||||
// NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc.
|
||||
// NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc. Strings are unique
|
||||
// Assign sectionId to variable as this will be an argument for the GetMaterialName method
|
||||
string sectionId = GetSectionName(frame);
|
||||
string materialId = GetMaterialName(sectionId);
|
||||
assignments[ObjectPropertyKey.SECTION_ID] = sectionId;
|
||||
assignments[ObjectPropertyKey.MATERIAL_ID] = materialId;
|
||||
|
||||
// CNX-2725 adds more numeric props for dashboard-ing
|
||||
double length = GetLength(frame);
|
||||
double area = GetCrossSectionalArea(sectionId);
|
||||
|
||||
double volume = double.NaN;
|
||||
if (!double.IsNaN(length) && !double.IsNaN(area) && length > 0 && area > 0)
|
||||
{
|
||||
// I am paranoid about what etabs could throw our way
|
||||
double computedVolume = length * area;
|
||||
volume = (!double.IsInfinity(computedVolume) && !double.IsNaN(computedVolume)) ? computedVolume : double.NaN;
|
||||
}
|
||||
|
||||
geometry.AddWithUnits(ObjectPropertyKey.LENGTH, length, _settingsStore.Current.SpeckleUnits);
|
||||
geometry.AddWithUnits(ObjectPropertyKey.CROSS_SECTIONAL_AREA, area, $"{_settingsStore.Current.SpeckleUnits}²");
|
||||
geometry.AddWithUnits(ObjectPropertyKey.VOLUME, volume, $"{_settingsStore.Current.SpeckleUnits}³");
|
||||
|
||||
// store the object, section, and material id relationships in their corresponding caches to be accessed by the connector
|
||||
if (!string.IsNullOrEmpty(sectionId))
|
||||
{
|
||||
@@ -196,4 +200,56 @@ public sealed class CsiFramePropertiesExtractor
|
||||
_ = _settingsStore.Current.SapModel.PropFrame.GetMaterial(sectionName, ref materialName);
|
||||
return materialName;
|
||||
}
|
||||
|
||||
private double GetLength(CsiFrameWrapper frame)
|
||||
{
|
||||
// using the DatabaseTableExtractor fetch table with key "Frame Assignments - Summary"
|
||||
// limit query size to "UniqueName" and "Length" fields
|
||||
string length = _databaseTableExtractor
|
||||
.GetTableData("Frame Assignments - Summary", requestedColumns: ["UniqueName", ObjectPropertyKey.LENGTH])
|
||||
.GetRowValue(frame.Name, ObjectPropertyKey.LENGTH);
|
||||
|
||||
// all database data is returned as strings
|
||||
return double.TryParse(length, out double result) ? result : double.NaN;
|
||||
}
|
||||
|
||||
private double GetCrossSectionalArea(string sectionName)
|
||||
{
|
||||
if (_csiToSpeckleCacheSingleton.FrameSectionAreaCache.TryGetValue(sectionName, out double value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
double area = 0,
|
||||
as2 = 0,
|
||||
as3 = 0,
|
||||
torsion = 0,
|
||||
i22 = 0,
|
||||
i33 = 0,
|
||||
s22 = 0,
|
||||
s33 = 0,
|
||||
z22 = 0,
|
||||
z33 = 0,
|
||||
r22 = 0,
|
||||
r33 = 0;
|
||||
int result = _settingsStore.Current.SapModel.PropFrame.GetSectProps(
|
||||
sectionName,
|
||||
ref area,
|
||||
ref as2,
|
||||
ref as3,
|
||||
ref torsion,
|
||||
ref i22,
|
||||
ref i33,
|
||||
ref s22,
|
||||
ref s33,
|
||||
ref z22,
|
||||
ref z33,
|
||||
ref r22,
|
||||
ref r33
|
||||
);
|
||||
|
||||
double validatedArea = result == 0 ? area : double.NaN;
|
||||
_csiToSpeckleCacheSingleton.FrameSectionAreaCache.Add(sectionName, validatedArea);
|
||||
return validatedArea;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-17
@@ -8,28 +8,18 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers;
|
||||
/// Extracts properties common to shell elements across CSi products (e.g., Etabs, Sap2000)
|
||||
/// using the AreaObj API calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Design Decisions:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Individual methods preferred over batched calls due to:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Independent API calls with no performance gain from batching (?)</description></item>
|
||||
/// <item><description>Easier debugging and error tracing</description></item>
|
||||
/// <item><description>Simpler maintenance as each method maps to one API concept</description></item>
|
||||
/// </list>
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class CsiShellPropertiesExtractor
|
||||
{
|
||||
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
|
||||
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
|
||||
|
||||
public CsiShellPropertiesExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
|
||||
public CsiShellPropertiesExtractor(
|
||||
IConverterSettingsStore<CsiConversionSettings> settingsStore,
|
||||
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
|
||||
}
|
||||
|
||||
public void ExtractProperties(CsiShellWrapper shell, PropertyExtractionResult shellData)
|
||||
@@ -37,7 +27,7 @@ public sealed class CsiShellPropertiesExtractor
|
||||
shellData.ApplicationId = shell.GetSpeckleApplicationId(_settingsStore.Current.SapModel);
|
||||
|
||||
var geometry = shellData.Properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
|
||||
geometry["Joints"] = GetPointNames(shell); // TODO: 🪲 Viewer shows 4 but only displays 3
|
||||
geometry["Joints"] = GetPointNames(shell);
|
||||
|
||||
var assignments = shellData.Properties.EnsureNested(ObjectPropertyCategory.ASSIGNMENTS);
|
||||
assignments[CommonObjectProperty.GROUPS] = GetGroupAssigns(shell);
|
||||
|
||||
+12
@@ -16,4 +16,16 @@ public class CsiToSpeckleCacheSingleton
|
||||
/// A map of (section id, shell object id). Assumes the section id is the unique name of the section
|
||||
/// </summary>
|
||||
public Dictionary<string, List<string>> ShellSectionCache { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// A cache of cross-sectional areas used
|
||||
/// </summary>
|
||||
public Dictionary<string, double> FrameSectionAreaCache { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// A cache of resolved shell section properties populated by "EtabsShellPropertiesExtractor"
|
||||
/// and consumed by "EtabsShellSectionPropertyExtractor".
|
||||
/// This eliminates redundant section resolution API calls.
|
||||
/// </summary>
|
||||
public Dictionary<string, Dictionary<string, object?>> ShellSectionPropertiesCache { get; set; } = [];
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public abstract class CsiObjectToSpeckleConverterBase : IToSpeckleTopLevelConver
|
||||
|
||||
public Base Convert(object target) => Convert((CsiWrapperBase)target);
|
||||
|
||||
public Base Convert(CsiWrapperBase wrapper)
|
||||
private Base Convert(CsiWrapperBase wrapper)
|
||||
{
|
||||
var displayValue = _displayValueExtractor.GetDisplayValue(wrapper).ToList();
|
||||
var objectData = _applicationPropertiesExtractor.ExtractProperties(wrapper);
|
||||
|
||||
@@ -21,8 +21,14 @@ public static class ObjectPropertyCategory
|
||||
/// </summary>
|
||||
public static class ObjectPropertyKey
|
||||
{
|
||||
public const string AREA = "Area";
|
||||
public const string CROSS_SECTIONAL_AREA = "Cross-Sectional Area";
|
||||
public const string DESIGN_PROCEDURE = "Design Procedure";
|
||||
public const string LENGTH = "Length";
|
||||
public const string MATERIAL_ID = "Material";
|
||||
public const string SECTION_ID = "Section Property";
|
||||
public const string THICKNESS = "Thickness";
|
||||
public const string VOLUME = "Volume";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,6 +20,7 @@ public static class ServiceRegistration
|
||||
serviceCollection.AddScoped<EtabsShellPropertiesExtractor>();
|
||||
serviceCollection.AddScoped<IApplicationPropertiesExtractor, EtabsPropertiesExtractor>();
|
||||
serviceCollection.AddScoped<CsiObjectToSpeckleConverterBase, EtabsObjectToSpeckleConverter>();
|
||||
serviceCollection.AddScoped<EtabsShellSectionResolver>();
|
||||
|
||||
serviceCollection.AddMatchingInterfacesAsTransient(converterAssembly);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsFramePropertiesExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsJointPropertiesExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellPropertiesExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellSectionResolver.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\EtabsObjectToSpeckleConverter.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+2
-38
@@ -1,6 +1,5 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.CSiShared;
|
||||
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
|
||||
using Speckle.Converters.CSiShared.Utils;
|
||||
|
||||
namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
|
||||
@@ -8,32 +7,13 @@ namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
|
||||
/// <summary>
|
||||
/// Extracts ETABS-specific properties from frame elements using the FrameObj API calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Responsibilities:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Extracts properties only available in ETABS (e.g., Label, Level)</description></item>
|
||||
/// <item><description>Complements <see cref="CsiFramePropertiesExtractor"/> by adding product-specific data</description></item>
|
||||
/// <item><description>Follows same pattern of single-purpose methods for clear API mapping</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// Design Decisions:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Maintains separate methods for each property following CSI API structure</description></item>
|
||||
/// <item><description>Properties are organized by their functional groups (Object ID, Assignments, Design)</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class EtabsFramePropertiesExtractor
|
||||
{
|
||||
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
|
||||
private readonly DatabaseTableExtractor _databaseTableExtractor;
|
||||
|
||||
public EtabsFramePropertiesExtractor(
|
||||
IConverterSettingsStore<CsiConversionSettings> settingsStore,
|
||||
DatabaseTableExtractor databaseTableExtractor
|
||||
)
|
||||
public EtabsFramePropertiesExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_databaseTableExtractor = databaseTableExtractor;
|
||||
}
|
||||
|
||||
public void ExtractProperties(CsiFrameWrapper frame, Dictionary<string, object?> properties)
|
||||
@@ -46,11 +26,7 @@ public sealed class EtabsFramePropertiesExtractor
|
||||
assignments[CommonObjectProperty.SPRING_ASSIGNMENT] = GetSpringAssignmentName(frame);
|
||||
|
||||
var design = properties.EnsureNested(ObjectPropertyCategory.DESIGN);
|
||||
design["Design Procedure"] = GetDesignProcedure(frame);
|
||||
|
||||
var geometry = properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
|
||||
double length = GetLength(frame);
|
||||
geometry.AddWithUnits("Length", length, _settingsStore.Current.SpeckleUnits);
|
||||
design[ObjectPropertyKey.DESIGN_PROCEDURE] = GetDesignProcedure(frame);
|
||||
}
|
||||
|
||||
private (string label, string level) GetLabelAndLevel(CsiFrameWrapper frame)
|
||||
@@ -90,16 +66,4 @@ public sealed class EtabsFramePropertiesExtractor
|
||||
_ = _settingsStore.Current.SapModel.FrameObj.GetSpringAssignment(frame.Name, ref springPropertyName);
|
||||
return springPropertyName;
|
||||
}
|
||||
|
||||
private double GetLength(CsiFrameWrapper frame)
|
||||
{
|
||||
// using the DatabaseTableExtractor fetch table with key "Frame Assignments - Summary"
|
||||
// limit query size to "UniqueName" and "Length" fields
|
||||
string length = _databaseTableExtractor
|
||||
.GetTableData("Frame Assignments - Summary", requestedColumns: ["UniqueName", "Length"])
|
||||
.GetRowValue(frame.Name, "Length");
|
||||
|
||||
// all database data is returned as strings
|
||||
return double.TryParse(length, out double result) ? result : double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
+80
-16
@@ -9,35 +9,24 @@ namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
|
||||
/// <summary>
|
||||
/// Extracts ETABS-specific properties from shell elements using the AreaObj API calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Responsibilities:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Extracts properties only available in ETABS (e.g., Label, Level)</description></item>
|
||||
/// <item><description>Complements <see cref="CsiShellPropertiesExtractor"/> by adding product-specific data</description></item>
|
||||
/// <item><description>Follows same pattern of single-purpose methods for clear API mapping</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// Design Decisions:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Maintains separate methods for each property following CSI API structure</description></item>
|
||||
/// <item><description>Properties are organized by their functional groups (Object ID, Assignments, Design)</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class EtabsShellPropertiesExtractor
|
||||
{
|
||||
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
|
||||
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
|
||||
private readonly DatabaseTableExtractor _databaseTableExtractor;
|
||||
private readonly EtabsShellSectionResolver _etabsShellSectionResolver;
|
||||
|
||||
public EtabsShellPropertiesExtractor(
|
||||
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton,
|
||||
IConverterSettingsStore<CsiConversionSettings> settingsStore,
|
||||
DatabaseTableExtractor databaseTableExtractor
|
||||
DatabaseTableExtractor databaseTableExtractor,
|
||||
EtabsShellSectionResolver etabsShellSectionResolver
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
|
||||
_databaseTableExtractor = databaseTableExtractor;
|
||||
_etabsShellSectionResolver = etabsShellSectionResolver;
|
||||
}
|
||||
|
||||
public void ExtractProperties(CsiShellWrapper shell, Dictionary<string, object?> properties)
|
||||
@@ -62,9 +51,22 @@ public sealed class EtabsShellPropertiesExtractor
|
||||
assignments[ObjectPropertyKey.SECTION_ID] = sectionId;
|
||||
assignments[ObjectPropertyKey.MATERIAL_ID] = materialId;
|
||||
|
||||
// CNX-2725 adds more numeric props for dashboard-ing
|
||||
var geometry = properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
|
||||
double area = GetArea(shell, designOrientation);
|
||||
geometry.AddWithUnits("Area", area, $"{_settingsStore.Current.SpeckleUnits}²");
|
||||
double thickness = GetSectionThickness(sectionId);
|
||||
|
||||
double volume = double.NaN;
|
||||
if (!double.IsNaN(area) && !double.IsNaN(thickness) && area > 0 && thickness > 0)
|
||||
{
|
||||
// I am paranoid about what etabs could throw our way
|
||||
double computedVolume = area * thickness;
|
||||
volume = (!double.IsInfinity(computedVolume) && !double.IsNaN(computedVolume)) ? computedVolume : double.NaN;
|
||||
}
|
||||
|
||||
geometry.AddWithUnits(ObjectPropertyKey.THICKNESS, thickness, _settingsStore.Current.SpeckleUnits);
|
||||
geometry.AddWithUnits(ObjectPropertyKey.AREA, area, $"{_settingsStore.Current.SpeckleUnits}²");
|
||||
geometry.AddWithUnits(ObjectPropertyKey.VOLUME, volume, $"{_settingsStore.Current.SpeckleUnits}³");
|
||||
|
||||
// store the object, section, and material id relationships in their corresponding caches to be accessed by the connector
|
||||
if (!string.IsNullOrEmpty(sectionId))
|
||||
@@ -188,4 +190,66 @@ public sealed class EtabsShellPropertiesExtractor
|
||||
// all database data is returned as strings
|
||||
return double.TryParse(area, out var result) ? result : double.NaN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets section thickness, resolving and caching section properties on first encounter.
|
||||
/// </summary>
|
||||
/// <param name="sectionId">The section name to get thickness for</param>
|
||||
/// <returns>Thickness value, or NaN if section is invalid or thickness cannot be determined</returns>
|
||||
private double GetSectionThickness(string sectionId)
|
||||
{
|
||||
// Guard against invalid sections
|
||||
if (string.IsNullOrEmpty(sectionId) || sectionId == "None")
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
// Check if section already resolved and cached
|
||||
if (!_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache.TryGetValue(sectionId, out var sectionProperties))
|
||||
{
|
||||
// First encounter - resolve section and cache all properties
|
||||
sectionProperties = _etabsShellSectionResolver.ResolveSection(sectionId);
|
||||
_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache[sectionId] = sectionProperties;
|
||||
}
|
||||
|
||||
// Extract thickness from cached properties
|
||||
return ExtractThicknessFromProperties(sectionProperties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts thickness value from resolved section properties dictionary structure.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Section properties have nested structure:
|
||||
/// { "Property Data" -> { "Thickness" -> { "value" -> double, "units" -> string } } }
|
||||
/// </remarks>
|
||||
private static double ExtractThicknessFromProperties(Dictionary<string, object?> sectionProperties)
|
||||
{
|
||||
if (!sectionProperties.TryGetValue(SectionPropertyCategory.PROPERTY_DATA, out object? propertyDataObj))
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
if (propertyDataObj is not Dictionary<string, object?> propertyData)
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
if (!propertyData.TryGetValue(ObjectPropertyKey.THICKNESS, out object? thicknessObj))
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
if (thicknessObj is not Dictionary<string, object> thicknessDict)
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
if (!thicknessDict.TryGetValue("value", out object? valueObj))
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
return valueObj is double thickness ? thickness : double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ using Speckle.Converters.Common;
|
||||
using Speckle.Converters.CSiShared;
|
||||
using Speckle.Converters.CSiShared.Utils;
|
||||
|
||||
namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
|
||||
namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve the section type and retrieve its properties by trying different section resolvers.
|
||||
+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>
|
||||
|
||||
+150
-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,134 @@ 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))
|
||||
{
|
||||
if (materialId != DB.ElementId.InvalidElementId.ToString())
|
||||
{
|
||||
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>();
|
||||
|
||||
+22
@@ -73,6 +73,28 @@ public class ClassPropertiesExtractor
|
||||
elementProperties.Add("worksetName", worksetName);
|
||||
}
|
||||
|
||||
if (element is DB.FamilyInstance familyInstance)
|
||||
{
|
||||
try
|
||||
{
|
||||
// get room id if applicable (only for FamilyInstance elements)
|
||||
if (familyInstance.Room is not null)
|
||||
{
|
||||
elementProperties.Add("roomId", familyInstance.Room.Id.ToString());
|
||||
}
|
||||
|
||||
// get space id if applicable (only for FamilyInstance elements)
|
||||
if (familyInstance.Space is not null)
|
||||
{
|
||||
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (!e.IsFatal())
|
||||
{
|
||||
// silently ignore - not critical
|
||||
}
|
||||
}
|
||||
|
||||
// get group name if applicable
|
||||
// TODO: in in group proxies separate issue. Below comments from PR #1081
|
||||
// We're using group proxies in Rhino etc. Groups should be handled similarly in Revit, unless there's a good
|
||||
|
||||
+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" />
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Intro
|
||||
The rhino importer is functionally made from two parts:
|
||||
- The `Speckle.Importers.JobProcessor`: A windows background service, designed to always be running.
|
||||
- The `Speckle.Importers.Rhino`: A commandline app triggered by the JobProcessor to handle a single job.
|
||||
|
||||
They are deployed as a single product, and share the Innosetup installer deployment system of connectors.
|
||||
|
||||
> Note: this is documentation for internal purposes only.
|
||||
> Despite much of the code being open-source, we do not offer any support or documentation for self hosters.
|
||||
|
||||
# First Time Setup and Checking For Updates
|
||||
|
||||
On the production/staging Windows servers, you'll find a powershell script `InstallUpdate.ps1` that
|
||||
does a couple of things:
|
||||
- Ensures the latest version of the importer is installed
|
||||
- Defines the Postgres connection string, and how many service instances to stop/start (right now, just the 1 for prod)
|
||||
|
||||
`InstallUpdate.ps1` can be run to both check for updates and install updates, and to perform fresh installs and configuring the service(s)
|
||||
The template for that script can be found [here](https://github.com/specklesystems/gitOps/blob/main/terraform/modules/windows-machine/templates/autoupdater/InstallUpdate.ps1.tpl) but needs to be tweaked per deployment.
|
||||
|
||||
There's lot more to do with the windows infra/rhino licence config that's not covered in this readme document.
|
||||
|
||||
# The Windows Service
|
||||
|
||||
You can view it from the (search "services" in search), It should be running at all times.
|
||||
and can be stopped/started manually from the "services" menu
|
||||
|
||||
It does not display a terminal, the best place to see logs is in our seq-dev instance.
|
||||
|
||||
You can view error/crashes either via
|
||||
1. Event viewer - there is already a "Custom View" called "Speckle Rhino Job Processor"
|
||||
2. Reliability Monitor - Shows historical crash histogram
|
||||
Note, the `Speckle.Importers.Rhino` crashes a lot due to Rhino.Inside behaviour, it's handled
|
||||
crashes of the `Speckle.Importers.JobProcessor` are more serious.
|
||||
|
||||
If you need to troubleshoot, it is still possible to manually start the job processor exe as a command line app, rather than as a window service
|
||||
this will give you back a terminal with all logs. But this is probably better done on the staging server...
|
||||
|
||||
Because it's a Windows Service, it will start up with the Windows OS.
|
||||
If the process crashes (e.g. because it could not connect to the server/db)
|
||||
it will automatically be restarted, with a backoff policy defined in the `InstallUpdate.ps1` script
|
||||
|
||||
If it's in a weird state where you need to manually restart it, this can be done from Services/task manager. Or simply by re-starting the machine.
|
||||
|
||||
# Tricking the InstallUpdate script into running even when there's no updates
|
||||
If you want to make a change to the env vars of the service, or change the setup of the services in some way, it can be useful to run the script
|
||||
without there actually being an update.
|
||||
|
||||
The easiest way to do this is to remove the registry key in `HKEY_CURRENT_USER\Software\Speckle\Services\InstalledConnectors`
|
||||
That way, the script assumes that there is an update when ran.
|
||||
|
||||
# Debugging / Running Against a Local Server
|
||||
|
||||
If you have a local Speckle server and want to test processing Rhino jobs.
|
||||
Firstly, there's a few things on the server you'll need to configure.
|
||||
I'd recommend using the docker-compose files in the root of the server repo.
|
||||
|
||||
On speckle-frontend-2 set the envvar:
|
||||
```yaml
|
||||
NUXT_PUBLIC_FF_RHINO_FILE_IMPORTER_ENABLED: 'true'
|
||||
```
|
||||
|
||||
On speckle-server set the envvars:
|
||||
```yaml
|
||||
FILEIMPORT_SERVICE_USE_PRIVATE_OBJECTS_SERVER_URL: 'false'
|
||||
FF_RHINO_FILE_IMPORTER_ENABLED: 'true'
|
||||
FILE_IMPORT_TIME_LIMIT_MIN: 30
|
||||
```
|
||||
|
||||
Next, you can run the `Speckle.Importers.JobProcessor: Local Docker DB` launch configuration straight from your IDE.
|
||||
The `launchSettings.json` should already be setup with the correct connection string.
|
||||
It will run as normal CLI app, not a Windows Service, but aside from minor logging differences, this will work the same.
|
||||
|
||||
N.b. you will find you are not able to easily debug the `Speckle.Importers.Rhino` project like this, because it's a spawned sub process.
|
||||
You have two options.
|
||||
1. Capture the command line args and call it manually from your IDE, bypassing the JobProcessor.
|
||||
2. Add a `Thread.Sleep(10000)` near the start of the entry point of the Rhino importer, and during the sleep time, attach your IDE to a running process.
|
||||
@@ -7,27 +7,19 @@ namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
|
||||
/// Creates a headless doc and imports the file
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note: imported geometry will be converted to the default <c>mm</c> units
|
||||
/// If we need to preserve the file units, a custom config needs to be created
|
||||
/// Note: using OpenHeadless should preserve the original file's unit system for file types that have units
|
||||
/// </remarks>
|
||||
public sealed class DefaultConfig : IFileTypeConfig
|
||||
internal sealed class DefaultConfig : IFileTypeConfig
|
||||
{
|
||||
public RhinoDoc OpenInHeadlessDocument(string filePath)
|
||||
{
|
||||
var doc = RhinoDoc.CreateHeadless(null);
|
||||
try
|
||||
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, null);
|
||||
if (doc is null)
|
||||
{
|
||||
if (!doc.Import(filePath, null))
|
||||
{
|
||||
throw new SpeckleException("Rhino could not import this file");
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
doc.Dispose();
|
||||
throw;
|
||||
throw new SpeckleException("Rhino could not open this file");
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Rhino;
|
||||
using Rhino.FileIO;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
|
||||
|
||||
internal sealed class DgnConfig : IFileTypeConfig
|
||||
{
|
||||
private readonly FileDgnReadOptions _readOptions = new() { ImportViews = true };
|
||||
|
||||
public RhinoDoc OpenInHeadlessDocument(string filePath)
|
||||
{
|
||||
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, _readOptions.ToDictionary());
|
||||
if (doc is null)
|
||||
{
|
||||
throw new SpeckleException("Rhino could not open this file");
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
|
||||
|
||||
public sealed class FbxConfig : IFileTypeConfig
|
||||
internal sealed class FbxConfig : IFileTypeConfig
|
||||
{
|
||||
private readonly FileFbxReadOptions _readOptions =
|
||||
new()
|
||||
@@ -16,20 +16,12 @@ public sealed class FbxConfig : IFileTypeConfig
|
||||
|
||||
public RhinoDoc OpenInHeadlessDocument(string filePath)
|
||||
{
|
||||
var doc = RhinoDoc.CreateHeadless(null);
|
||||
try
|
||||
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, _readOptions.ToDictionary());
|
||||
if (doc is null)
|
||||
{
|
||||
if (!doc.Import(filePath, _readOptions.ToDictionary()))
|
||||
{
|
||||
throw new SpeckleException("Rhino could not import this file");
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
doc.Dispose();
|
||||
throw;
|
||||
throw new SpeckleException("Rhino could not open this file");
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -4,7 +4,7 @@ using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
|
||||
|
||||
public sealed class ObjConfig : IFileTypeConfig
|
||||
internal sealed class ObjConfig : IFileTypeConfig
|
||||
{
|
||||
private readonly FileObjReadOptions _readOptions =
|
||||
new(new FileReadOptions() { OpenMode = true })
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using Rhino;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RhinoDoc.OpenHeadless(string)"/> will preserve the units defined by the file
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For this config to be safe... we need to make sure we're running Rhino 8.24.25251 or greater due to https://mcneel.myjetbrains.com/youtrack/issue/RH-89162
|
||||
/// </remarks>
|
||||
public sealed class Rhino3dmConfig : IFileTypeConfig
|
||||
{
|
||||
public RhinoDoc OpenInHeadlessDocument(string filePath)
|
||||
{
|
||||
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath);
|
||||
if (doc is null)
|
||||
{
|
||||
throw new SpeckleException("Rhino could not open this file");
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Importers.Rhino.Internal.FileTypeConfig;
|
||||
|
||||
public sealed class SketchupConfig : IFileTypeConfig
|
||||
internal sealed class SketchupConfig : IFileTypeConfig
|
||||
{
|
||||
private readonly FileSkpReadOptions _options =
|
||||
new()
|
||||
@@ -20,20 +20,12 @@ public sealed class SketchupConfig : IFileTypeConfig
|
||||
|
||||
public RhinoDoc OpenInHeadlessDocument(string filePath)
|
||||
{
|
||||
var doc = RhinoDoc.CreateHeadless(null);
|
||||
try
|
||||
RhinoDoc? doc = RhinoDoc.OpenHeadless(filePath, _options.ToDictionary());
|
||||
if (doc is null)
|
||||
{
|
||||
if (!doc.Import(filePath, _options.ToDictionary()))
|
||||
{
|
||||
throw new SpeckleException("Rhino could not import this file");
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
doc.Dispose();
|
||||
throw;
|
||||
throw new SpeckleException("Rhino could not import this file");
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rhino;
|
||||
using Rhino.Runtime.InProcess;
|
||||
@@ -9,55 +9,34 @@ using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
namespace Speckle.Importers.Rhino.Internal;
|
||||
|
||||
internal sealed class ImporterInstance(Sender sender, ILogger<ImporterInstance> logger) : IDisposable
|
||||
internal sealed class ImporterInstance(ImporterArgs args, Sender sender, ILogger<ImporterInstance> logger) : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger = logger;
|
||||
private readonly RhinoCore _rhinoInstance = new(["/netcore-8"], WindowStyle.NoWindow);
|
||||
|
||||
private RhinoDoc? _rhinoDoc;
|
||||
private readonly RhinoDoc _rhinoDoc = OpenDocument(args, logger);
|
||||
|
||||
public async Task<ImporterResponse> Run(ImporterArgs args, CancellationToken cancellationToken)
|
||||
{
|
||||
using var scopeJobId = ActivityScope.SetTag("jobId", args.JobId);
|
||||
// using var scopeJobType = ActivityScope.SetTag("jobType", a.JobType);
|
||||
using var scopeAttempt = ActivityScope.SetTag("job.attempt", args.Attempt.ToString());
|
||||
using var scopeServerUrl = ActivityScope.SetTag("serverUrl", args.Account.serverInfo.url);
|
||||
using var scopeProjectId = ActivityScope.SetTag("projectId", args.Project.id);
|
||||
using var scopeModelId = ActivityScope.SetTag("modelId", args.ModelId);
|
||||
using var scopeBlobId = ActivityScope.SetTag("blobId", args.BlobId);
|
||||
using var scopeFileType = ActivityScope.SetTag("fileType", Path.GetExtension(args.FilePath).TrimStart('.'));
|
||||
UserActivityScope.AddUserScope(args.Account);
|
||||
private readonly IReadOnlyList<IDisposable> _scopes =
|
||||
[
|
||||
ActivityScope.SetTag("jobId", args.JobId),
|
||||
ActivityScope.SetTag("job.attempt", args.Attempt.ToString()),
|
||||
// ActivityScope.SetTag("jobType", args.JobType),
|
||||
ActivityScope.SetTag("serverUrl", args.Account.serverInfo.url),
|
||||
ActivityScope.SetTag("projectId", args.Project.id),
|
||||
ActivityScope.SetTag("modelId", args.ModelId),
|
||||
ActivityScope.SetTag("blobId", args.BlobId),
|
||||
ActivityScope.SetTag("fileType", Path.GetExtension(args.FilePath).TrimStart('.')),
|
||||
UserActivityScope.AddUserScope(args.Account),
|
||||
];
|
||||
|
||||
var result = await TryImport(args, cancellationToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "IPC")]
|
||||
private async Task<ImporterResponse> TryImport(ImporterArgs args, CancellationToken cancellationToken)
|
||||
public async Task<Version> RunRhinoImport(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = await RunRhinoImport(args, cancellationToken);
|
||||
return new ImporterResponse { Version = version, ErrorMessage = null };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Import attempt failed with exception");
|
||||
return new ImporterResponse { ErrorMessage = ex.Message, Version = null };
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Version> RunRhinoImport(ImporterArgs args, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var config = GetConfig(Path.GetExtension(args.FilePath));
|
||||
logger.LogInformation("Opening file {FilePath}", args.FilePath);
|
||||
|
||||
_rhinoDoc = config.OpenInHeadlessDocument(args.FilePath);
|
||||
RhinoDoc.ActiveDoc = _rhinoDoc;
|
||||
|
||||
var version = await sender.Send(args.Project, args.ModelId, args.Account, cancellationToken);
|
||||
var version = await sender
|
||||
.Send(args.Project, args.ModelId, args.Account, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return version;
|
||||
}
|
||||
finally
|
||||
@@ -66,12 +45,20 @@ internal sealed class ImporterInstance(Sender sender, ILogger<ImporterInstance>
|
||||
}
|
||||
}
|
||||
|
||||
private static RhinoDoc OpenDocument(ImporterArgs args, ILogger logger)
|
||||
{
|
||||
using var config = GetConfig(Path.GetExtension(args.FilePath));
|
||||
logger.LogInformation("Opening file {FilePath}", args.FilePath);
|
||||
return config.OpenInHeadlessDocument(args.FilePath);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static IFileTypeConfig GetConfig(string extension) =>
|
||||
extension.ToLowerInvariant() switch
|
||||
{
|
||||
".skp" => new SketchupConfig(),
|
||||
".obj" => new ObjConfig(),
|
||||
".3dm" => new Rhino3dmConfig(),
|
||||
".dgn" => new DgnConfig(),
|
||||
".fbx" => new FbxConfig(),
|
||||
_ => new DefaultConfig(),
|
||||
};
|
||||
@@ -83,5 +70,9 @@ internal sealed class ImporterInstance(Sender sender, ILogger<ImporterInstance>
|
||||
// https://discourse.mcneel.com/t/rhino-inside-fatal-app-crashes-when-disposing-headless-documents/208673
|
||||
_rhinoDoc?.Dispose();
|
||||
_rhinoInstance.Dispose();
|
||||
foreach (var scope in _scopes)
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Speckle.Importers.Rhino.Internal;
|
||||
|
||||
internal sealed class ImporterInstanceFactory(Sender sender, ILogger<ImporterInstance> logger)
|
||||
{
|
||||
public ImporterInstance Create(ImporterArgs args) => new(args, sender, logger);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ internal static class ServiceRegistration
|
||||
services.AddTransient<Progress>();
|
||||
services.AddTransient<Sender>();
|
||||
services.AddTransient<ImporterInstance>();
|
||||
services.AddTransient<ImporterInstanceFactory>();
|
||||
|
||||
// override default thread context
|
||||
services.AddSingleton<IThreadContext>(new ImporterThreadContext());
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RhinoInside;
|
||||
using Speckle.Importers.Rhino.Internal;
|
||||
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
namespace Speckle.Importers.Rhino;
|
||||
|
||||
@@ -18,9 +20,12 @@ public static class Program
|
||||
Console.WriteLine($"Loading Rhino @ {Resolver.RhinoSystemDirectory}");
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "IPC")]
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
ILogger? logger = null;
|
||||
ImporterInstance? importer = null;
|
||||
|
||||
try
|
||||
{
|
||||
var importerArgs = JsonSerializer.Deserialize<ImporterArgs>(args[0], s_serializerOptions);
|
||||
@@ -32,13 +37,34 @@ public static class Program
|
||||
TaskScheduler.UnobservedTaskException += (_, eventArgs) =>
|
||||
logger.LogCritical(eventArgs.Exception, "Unobserved Task Exception");
|
||||
|
||||
var importer = serviceProvider.GetRequiredService<ImporterInstance>();
|
||||
var factory = serviceProvider.GetRequiredService<ImporterInstanceFactory>();
|
||||
|
||||
// Error handling flow below here looks a bit of a mess, but we're having to navigate threading issues with rhino inside
|
||||
// https://discourse.mcneel.com/t/rhino-inside-fatal-app-crashes-when-disposing-headless-documents/208673/7
|
||||
try
|
||||
{
|
||||
// This needs to be called on the main thread
|
||||
importer = factory.Create(importerArgs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteResult(new() { ErrorMessage = ex.Message }, importerArgs.ResultsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// As soon as the main thread is yielded, it will be hogged by Rhino
|
||||
// Task.Run ensures we run everything on a thread pool thread.
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var result = await importer.Run(importerArgs, CancellationToken.None);
|
||||
var serializedResult = JsonSerializer.Serialize(result, s_serializerOptions);
|
||||
File.WriteAllLines(importerArgs.ResultsPath, [serializedResult]);
|
||||
try
|
||||
{
|
||||
Version result = await importer.RunRhinoImport(CancellationToken.None).ConfigureAwait(false);
|
||||
WriteResult(new() { Version = result }, importerArgs.ResultsPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteResult(new() { ErrorMessage = ex.Message }, importerArgs.ResultsPath);
|
||||
}
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -53,7 +79,18 @@ public static class Program
|
||||
{
|
||||
Console.WriteLine(MESSAGE);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
importer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteResult(ImporterResponse result, string resultsPath)
|
||||
{
|
||||
var serializedResult = JsonSerializer.Serialize(result, s_serializerOptions);
|
||||
File.WriteAllLines(resultsPath, [serializedResult]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.23.25251.13001"/>
|
||||
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.23.25251.13001"/>
|
||||
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.23.25251.13001"/>
|
||||
<PackageReference Include="RhinoCommon" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.24.25281.15001"/>
|
||||
<PackageReference Include="Grasshopper" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.24.25281.15001"/>
|
||||
<PackageReference Include="RhinoWindows" IncludeAssets="compile; build" PrivateAssets="all" VersionOverride="8.24.25281.15001"/>
|
||||
<PackageReference Include="Rhino.Inside" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"net8.0-windows7.0": {
|
||||
"Grasshopper": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.23.25251.13001, )",
|
||||
"resolved": "8.23.25251.13001",
|
||||
"contentHash": "AoswT4QQlD21t/ywsUy2cZRuSQDg39WGIVoyc0jOPv7973WwMrGO891ZIQ0vRZ23s4NGa5lqPU53ZgoGZqSpGA==",
|
||||
"requested": "[8.24.25281.15001, )",
|
||||
"resolved": "8.24.25281.15001",
|
||||
"contentHash": "TEtB8nElTvhMJctLhv8UC1v6jscYdTgsoRQIr31ewGZr6cpgGtQTBmUk26/9ZvQxXCgOp7Y4EZdcEZkmqCm1SQ==",
|
||||
"dependencies": {
|
||||
"RhinoCommon": "[8.23.25251.13001]"
|
||||
"RhinoCommon": "[8.24.25281.15001]"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
@@ -48,20 +48,20 @@
|
||||
},
|
||||
"RhinoCommon": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.23.25251.13001, )",
|
||||
"resolved": "8.23.25251.13001",
|
||||
"contentHash": "FKkGghsK0lG7nrjweP3W009lbqMn9tAPF27gw8wognZfZ4hYkTZygOpF99p2Vl7gn+faGKEmh11LgDj6PU/YNw==",
|
||||
"requested": "[8.24.25281.15001, )",
|
||||
"resolved": "8.24.25281.15001",
|
||||
"contentHash": "K8dd7DJvEUUYHpwkyyxr/ojK3e8swlE0STeyG+ulVWkWNHK6gIRDxMYCwB7kNyHHMgpr/vpQlMgR3SVD1GoMTA==",
|
||||
"dependencies": {
|
||||
"System.Drawing.Common": "7.0.0"
|
||||
}
|
||||
},
|
||||
"RhinoWindows": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.23.25251.13001, )",
|
||||
"resolved": "8.23.25251.13001",
|
||||
"contentHash": "xq30IuvvjlensoVROEIenPotvV9o7ynXlROWuZsLfWJp4NwSHc1F/cLn2Rqd0xHCZGhxlwhIYjrdU5WMtDPlxQ==",
|
||||
"requested": "[8.24.25281.15001, )",
|
||||
"resolved": "8.24.25281.15001",
|
||||
"contentHash": "JbG98P80Hskpomzx1xhqFoz2WmVnhjda0noQyZE+dE678ZKmw+O3i/iIjaN5jydXvTu/fZXRzQmEDL7M7Ura8g==",
|
||||
"dependencies": {
|
||||
"RhinoCommon": "[8.23.25251.13001]"
|
||||
"RhinoCommon": "[8.24.25281.15001]"
|
||||
}
|
||||
},
|
||||
"Speckle.InterfaceGenerator": {
|
||||
|
||||
@@ -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.
|
||||
@@ -47,24 +45,15 @@ Make sure to also check and ⭐️ these other Speckle next generation repositor
|
||||
|
||||
# Developing and Debugging
|
||||
|
||||
Clone this repo. **Each section has its own readme**, so follow each readme for specific build and debug instructions.
|
||||
## Developing
|
||||
|
||||
Issues or questions? We encourage everyone interested to debug / hack / contribute / give feedback to this project.
|
||||
It is recommended that you use Jetbrains Rider (version 2024.3 or greater) or Visual Studio 2022 (version 17.13 or greater)
|
||||
The project requires version 8.0.4xx of the .NET SDK.
|
||||
You can download the latest version from https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
||||
|
||||
> **A note on Accounts:**
|
||||
> The connectors themselves don't have features to manage your Speckle accounts; this functionality is delegated to the Speckle Manager desktop app. You can install it [from here](https://speckle-releases.ams3.digitaloceanspaces.com/manager/SpeckleManager%20Setup.exe).
|
||||
From there you can open the main `Speckle.Connectors.sln` solution and build the project
|
||||
|
||||
## Local Builds
|
||||
|
||||
For good development experience and environment setup, run the commands below as needed.
|
||||
|
||||
### Switching to SLNX
|
||||
|
||||
SLNX was introduced with .NET 9 (in May 2024), Visual Studio 17.13 and Rider 2024.3. The older SLNs being used remain for now but will be removed when .NET 10 is introduced to the repo. SLNXs specific to certain host apps are being generated from the main SLN to allow for faster developmenet.
|
||||
|
||||
[https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/](https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/)
|
||||
|
||||
[https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/](https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/)
|
||||
For good development experience and environment setup, you the commands are avaible needed.
|
||||
|
||||
### Formatting
|
||||
We're using [CSharpier](https://github.com/belav/csharpier) to format our code. You can install Csharpier in a few ways:
|
||||
@@ -109,6 +98,14 @@ This solution includes the Core and Objects projects from the speckle-sharp-sdk
|
||||
> [!WARNING]
|
||||
> Using `Local.sln` will modify all your package locks. **Don't check these in!** Revert with the `clean-locks` command or use the regular solution to revert once your changes are made.
|
||||
|
||||
## Switching to SLNX
|
||||
|
||||
SLNX was introduced with .NET 9 (in May 2024), Visual Studio 17.13 and Rider 2024.3. The older SLNs being used remain for now but will be removed when .NET 10 is introduced to the repo. SLNXs specific to certain host apps are being generated from the main SLN to allow for faster developmenet.
|
||||
|
||||
[https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/](https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/)
|
||||
|
||||
[https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/](https://devblogs.microsoft.com/visualstudio/new-simpler-solution-file-format/)
|
||||
|
||||
# Security and Licensing
|
||||
|
||||
### Security
|
||||
|
||||
@@ -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