Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a04c02cba | |||
| 0670c1866f | |||
| 60bb160a0d | |||
| 9127284774 | |||
| 0ea495e698 | |||
| 8673879e48 | |||
| 8d04c9f9c8 | |||
| d73ac2446a | |||
| 3edc877466 | |||
| 9a3d41db3d | |||
| eb166c0931 | |||
| cb15106ca7 | |||
| 3768157efe | |||
| ab0ebd5f46 | |||
| c51a0fda53 | |||
| 4fdc522e42 | |||
| 5290859bf0 | |||
| f65bafde78 | |||
| f37eef7dc0 | |||
| b08ceb3f66 | |||
| a0e23e6acd | |||
| 920f3b3032 | |||
| 94eca4e031 | |||
| 389204a0d1 | |||
| cd30370654 | |||
| 9b9c383128 | |||
| e87eebefd1 | |||
| c5deaf4844 | |||
| 882358e22a | |||
| 532adb855f |
+14
-14
@@ -6,34 +6,34 @@
|
||||
|
||||
# Connectors
|
||||
|
||||
/Connectors/ArcGIS/* @KatKatKateryna
|
||||
/Connectors/Autocad/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Connectors/Civil3d/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Connectors/Autocad/* @oguzhankoral @JR-Morgan @dogukankaratas
|
||||
/Connectors/Civil3d/* @oguzhankoral @JR-Morgan @dogukankaratas
|
||||
/Connectors/CSi/* @bjoernsteinhagen @dogukankaratas
|
||||
/Connectors/Navisworks/* @jsdbroughton
|
||||
/Connectors/Revit/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Connectors/Rhino/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Connectors/Revit/* @oguzhankoral
|
||||
/Connectors/Rhino/* @oguzhankoral @JR-Morgan
|
||||
/Connectors/Tekla/* @bjoernsteinhagen @dogukankaratas
|
||||
|
||||
# Converters
|
||||
/Convertors/ArcGIS/* @KatKatKateryna
|
||||
/Convertors/Autocad/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Convertors/Civil3d/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Convertors/Autocad/* @oguzhankoral @JR-Morgan @dogukankaratas
|
||||
/Convertors/Civil3d/* @oguzhankoral @JR-Morgan @dogukankaratas
|
||||
/Convertors/CSi/* @bjoernsteinhagen @dogukankaratas
|
||||
/Convertors/Navisworks/* @jsdbroughton
|
||||
/Convertors/Revit/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Convertors/Rhino/* @clairekuang @oguzhankoral @didimitrie
|
||||
/Convertors/Revit/* @oguzhankoral
|
||||
/Convertors/Rhino/* @oguzhankoral @JR-Morgan
|
||||
/Convertors/Tekla/* @bjoernsteinhagen @dogukankaratas
|
||||
|
||||
# DUI
|
||||
|
||||
/DUI3/* @clairekuang @oguzhankoral @didimitrie
|
||||
/DUI3/* @oguzhankoral
|
||||
|
||||
# Importers
|
||||
/Importers/* @JR-Morgan @didimitrie @oguzhankoral @adamhathcock
|
||||
/Importers/* @JR-Morgan @oguzhankoral
|
||||
|
||||
# SDK
|
||||
/SDK/* @JR-Morgan @clairekuang @didimitrie @oguzhankoral @adamhathcock
|
||||
/SDK/* @JR-Morgan @oguzhankoral
|
||||
|
||||
# Build
|
||||
/Build/* @JR-Morgan @oguzhankoral @adamhathcock
|
||||
/Build/* @JR-Morgan @oguzhankoral
|
||||
/.github/* @JR-Morgan @oguzhankoral
|
||||
/.config/* @JR-Morgan @oguzhankoral
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
run: ./build.ps1 zip
|
||||
|
||||
- name: ⬆️ Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: output-${{ env.SEMVER }}
|
||||
path: output/*.*
|
||||
@@ -76,6 +76,6 @@ jobs:
|
||||
display-workflow-run-url-interval: 10s
|
||||
|
||||
# Allows us to inspect the artifacts of failed builds, since this below step will be skipped if the above step fails
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
- uses: geekyeggo/delete-artifact@v6
|
||||
with:
|
||||
name: output-*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<Target AfterTargets="Clean" Name="CleanAddinAutocad" Condition="'$(AutoCADVersion)' != '' And '$(ContinuousIntegrationBuild)' != 'true' And '$(OS)' == 'Windows_NT'">
|
||||
<RemoveDir Directories="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Autocad$(AutoCADVersion);" />
|
||||
<RemoveDir Directories="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Autocad.bundle\Contents\Windows\Speckle.Connectors.Autocad$(AutoCADVersion);" />
|
||||
</Target>
|
||||
|
||||
<Target AfterTargets="Build" Name="AfterBuildAutoCAD" Condition="'$(AutoCADVersion)' != '' And '$(ContinuousIntegrationBuild)' != 'true' And '$(OS)' == 'Windows_NT'">
|
||||
@@ -9,11 +9,12 @@
|
||||
<AutoCADDLLs Include="$(TargetDir)\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Message Text="AutoCAD Version $(AutoCADVersion)" Importance="high"/>
|
||||
<Copy DestinationFolder="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Autocad$(AutoCADVersion)\%(RecursiveDir)" SourceFiles="@(AutoCADDLLs)" />
|
||||
<Copy DestinationFolder="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Autocad.bundle\Contents\Windows\Speckle.Connectors.Autocad$(AutoCADVersion)\%(RecursiveDir)" SourceFiles="@(AutoCADDLLs)" />
|
||||
<Copy DestinationFolder="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Autocad.bundle\" SourceFiles="$(TargetDir)\Plugin\BundleAutoCAD\PackageContents.xml" />
|
||||
</Target>
|
||||
|
||||
<Target AfterTargets="Clean" Name="CleanAddinCivil3D" Condition="'$(Civil3DVersion)' != '' And '$(ContinuousIntegrationBuild)' != 'true' And '$(OS)' == 'Windows_NT'">
|
||||
<RemoveDir Directories="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Civil3d$(Civil3DVersion);" />
|
||||
<RemoveDir Directories="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Civil3d.bundle\Contents\Windows\Speckle.Connectors.Civil3d$(Civil3DVersion);" />
|
||||
</Target>
|
||||
|
||||
<Target AfterTargets="Build" Name="AfterBuildCivil3D" Condition="'$(Civil3DVersion)' != '' And '$(ContinuousIntegrationBuild)' != 'true' And '$(OS)' == 'Windows_NT'">
|
||||
@@ -21,7 +22,8 @@
|
||||
<Civil3DDLLs Include="$(TargetDir)\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Message Text="Civil3D Version $(Civil3DVersion)" Importance="high"/>
|
||||
<Copy DestinationFolder="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Civil3d$(Civil3DVersion)\%(RecursiveDir)" SourceFiles="@(Civil3DDLLs)" />
|
||||
<Copy DestinationFolder="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Civil3d.bundle\Contents\Windows\Speckle.Connectors.Civil3d$(Civil3DVersion)\%(RecursiveDir)" SourceFiles="@(Civil3DDLLs)" />
|
||||
<Copy DestinationFolder="$(AppData)\Autodesk\ApplicationPlugins\Speckle.Connectors.Civil3d.bundle\" SourceFiles="$(TargetDir)\Plugin\BundleCivil3D\PackageContents.xml" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition="'$(AutoCADVersion)' != '' And '$(ContinuousIntegrationBuild)' != 'true' And '$(OS)' == 'Windows_NT'">
|
||||
|
||||
+68
-2
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.AutoCAD.Geometry;
|
||||
using Speckle.Connectors.Autocad.HostApp.Extensions;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Connectors.Common.Cancellation;
|
||||
@@ -40,6 +41,9 @@ public abstract class AutocadSendBaseBinding : ISendBinding
|
||||
/// </summary>
|
||||
private ConcurrentBag<string> ChangedObjectIds { get; set; } = new();
|
||||
|
||||
private readonly List<string> _docSubsTracker = new();
|
||||
private readonly Dictionary<string, Matrix3d> _docUcsTracker = new();
|
||||
|
||||
protected AutocadSendBaseBinding(
|
||||
DocumentModelStore store,
|
||||
IBrowserBridge parent,
|
||||
@@ -71,6 +75,10 @@ public abstract class AutocadSendBaseBinding : ISendBinding
|
||||
// catches the case when autocad just opens up with a blank new doc
|
||||
SubscribeToObjectChanges(Application.DocumentManager.CurrentDocument);
|
||||
}
|
||||
|
||||
Application.SystemVariableChanged += (_, e) =>
|
||||
_topLevelExceptionHandler.CatchUnhandled(() => OnSystemVariableChanged(e));
|
||||
|
||||
// Since ids of the objects generates from same seed, we should clear the cache always whenever doc swapped.
|
||||
_store.DocumentChanged += (_, _) =>
|
||||
{
|
||||
@@ -78,8 +86,6 @@ public abstract class AutocadSendBaseBinding : ISendBinding
|
||||
};
|
||||
}
|
||||
|
||||
private readonly List<string> _docSubsTracker = new();
|
||||
|
||||
private void SubscribeToObjectChanges(Document doc)
|
||||
{
|
||||
if (doc == null || doc.Database == null || _docSubsTracker.Contains(doc.Name))
|
||||
@@ -88,11 +94,58 @@ public abstract class AutocadSendBaseBinding : ISendBinding
|
||||
}
|
||||
|
||||
_docSubsTracker.Add(doc.Name);
|
||||
_docUcsTracker[doc.Name] = doc.Editor.CurrentUserCoordinateSystem;
|
||||
|
||||
doc.Database.ObjectAppended += (_, e) => OnObjectChanged(e.DBObject);
|
||||
doc.Database.ObjectErased += (_, e) => OnObjectChanged(e.DBObject);
|
||||
doc.Database.ObjectModified += (_, e) => OnObjectChanged(e.DBObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles system variable changes to detect UCS modifications.
|
||||
/// When UCS changes, clears the conversion cache and expires all sender model cards.
|
||||
/// </summary>
|
||||
private void OnSystemVariableChanged(Autodesk.AutoCAD.ApplicationServices.SystemVariableChangedEventArgs e)
|
||||
{
|
||||
// check if this is a UCS-defining system variable
|
||||
string varName = e.Name.ToUpperInvariant();
|
||||
bool isUcsChange = varName == "UCSNAME" || varName == "UCSORG" || varName == "UCSXDIR" || varName == "UCSYDIR";
|
||||
|
||||
if (!isUcsChange)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// get the currently active document
|
||||
Document doc = Application.DocumentManager.MdiActiveDocument;
|
||||
if (doc == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentUcs = doc.Editor.CurrentUserCoordinateSystem;
|
||||
|
||||
// first time tracking this document's UCS
|
||||
if (!_docUcsTracker.TryGetValue(doc.Name, out Matrix3d storedUcs))
|
||||
{
|
||||
_docUcsTracker[doc.Name] = currentUcs;
|
||||
return;
|
||||
}
|
||||
|
||||
// ucs hasn't actually changed (multiple variables fire for single UCS change)
|
||||
if (currentUcs.IsEqualTo(storedUcs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ucs has changed - all cached conversions invalid
|
||||
_sendConversionCache.ClearCache();
|
||||
_docUcsTracker[doc.Name] = currentUcs;
|
||||
|
||||
// expire all sender model cards
|
||||
_idleManager.SubscribeToIdle(nameof(ExpireAllSenders), async () => await ExpireAllSenders());
|
||||
}
|
||||
|
||||
private void OnObjectChanged(DBObject dbObject) =>
|
||||
_topLevelExceptionHandler.CatchUnhandled(() => OnChangeChangedObjectIds(dbObject));
|
||||
|
||||
@@ -123,6 +176,19 @@ public abstract class AutocadSendBaseBinding : ISendBinding
|
||||
ChangedObjectIds = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expires all sender model cards when a global change occurs (like UCS change).
|
||||
/// </summary>
|
||||
private async Task ExpireAllSenders()
|
||||
{
|
||||
var senders = _store.GetSenders();
|
||||
var expiredSenderIds = senders.Select(s => s.ModelCardId.NotNull()).ToList();
|
||||
if (expiredSenderIds.Count > 0)
|
||||
{
|
||||
await Commands.SetModelsExpired(expiredSenderIds);
|
||||
}
|
||||
}
|
||||
|
||||
public List<ISendFilter> GetSendFilters() => _sendFilters;
|
||||
|
||||
public List<ICardSetting> GetSendSettings() => [];
|
||||
|
||||
+5
-5
@@ -3,9 +3,9 @@ using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connectors.Autocad.HostApp.Extensions;
|
||||
using Speckle.Connectors.Autocad.Operations.Send;
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Converters.Autocad.Helpers;
|
||||
using Speckle.Converters.AutocadShared.ToSpeckle;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
@@ -46,6 +46,7 @@ public class AutocadInstanceUnpacker : IInstanceUnpacker<AutocadRootObject>
|
||||
{
|
||||
UnpackInstance(blockReference, 0, transaction);
|
||||
}
|
||||
|
||||
_instanceObjectsManager.AddAtomicObject(obj.ApplicationId, obj);
|
||||
}
|
||||
return _instanceObjectsManager.GetUnpackResult();
|
||||
@@ -66,13 +67,14 @@ public class AutocadInstanceUnpacker : IInstanceUnpacker<AutocadRootObject>
|
||||
? instance.AnonymousBlockTableRecord
|
||||
: instance.BlockTableRecord;
|
||||
|
||||
// transforms on instances are always stored in WCS
|
||||
InstanceProxy instanceProxy =
|
||||
new()
|
||||
{
|
||||
applicationId = instanceId,
|
||||
definitionId = definitionId.ToString(),
|
||||
maxDepth = depth,
|
||||
transform = GetMatrix(instance.BlockTransform.ToArray()),
|
||||
transform = TransformHelper.ConvertToInstanceMatrix4x4(instance.BlockTransform),
|
||||
units = _unitsConverter.ConvertOrThrow(Application.DocumentManager.CurrentDocument.Database.Insunits)
|
||||
};
|
||||
|
||||
@@ -173,6 +175,7 @@ public class AutocadInstanceUnpacker : IInstanceUnpacker<AutocadRootObject>
|
||||
UnpackInstance(blockReference, depth + 1, transaction);
|
||||
}
|
||||
|
||||
_instanceObjectsManager.AddAtomicDefinitionObjectId(appId);
|
||||
_instanceObjectsManager.AddAtomicObject(appId, new(obj, appId));
|
||||
}
|
||||
|
||||
@@ -183,7 +186,4 @@ public class AutocadInstanceUnpacker : IInstanceUnpacker<AutocadRootObject>
|
||||
_logger.LogError(ex, "Failed unpacking Autocad instance");
|
||||
}
|
||||
}
|
||||
|
||||
private Matrix4x4 GetMatrix(double[] t) =>
|
||||
new(t[0], t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8], t[9], t[10], t[11], t[12], t[13], t[14], t[15]);
|
||||
}
|
||||
|
||||
+12
@@ -51,6 +51,12 @@ public class AutocadMaterialUnpacker
|
||||
|
||||
if (transaction.GetObject(entity.MaterialId, OpenMode.ForRead) is Material material)
|
||||
{
|
||||
// skip default material
|
||||
if (material.Name == "Global")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string materialId = material.GetSpeckleApplicationId();
|
||||
if (materialProxies.TryGetValue(materialId, out RenderMaterialProxy? value))
|
||||
{
|
||||
@@ -77,6 +83,12 @@ public class AutocadMaterialUnpacker
|
||||
{
|
||||
if (transaction.GetObject(layer.MaterialId, OpenMode.ForRead) is Material material)
|
||||
{
|
||||
// skip default material
|
||||
if (material.Name == "Global")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string materialId = material.GetSpeckleApplicationId();
|
||||
string layerId = layer.GetSpeckleApplicationId(); // Do not use handle directly, see note in the 'GetSpeckleApplicationId' method
|
||||
if (materialProxies.TryGetValue(materialId, out RenderMaterialProxy? value))
|
||||
|
||||
+60
-8
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.AutoCAD.Geometry;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connectors.Autocad.HostApp;
|
||||
using Speckle.Connectors.Common.Builders;
|
||||
@@ -7,6 +8,8 @@ using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Connectors.Common.Conversion;
|
||||
using Speckle.Connectors.Common.Extensions;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Converters.Autocad;
|
||||
using Speckle.Converters.Autocad.Helpers;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Logging;
|
||||
@@ -19,6 +22,7 @@ namespace Speckle.Connectors.Autocad.Operations.Send;
|
||||
public abstract class AutocadRootObjectBaseBuilder : IRootObjectBuilder<AutocadRootObject>
|
||||
{
|
||||
private readonly IRootToSpeckleConverter _converter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _converterSettings;
|
||||
private readonly string[] _documentPathSeparator = ["\\"];
|
||||
private readonly ISendConversionCache _sendConversionCache;
|
||||
private readonly AutocadInstanceUnpacker _instanceUnpacker;
|
||||
@@ -30,6 +34,7 @@ public abstract class AutocadRootObjectBaseBuilder : IRootObjectBuilder<AutocadR
|
||||
|
||||
protected AutocadRootObjectBaseBuilder(
|
||||
IRootToSpeckleConverter converter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> converterSettings,
|
||||
ISendConversionCache sendConversionCache,
|
||||
AutocadInstanceUnpacker instanceObjectManager,
|
||||
AutocadMaterialUnpacker materialUnpacker,
|
||||
@@ -40,6 +45,7 @@ public abstract class AutocadRootObjectBaseBuilder : IRootObjectBuilder<AutocadR
|
||||
)
|
||||
{
|
||||
_converter = converter;
|
||||
_converterSettings = converterSettings;
|
||||
_sendConversionCache = sendConversionCache;
|
||||
_instanceUnpacker = instanceObjectManager;
|
||||
_materialUnpacker = materialUnpacker;
|
||||
@@ -81,17 +87,35 @@ public abstract class AutocadRootObjectBaseBuilder : IRootObjectBuilder<AutocadR
|
||||
using Transaction tr = doc.Database.TransactionManager.StartTransaction();
|
||||
|
||||
// 1 - Unpack the instances
|
||||
var (atomicObjects, instanceProxies, instanceDefinitionProxies) = _instanceUnpacker.UnpackSelection(objects);
|
||||
var (atomicObjects, atomicDefinitionObjectIds, instanceProxies, instanceDefinitionProxies) =
|
||||
_instanceUnpacker.UnpackSelection(objects);
|
||||
root[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies;
|
||||
|
||||
// 2 - Unpack the groups
|
||||
root[ProxyKeys.GROUP] = _groupUnpacker.UnpackGroups(atomicObjects);
|
||||
|
||||
// 3 - Add the Reference Point
|
||||
Matrix3d? referenceTransform = null;
|
||||
if (
|
||||
Application.DocumentManager.CurrentDocument.Editor.CurrentUserCoordinateSystem is Matrix3d matrix
|
||||
&& matrix != Matrix3d.Identity
|
||||
)
|
||||
{
|
||||
referenceTransform = matrix.Inverse();
|
||||
|
||||
/* POC: Do not attach transform to root for now! we are not consuming this in autocad/civil on receive and in revit it will undo all baked transforms :(
|
||||
var transformMatrix = ReferencePointHelper.CreateTransformDataForRootObject(matrix);
|
||||
root[ReferencePointHelper.REFERENCE_POINT_TRANSFORM_KEY] = transformMatrix;
|
||||
*/
|
||||
}
|
||||
|
||||
using (var _ = _activityFactory.Start("Converting objects"))
|
||||
{
|
||||
// 3 - Convert atomic objects
|
||||
List<LayerTableRecord> usedAcadLayers = new(); // Keeps track of autocad layers used, so we can pass them on later to the material and color unpacker.
|
||||
List<SendConversionResult> results = new();
|
||||
int count = 0;
|
||||
|
||||
// 4 - Convert atomic objects
|
||||
foreach (var (entity, applicationId) in atomicObjects)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -104,9 +128,28 @@ public abstract class AutocadRootObjectBaseBuilder : IRootObjectBuilder<AutocadR
|
||||
root.elements.Add(objectCollection);
|
||||
}
|
||||
|
||||
var result = ConvertAutocadEntity(entity, applicationId, objectCollection, instanceProxies, projectId);
|
||||
results.Add(result);
|
||||
SendConversionResult? result = null;
|
||||
// If this is a atomic definition object, we *do not* want to bake in the reference point transform to the object
|
||||
if (atomicDefinitionObjectIds.Contains(applicationId))
|
||||
{
|
||||
using (_converterSettings.Push(currentSettings => currentSettings with { ReferencePointTransform = null }))
|
||||
{
|
||||
result = ConvertAutocadEntity(entity, applicationId, objectCollection, instanceProxies, projectId);
|
||||
}
|
||||
}
|
||||
else // this is a selected atomic object (not part of definition)
|
||||
{
|
||||
result = ConvertAutocadEntity(
|
||||
entity,
|
||||
applicationId,
|
||||
objectCollection,
|
||||
instanceProxies,
|
||||
projectId,
|
||||
referenceTransform // set this for top level instance proxies to use if needed
|
||||
);
|
||||
}
|
||||
|
||||
results.Add(result);
|
||||
onOperationProgressed.Report(new("Converting", (double)++count / atomicObjects.Count));
|
||||
}
|
||||
|
||||
@@ -115,10 +158,10 @@ public abstract class AutocadRootObjectBaseBuilder : IRootObjectBuilder<AutocadR
|
||||
throw new SpeckleException("Failed to convert all objects."); // fail fast instead creating empty commit! It will appear as model card error with red color.
|
||||
}
|
||||
|
||||
// 4 - Unpack the render material proxies
|
||||
// 5 - Unpack the render material proxies
|
||||
root[ProxyKeys.RENDER_MATERIAL] = _materialUnpacker.UnpackMaterials(atomicObjects, usedAcadLayers);
|
||||
|
||||
// 5 - Unpack the color proxies
|
||||
// 6 - Unpack the color proxies
|
||||
root[ProxyKeys.COLOR] = _colorUnpacker.UnpackColors(atomicObjects, usedAcadLayers);
|
||||
|
||||
// add any additional properties (most likely from verticals)
|
||||
@@ -143,15 +186,24 @@ public abstract class AutocadRootObjectBaseBuilder : IRootObjectBuilder<AutocadR
|
||||
string applicationId,
|
||||
Collection collectionHost,
|
||||
IReadOnlyDictionary<string, InstanceProxy> instanceProxies,
|
||||
string projectId
|
||||
string projectId,
|
||||
Matrix3d? transform = null
|
||||
)
|
||||
{
|
||||
string sourceType = entity.GetType().ToString();
|
||||
try
|
||||
{
|
||||
Base converted;
|
||||
if (entity is BlockReference && instanceProxies.TryGetValue(applicationId, out InstanceProxy? instanceProxy))
|
||||
if (entity is BlockReference br && instanceProxies.TryGetValue(applicationId, out InstanceProxy? instanceProxy))
|
||||
{
|
||||
// modify transform by reference point this if it is top level
|
||||
if (instanceProxy.maxDepth == 0 && transform is Matrix3d validTransform)
|
||||
{
|
||||
instanceProxy.transform = TransformHelper.ConvertToInstanceMatrix4x4(
|
||||
br.BlockTransform.PreMultiplyBy(validTransform)
|
||||
);
|
||||
}
|
||||
|
||||
converted = instanceProxy;
|
||||
}
|
||||
else if (_sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
|
||||
|
||||
+3
@@ -2,6 +2,7 @@ using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connectors.Autocad.HostApp;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Converters.Autocad;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
@@ -15,6 +16,7 @@ public sealed class AutocadRootObjectBuilder : AutocadRootObjectBaseBuilder
|
||||
public AutocadRootObjectBuilder(
|
||||
AutocadLayerUnpacker layerUnpacker,
|
||||
IRootToSpeckleConverter converter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> converterSettings,
|
||||
ISendConversionCache sendConversionCache,
|
||||
AutocadInstanceUnpacker instanceObjectManager,
|
||||
AutocadMaterialUnpacker materialUnpacker,
|
||||
@@ -25,6 +27,7 @@ public sealed class AutocadRootObjectBuilder : AutocadRootObjectBaseBuilder
|
||||
)
|
||||
: base(
|
||||
converter,
|
||||
converterSettings,
|
||||
sendConversionCache,
|
||||
instanceObjectManager,
|
||||
materialUnpacker,
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ApplicationPackage
|
||||
SchemaVersion="1.0"
|
||||
AppVersion="1.0"
|
||||
Author="AEC SYSTEMS LTD"
|
||||
Name="Speckle for AutoCAD"
|
||||
Description="Speckle for AutoCAD"
|
||||
Icon="./Contents/Resources/Logo.ico"
|
||||
ProductCode="{958850b9-3782-4e6f-b9b3-1b570e0fcae1}"
|
||||
UpgradeCode="{274de351-80aa-4972-9b45-6a93f8ce968f}"> <!-- For now, we're not updating the AppVersion & ProductCodes with each version -->
|
||||
|
||||
<CompanyDetails
|
||||
Name="Speckle"
|
||||
Url="https://speckle.systems"
|
||||
Email="support@speckle.systems" />
|
||||
<Components>
|
||||
|
||||
<!-- AutoCAD 2022 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Autocad2022"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Autocad2022/Speckle.Connectors.Autocad2022.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="AutoCAD"
|
||||
SeriesMin="R24.1"
|
||||
SeriesMax="R24.1" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2023 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Autocad2023"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Autocad2023/Speckle.Connectors.Autocad2023.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="AutoCAD"
|
||||
SeriesMin="R24.2"
|
||||
SeriesMax="R24.2" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2024 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Autocad2024"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Autocad2024/Speckle.Connectors.Autocad2024.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="AutoCAD"
|
||||
SeriesMin="R24.3"
|
||||
SeriesMax="R24.3" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2025 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Autocad2025"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Autocad2025/Speckle.Connectors.Autocad2025.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="AutoCAD"
|
||||
SeriesMin="R25.0"
|
||||
SeriesMax="R25.0" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2026 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Autocad2026"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Autocad2026/Speckle.Connectors.Autocad2026.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="AutoCAD"
|
||||
SeriesMin="R25.1"
|
||||
SeriesMax="R25.1" />
|
||||
</ComponentEntry>
|
||||
</Components>
|
||||
|
||||
</ApplicationPackage>
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ApplicationPackage
|
||||
SchemaVersion="1.0"
|
||||
AppVersion="1.0"
|
||||
Author="AEC SYSTEMS LTD"
|
||||
Name="Speckle for Civil 3D"
|
||||
Description="Speckle for Civil 3D"
|
||||
Icon="./Contents/Resources/Logo.ico"
|
||||
ProductCode="{cecad7b2-daa9-4a9d-941d-737619ffd999}"
|
||||
UpgradeCode="{ccf4134d-0e24-4e91-8bf4-781ec7313bef}"> <!-- For now, we're not updating the AppVersion & ProductCodes with each version -->
|
||||
|
||||
<CompanyDetails
|
||||
Name="Speckle"
|
||||
Url="https://speckle.systems"
|
||||
Email="support@speckle.systems" />
|
||||
<Components>
|
||||
|
||||
<!-- AutoCAD 2022 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Civil3d2022"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Civil3d2022/Speckle.Connectors.Civil3d2022.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="Civil3D"
|
||||
SeriesMin="R24.1"
|
||||
SeriesMax="R24.1" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2023 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Civil3d2023"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Civil3d2023/Speckle.Connectors.Civil3d2023.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="Civil3D"
|
||||
SeriesMin="R24.2"
|
||||
SeriesMax="R24.2" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2024 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Civil3d2024"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Civil3d2024/Speckle.Connectors.Civil3d2024.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="Civil3D"
|
||||
SeriesMin="R24.3"
|
||||
SeriesMax="R24.3" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2025 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Civil3d2025"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Civil3d2025/Speckle.Connectors.Civil3d2025.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="Civil3D"
|
||||
SeriesMin="R25.0"
|
||||
SeriesMax="R25.0" />
|
||||
</ComponentEntry>
|
||||
|
||||
<!-- AutoCAD 2026 -->
|
||||
<ComponentEntry
|
||||
AppName="Speckle.Connectors.Civil3d2026"
|
||||
ModuleName="./Contents/Windows/Speckle.Connectors.Civil3d2026/Speckle.Connectors.Civil3d2026.dll"
|
||||
LoadOnAutoCADStartup="True">
|
||||
<RuntimeRequirements
|
||||
OS="Win64"
|
||||
Platform="Civil3D"
|
||||
SeriesMin="R25.1"
|
||||
SeriesMax="R25.1" />
|
||||
</ComponentEntry>
|
||||
</Components>
|
||||
|
||||
</ApplicationPackage>
|
||||
+8
@@ -53,4 +53,12 @@
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Resources\s2logo16.png" />
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Resources\s2logo32.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="$(MSBuildThisFileDirectory)Plugin\BundleCivil3D\PackageContents.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="$(MSBuildThisFileDirectory)Plugin\BundleAutoCAD\PackageContents.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+3
@@ -4,6 +4,7 @@ using Speckle.Connectors.Autocad.HostApp;
|
||||
using Speckle.Connectors.Autocad.Operations.Send;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Converters.Autocad;
|
||||
using Speckle.Converters.Civil3dShared.ToSpeckle;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Sdk.Logging;
|
||||
@@ -20,6 +21,7 @@ public sealed class Civil3dRootObjectBuilder : AutocadRootObjectBaseBuilder
|
||||
AutocadLayerUnpacker layerUnpacker,
|
||||
PropertySetDefinitionHandler propertySetDefinitionHandler,
|
||||
IRootToSpeckleConverter converter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> converterSettings,
|
||||
ISendConversionCache sendConversionCache,
|
||||
AutocadInstanceUnpacker instanceObjectManager,
|
||||
AutocadMaterialUnpacker materialUnpacker,
|
||||
@@ -30,6 +32,7 @@ public sealed class Civil3dRootObjectBuilder : AutocadRootObjectBaseBuilder
|
||||
)
|
||||
: base(
|
||||
converter,
|
||||
converterSettings,
|
||||
sendConversionCache,
|
||||
instanceObjectManager,
|
||||
materialUnpacker,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connector.Navisworks.HostApp;
|
||||
using Speckle.Connectors.Common.Builders;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
@@ -366,13 +366,13 @@ public class NavisworksRootObjectBuilder(
|
||||
{
|
||||
var groupKey = kvp.Key;
|
||||
var geometries = kvp.Value;
|
||||
var groupKeyHash = groupKey.ToHashString();
|
||||
var groupKeyPath = groupKey.ToPathString();
|
||||
|
||||
var defProxy = new InstanceDefinitionProxy
|
||||
{
|
||||
name = $"Shared Geometry {groupKeyHash}",
|
||||
name = $"Shared Geometry {groupKeyPath}",
|
||||
objects = geometries.Select(g => g.applicationId ?? "").Where(id => !string.IsNullOrEmpty(id)).ToList(),
|
||||
applicationId = $"{DEFINITION_ID_PREFIX}{groupKeyHash}",
|
||||
applicationId = $"{DEFINITION_ID_PREFIX}{groupKeyPath}",
|
||||
maxDepth = 0
|
||||
};
|
||||
|
||||
|
||||
+15
-2
@@ -2,6 +2,8 @@ using Autodesk.Revit.DB;
|
||||
using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Connectors.DUI.Models;
|
||||
using Speckle.Connectors.DUI.Models.Card;
|
||||
using Speckle.Connectors.DUI.Utils;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Connectors.Revit.Plugin;
|
||||
using Speckle.Connectors.RevitShared;
|
||||
using Speckle.Connectors.RevitShared.Operations.Send.Filters;
|
||||
@@ -24,6 +26,8 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
|
||||
private readonly ISpeckleApplication _speckleApplication;
|
||||
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
|
||||
private readonly IRevitTask _revitTask;
|
||||
private readonly ParameterUpdater _parameterUpdater;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
public BasicConnectorBindingRevit(
|
||||
DocumentModelStore store,
|
||||
@@ -31,7 +35,9 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
|
||||
RevitContext revitContext,
|
||||
ISpeckleApplication speckleApplication,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
IRevitTask revitTask
|
||||
IRevitTask revitTask,
|
||||
ParameterUpdater parameterUpdater,
|
||||
IJsonSerializer jsonSerializer
|
||||
)
|
||||
{
|
||||
Name = "baseBinding";
|
||||
@@ -41,6 +47,8 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
|
||||
_speckleApplication = speckleApplication;
|
||||
_topLevelExceptionHandler = topLevelExceptionHandler;
|
||||
_revitTask = revitTask;
|
||||
_parameterUpdater = parameterUpdater;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
Commands = new BasicConnectorBindingCommands(parent);
|
||||
|
||||
_store.DocumentChanged += (_, _) =>
|
||||
@@ -73,7 +81,12 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
|
||||
}
|
||||
|
||||
//should this use the Hashcode of the document instead of something like CreationGUID?
|
||||
var info = new DocumentInfo(doc.PathName, doc.Title, doc.GetHashCode().ToString());
|
||||
var info = new DocumentInfo(
|
||||
doc.PathName,
|
||||
doc.Title,
|
||||
doc.GetHashCode().ToString(),
|
||||
doc.IsModelInCloud ? doc.GetCloudModelUrn() : null
|
||||
);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Speckle.Connectors.DUI.Bindings;
|
||||
using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Connectors.DUI.Utils;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Connectors.Revit.Operations.Receive;
|
||||
using Speckle.Connectors.Revit.Plugin;
|
||||
using Speckle.Connectors.RevitShared;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Bindings;
|
||||
|
||||
public static class ParameterScopes
|
||||
{
|
||||
public const string INSTANCE = "Instance Parameters";
|
||||
public const string TYPE = "Type Parameters";
|
||||
public const string SYSTEM_TYPE = "System Type Parameters";
|
||||
}
|
||||
|
||||
public record ParsedParameterPath(string Scope, string Category, string Name)
|
||||
{
|
||||
public string[] ToArray() => [Scope, Category, Name];
|
||||
}
|
||||
|
||||
internal sealed class RevitParametersBinding : IParametersBinding
|
||||
{
|
||||
public string Name => "parametersBinding";
|
||||
public IBrowserBridge Parent { get; }
|
||||
|
||||
private readonly RevitContext _revitContext;
|
||||
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
|
||||
private readonly IRevitTask _revitTask;
|
||||
private readonly ParameterUpdater _parameterUpdater;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IBasicConnectorBinding _baseBinding;
|
||||
|
||||
public RevitParametersBinding(
|
||||
IBrowserBridge parent,
|
||||
RevitContext revitContext,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
IRevitTask revitTask,
|
||||
ParameterUpdater parameterUpdater,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IBasicConnectorBinding baseBinding
|
||||
)
|
||||
{
|
||||
Parent = parent;
|
||||
_revitContext = revitContext;
|
||||
_topLevelExceptionHandler = topLevelExceptionHandler;
|
||||
_revitTask = revitTask;
|
||||
_parameterUpdater = parameterUpdater;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_baseBinding = baseBinding;
|
||||
}
|
||||
|
||||
public async Task Update(string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wrapper = _jsonSerializer.Deserialize<ParameterChangesWrapper>(payload);
|
||||
var requests = wrapper?.Changes;
|
||||
|
||||
if (requests == null || requests.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var activeUIDoc =
|
||||
_revitContext.UIApplication?.ActiveUIDocument
|
||||
?? throw new SpeckleException("Unable to retrieve active UI document");
|
||||
var doc = activeUIDoc.Document;
|
||||
|
||||
int successCount = 0;
|
||||
List<string> errors = [];
|
||||
|
||||
await _revitTask
|
||||
.RunAsync(() =>
|
||||
{
|
||||
using var t = new Transaction(doc, "Speckle: Apply Parameter Changes");
|
||||
|
||||
// silence pop-ups like "duplicate mark values" etc. which blocks our param updates
|
||||
var failureOptions = t.GetFailureHandlingOptions();
|
||||
failureOptions.SetFailuresPreprocessor(new HideWarningsFailuresPreprocessor());
|
||||
t.SetFailureHandlingOptions(failureOptions);
|
||||
|
||||
t.Start();
|
||||
|
||||
foreach (var request in requests)
|
||||
{
|
||||
if (!TryValidateAndParseRequest(doc, request, out var element, out var parsedPath, out var errorMessage))
|
||||
{
|
||||
errors.Add(errorMessage!);
|
||||
continue;
|
||||
}
|
||||
|
||||
object? rawValue = request.To;
|
||||
if (rawValue is Newtonsoft.Json.Linq.JValue jValue)
|
||||
{
|
||||
rawValue = jValue.Value;
|
||||
}
|
||||
|
||||
var result = _parameterUpdater.Update(
|
||||
element!,
|
||||
parsedPath!.ToArray(),
|
||||
rawValue,
|
||||
request.InternalDefinitionName
|
||||
);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(result.ErrorMessage ?? "Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
t.Commit();
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
var groupedErrors = errors.GroupBy(e => e).Select(g => g.Count() > 1 ? $"{g.Count()} x {g.Key}" : g.Key);
|
||||
await _baseBinding.Commands.SetGlobalNotification(
|
||||
ToastNotificationType.WARNING,
|
||||
"Update Completed with Issues",
|
||||
$"Applied {successCount} updates. Encountered {errors.Count} errors: {string.Join(" | ", groupedErrors)}",
|
||||
autoClose: false
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _baseBinding.Commands.SetGlobalNotification(
|
||||
ToastNotificationType.SUCCESS,
|
||||
"Parameters Updated",
|
||||
$"Successfully applied {successCount} updates."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_topLevelExceptionHandler.CatchUnhandled(
|
||||
() => throw new SpeckleException("Failed to apply parameter updates", ex)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryValidateAndParseRequest(
|
||||
Document doc,
|
||||
ParameterChangeRequest request,
|
||||
out Element? element,
|
||||
out ParsedParameterPath? parsedPath,
|
||||
out string? errorMessage
|
||||
)
|
||||
{
|
||||
element = null;
|
||||
parsedPath = null;
|
||||
errorMessage = null;
|
||||
|
||||
if (string.IsNullOrEmpty(request.ApplicationId))
|
||||
{
|
||||
errorMessage = "Missing ApplicationId.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var elementId = ElementIdHelper.GetElementIdFromUniqueId(doc, request.ApplicationId);
|
||||
if (elementId == null)
|
||||
{
|
||||
errorMessage = $"Element UniqueId not found: {request.ApplicationId}";
|
||||
return false;
|
||||
}
|
||||
|
||||
element = doc.GetElement(elementId);
|
||||
if (element == null)
|
||||
{
|
||||
errorMessage = $"Element is null for Id: {elementId}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var rawPath = request.Path;
|
||||
if (string.IsNullOrEmpty(rawPath))
|
||||
{
|
||||
errorMessage = "Path is missing.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rawPath.StartsWith("properties.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
rawPath = rawPath[11..];
|
||||
}
|
||||
else if (rawPath.StartsWith("parameters.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
rawPath = rawPath[11..];
|
||||
}
|
||||
|
||||
var pathParts = rawPath.Split(['.'], 3);
|
||||
if (pathParts.Length != 3)
|
||||
{
|
||||
errorMessage = $"Path must have 3 parts. Got: '{rawPath}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
parsedPath = new ParsedParameterPath(pathParts[0], pathParts[1], pathParts[2]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ParameterChangesWrapper
|
||||
{
|
||||
public List<ParameterChangeRequest>? Changes { get; set; }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Speckle.Connectors.Common.Cancellation;
|
||||
using Speckle.Connectors.DUI.Bindings;
|
||||
using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Connectors.DUI.Settings;
|
||||
using Speckle.Connectors.Revit.Operations.Receive;
|
||||
using Speckle.Connectors.Revit.Plugin;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
@@ -24,7 +25,8 @@ public sealed class RevitReceiveBinding(
|
||||
private IReceiveBindingUICommands Commands { get; } = new ReceiveBindingUICommands(parent);
|
||||
|
||||
#pragma warning disable CA1024
|
||||
public List<ICardSetting> GetReceiveSettings() => [new Operations.Receive.Settings.ReceiveReferencePointSetting()];
|
||||
public List<ICardSetting> GetReceiveSettings() =>
|
||||
[new Operations.Receive.Settings.ReceiveReferencePointSetting(), new ReceiveInstancesAsFamiliesSetting()];
|
||||
#pragma warning restore CA1024
|
||||
|
||||
public void CancelReceive(string modelCardId) => cancellationManager.CancelOperation(modelCardId);
|
||||
@@ -45,7 +47,8 @@ public sealed class RevitReceiveBinding(
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
toHostSettingsManager.GetReceiveInstancesAsFamiliesSetting(card)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
private readonly LinkedModelHandler _linkedModelHandler;
|
||||
private readonly IThreadContext _threadContext;
|
||||
private readonly ISendOperationManagerFactory _sendOperationManagerFactory;
|
||||
private readonly ParameterUpdater _parameterUpdater;
|
||||
private bool _isDocChangedSubscribed;
|
||||
private EventHandler<Autodesk.Revit.DB.Events.DocumentChangedEventArgs>? _documentChangedHandler;
|
||||
private readonly ConnectorConfig _config;
|
||||
@@ -67,6 +68,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
IThreadContext threadContext,
|
||||
IRevitTask revitTask,
|
||||
ISendOperationManagerFactory sendOperationManagerFactory,
|
||||
ParameterUpdater parameterUpdater,
|
||||
IConfigStore configStore
|
||||
)
|
||||
: base("sendBinding", bridge)
|
||||
@@ -84,6 +86,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
_linkedModelHandler = linkedModelHandler;
|
||||
_threadContext = threadContext;
|
||||
_sendOperationManagerFactory = sendOperationManagerFactory;
|
||||
_parameterUpdater = parameterUpdater;
|
||||
_config = configStore.GetConnectorConfig();
|
||||
|
||||
Commands = new SendBindingUICommands(bridge);
|
||||
@@ -187,7 +190,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
_toSpeckleSettingsManager.GetSendParameterNullOrEmptyStringsSetting(document, card),
|
||||
_toSpeckleSettingsManager.GetLinkedModelsSetting(document, card),
|
||||
_toSpeckleSettingsManager.GetSendRebarsAsVolumetric(document, card),
|
||||
_toSpeckleSettingsManager.GetSendAreasAsMesh(document, card)
|
||||
_toSpeckleSettingsManager.GetSendAreasAsMesh(document, card),
|
||||
_toSpeckleSettingsManager.GetSendRebarsAsVolumetric(document, card)
|
||||
)
|
||||
);
|
||||
},
|
||||
@@ -197,6 +201,44 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
);
|
||||
}
|
||||
|
||||
public async Task UpdateParameters(List<ParameterChangeRequest> changes)
|
||||
{
|
||||
var document = _revitContext.UIApplication?.ActiveUIDocument?.Document;
|
||||
if (document == null)
|
||||
{
|
||||
throw new SpeckleException("No document is active.");
|
||||
}
|
||||
|
||||
await _threadContext.RunOnMainAsync(() =>
|
||||
{
|
||||
using var transaction = new Transaction(document, "Speckle Parameter Updates");
|
||||
transaction.Start();
|
||||
|
||||
foreach (var change in changes)
|
||||
{
|
||||
var element = document.GetElement(change.ApplicationId);
|
||||
if (element == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = ParsePath(change.Path);
|
||||
var result = _parameterUpdater.Update(element, path, change.To);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
return Task.FromResult(true);
|
||||
});
|
||||
}
|
||||
|
||||
private string[] ParsePath(string concatenatedPath)
|
||||
{
|
||||
// "properties.Parameters.Type Parameters.Other.Family Name"
|
||||
// → ["Type Parameters", "Other", "Family Name"]
|
||||
var segments = concatenatedPath.Split('.');
|
||||
return segments.Skip(2).ToArray();
|
||||
}
|
||||
|
||||
private static (string? fileName, long? fileBytes) GetFileInfo(Document document)
|
||||
{
|
||||
string fullPath = document.PathName;
|
||||
|
||||
+11
@@ -53,6 +53,9 @@ public static class ServiceRegistration
|
||||
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IBasicConnectorBinding>());
|
||||
serviceCollection.AddSingleton<IBasicConnectorBinding, BasicConnectorBindingRevit>();
|
||||
|
||||
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IParametersBinding>());
|
||||
serviceCollection.AddSingleton<IParametersBinding, RevitParametersBinding>();
|
||||
|
||||
// serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
|
||||
|
||||
// send operation and dependencies
|
||||
@@ -66,15 +69,23 @@ public static class ServiceRegistration
|
||||
serviceCollection.AddSingleton<ToSpeckleSettingsManager>();
|
||||
serviceCollection.AddSingleton<ToHostSettingsManager>();
|
||||
serviceCollection.AddSingleton<LinkedModelHandler>();
|
||||
serviceCollection.AddSingleton<ParameterUpdater>();
|
||||
|
||||
// receive operation and dependencies
|
||||
serviceCollection.AddScoped<IHostObjectBuilder, RevitHostObjectBuilder>();
|
||||
serviceCollection.AddScoped<ITransactionManager, TransactionManager>();
|
||||
serviceCollection.AddScoped<RevitFamilyBaker>();
|
||||
serviceCollection.AddScoped<FamilyGeometryBaker>();
|
||||
serviceCollection.AddScoped<RevitGroupBaker>();
|
||||
serviceCollection.AddScoped<RevitMaterialBaker>();
|
||||
serviceCollection.AddScoped<RevitViewBaker>();
|
||||
serviceCollection.AddScoped<RevitViewManager>();
|
||||
serviceCollection.AddScoped<DirectShapeUnpackStrategy>();
|
||||
serviceCollection.AddScoped<FamilyUnpackStrategy>();
|
||||
serviceCollection.AddScoped<RevitPreBakeSetupService>();
|
||||
serviceCollection.AddSingleton<RevitUtils>();
|
||||
serviceCollection.AddSingleton<FamilyCategoryUtils>();
|
||||
serviceCollection.AddSingleton<FamilyTransformUtils>();
|
||||
serviceCollection.AddSingleton<IFailuresPreprocessor, HideWarningsFailuresPreprocessor>();
|
||||
serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc());
|
||||
serviceCollection.AddScoped<LocalToGlobalConverterUtils>();
|
||||
|
||||
@@ -9,8 +9,19 @@ namespace Speckle.Connectors.Revit.HostApp;
|
||||
/// </summary>
|
||||
public class ElementUnpacker
|
||||
{
|
||||
private static readonly List<BuiltInCategory> s_skippedCategories =
|
||||
[
|
||||
BuiltInCategory.OST_SketchLines,
|
||||
BuiltInCategory.OST_MassForm,
|
||||
BuiltInCategory.OST_StairsSketchBoundaryLines,
|
||||
BuiltInCategory.OST_StairsSketchLandingCenterLines,
|
||||
BuiltInCategory.OST_StairsSketchRiserLines,
|
||||
BuiltInCategory.OST_RebarSketchLines,
|
||||
BuiltInCategory.OST_StairsSketchRunLines
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks a random set of revit objects into atomic objects. It currently unpacks groups recurisvely, nested families into atomic family instances.
|
||||
/// Unpacks a random set of revit objects into atomic objects. It currently unpacks groups recursively, nested families into atomic family instances.
|
||||
/// This method will also "pack" curtain walls if necessary (ie, if mullions or panels are selected without their parent curtain wall, they are sent independently; if the parent curtain wall is selected, they will be removed out as the curtain wall will include all its children).
|
||||
/// </summary>
|
||||
/// <param name="selectionElements"></param>
|
||||
@@ -32,7 +43,7 @@ public class ElementUnpacker
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks input element ids into their subelements, eg groups and nested family instances
|
||||
/// Unpacks input element ids into their sub-elements, eg groups and nested family instances
|
||||
/// </summary>
|
||||
/// <param name="objectIds"></param>
|
||||
/// <returns></returns>
|
||||
@@ -57,11 +68,21 @@ public class ElementUnpacker
|
||||
// UNPACK: Groups
|
||||
if (element is Group g)
|
||||
{
|
||||
// When a group is from a linked model, GetMemberIds may behave differently
|
||||
// We add null checks to handle cases where elements can't be properly resolved
|
||||
// POC: this might screw up generating hosting rel generation here, because nested families in groups get flattened out by GetMemberIds().
|
||||
var groupElements = g.GetMemberIds().Select(doc.GetElement).Where(el => el != null);
|
||||
unpackedElements.AddRange(UnpackElements(groupElements, doc));
|
||||
var memberIds = g.GetMemberIds();
|
||||
|
||||
if (memberIds.Count <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// using a collector more efficient
|
||||
using var collector = new FilteredElementCollector(doc, memberIds);
|
||||
collector.WhereElementIsNotElementType(); // exclude "Type" elements (FamilySymbols)
|
||||
var filter = new ElementMulticategoryFilter(s_skippedCategories, inverted: true); // exclude "Sketch/Form" categories
|
||||
collector.WherePasses(filter);
|
||||
|
||||
// recursively unpack the valid results
|
||||
unpackedElements.AddRange(UnpackElements(collector, doc));
|
||||
}
|
||||
else if (element is BaseArray baseArray)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.GraphTraversal;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
public class FamilyCategoryUtils
|
||||
{
|
||||
private readonly CategoryExtractor _categoryExtractor;
|
||||
private readonly ILogger<FamilyCategoryUtils> _logger;
|
||||
|
||||
public FamilyCategoryUtils(CategoryExtractor categoryExtractor, ILogger<FamilyCategoryUtils> logger)
|
||||
{
|
||||
_categoryExtractor = categoryExtractor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string? ExtractCategoryForDefinition(
|
||||
InstanceDefinitionProxy definition,
|
||||
ICollection<(Collection[] collectionPath, IInstanceComponent component)> instanceComponents,
|
||||
IReadOnlyDictionary<string, TraversalContext> speckleObjectLookup
|
||||
)
|
||||
{
|
||||
var definitionId = definition.applicationId ?? definition.id;
|
||||
|
||||
var firstInstance = instanceComponents
|
||||
.Select(x => x.component)
|
||||
.OfType<InstanceProxy>()
|
||||
.FirstOrDefault(i => i.definitionId == definitionId);
|
||||
|
||||
if (firstInstance == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var instanceObjectId = firstInstance.applicationId ?? firstInstance.id;
|
||||
if (instanceObjectId != null && speckleObjectLookup.TryGetValue(instanceObjectId, out var tc))
|
||||
{
|
||||
var parentDataObject = tc.Parent?.Current as DataObject;
|
||||
return _categoryExtractor.ExtractBuiltInCategory(parentDataObject, tc.Current);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetFamilyCategory(Document familyDoc, string? builtInCategoryString)
|
||||
{
|
||||
if (!familyDoc.IsFamilyDocument || string.IsNullOrEmpty(builtInCategoryString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Enum.TryParse(builtInCategoryString, out BuiltInCategory bic))
|
||||
{
|
||||
try
|
||||
{
|
||||
Category targetCategory = familyDoc.Settings.Categories.get_Item(bic);
|
||||
if (targetCategory != null)
|
||||
{
|
||||
familyDoc.OwnerFamily.FamilyCategory = targetCategory;
|
||||
}
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ArgumentException)
|
||||
{
|
||||
_logger.LogInformation("Category {Category} cannot be assigned to a Family. Falling back to default.", bic);
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid operation when setting category {Category}. Falling back to default.", bic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Objects.Other;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.Extensions;
|
||||
using Speckle.Sdk.Models.GraphTraversal;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
using SMesh = Speckle.Objects.Geometry.Mesh;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the low-level conversion and baking of Speckle geometries into a Revit Family Document.
|
||||
/// Extracted from RevitFamilyBaker to separate geometry generation from high-level orchestration, reducing class coupling.
|
||||
/// </summary>
|
||||
public class FamilyGeometryBaker
|
||||
{
|
||||
private readonly ILogger<FamilyGeometryBaker> _logger;
|
||||
private readonly ITypedConverter<Base, List<GeometryObject>> _geometryConverter;
|
||||
private readonly ITypedConverter<SMesh, GeometryObject> _freeformMeshConverter;
|
||||
|
||||
public FamilyGeometryBaker(
|
||||
ILogger<FamilyGeometryBaker> logger,
|
||||
ITypedConverter<Base, List<GeometryObject>> geometryConverter,
|
||||
ITypedConverter<SMesh, GeometryObject> freeformMeshConverter
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_geometryConverter = geometryConverter;
|
||||
_freeformMeshConverter = freeformMeshConverter;
|
||||
}
|
||||
|
||||
public void BakeFamilyGeometry(
|
||||
Document famDoc,
|
||||
InstanceDefinitionProxy definition,
|
||||
IReadOnlyDictionary<string, TraversalContext> objectLookup,
|
||||
IReadOnlyDictionary<string, RenderMaterial> materialMap,
|
||||
FamilyMaterialManager materialManager,
|
||||
Action<Document, InstanceProxy, FamilyMaterialManager?> placeNestedInstanceAction
|
||||
)
|
||||
{
|
||||
if (definition.objects.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var id in definition.objects)
|
||||
{
|
||||
if (!objectLookup.TryGetValue(id, out var tc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? extractedSubcategoryName = null;
|
||||
var parentTc = tc.Parent;
|
||||
while (parentTc != null)
|
||||
{
|
||||
if (parentTc.Current is Collection col && !string.IsNullOrWhiteSpace(col.name))
|
||||
{
|
||||
extractedSubcategoryName = col.name;
|
||||
break;
|
||||
}
|
||||
parentTc = parentTc.Parent;
|
||||
}
|
||||
|
||||
ProcessObjectForFamily(
|
||||
famDoc,
|
||||
tc.Current,
|
||||
null,
|
||||
definition.name,
|
||||
extractedSubcategoryName,
|
||||
materialManager,
|
||||
materialMap,
|
||||
placeNestedInstanceAction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessObjectForFamily(
|
||||
Document famDoc,
|
||||
Base obj,
|
||||
Category? currentSubcategory,
|
||||
string familyName,
|
||||
string? extractedSubcategoryName,
|
||||
FamilyMaterialManager? materialManager,
|
||||
IReadOnlyDictionary<string, RenderMaterial>? materialMap,
|
||||
Action<Document, InstanceProxy, FamilyMaterialManager?> placeNestedInstanceAction
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
Category? newSubcategory = currentSubcategory;
|
||||
string? subcategoryName = extractedSubcategoryName ?? (obj as Collection)?.name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subcategoryName))
|
||||
{
|
||||
var familyCategory = famDoc.OwnerFamily.FamilyCategory;
|
||||
if (familyCategory != null)
|
||||
{
|
||||
if (familyCategory.SubCategories.Contains(subcategoryName))
|
||||
{
|
||||
newSubcategory = familyCategory.SubCategories.get_Item(subcategoryName);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
newSubcategory = famDoc.Settings.Categories.NewSubcategory(familyCategory, subcategoryName);
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ArgumentException)
|
||||
{
|
||||
_logger.LogWarning("Failed to create Revit subcategory with name: {SubcategoryName}", subcategoryName);
|
||||
newSubcategory = currentSubcategory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (obj is Collection col)
|
||||
{
|
||||
foreach (var element in col.elements)
|
||||
{
|
||||
ProcessObjectForFamily(
|
||||
famDoc,
|
||||
element,
|
||||
newSubcategory,
|
||||
familyName,
|
||||
null,
|
||||
materialManager,
|
||||
materialMap,
|
||||
placeNestedInstanceAction
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (obj is InstanceProxy instanceProxy)
|
||||
{
|
||||
placeNestedInstanceAction(famDoc, instanceProxy, materialManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
BakeGeometry(famDoc, obj, newSubcategory, materialManager, materialMap, placeNestedInstanceAction);
|
||||
}
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ApplicationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Revit API error baking object {ObjectId} into family {Family}", obj.id, familyName);
|
||||
}
|
||||
catch (SpeckleException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Speckle error baking object {ObjectId} into family {Family}", obj.id, familyName);
|
||||
}
|
||||
}
|
||||
|
||||
private void BakeGeometry(
|
||||
Document famDoc,
|
||||
Base obj,
|
||||
Category? subcategory,
|
||||
FamilyMaterialManager? materialManager,
|
||||
IReadOnlyDictionary<string, RenderMaterial>? materialMap,
|
||||
Action<Document, InstanceProxy, FamilyMaterialManager?> placeNestedInstanceAction
|
||||
)
|
||||
{
|
||||
string objectId = obj.applicationId ?? obj.id.NotNull();
|
||||
string? speckleMatId = null;
|
||||
|
||||
if (materialMap != null && materialMap.TryGetValue(objectId, out var mat))
|
||||
{
|
||||
speckleMatId = mat.id;
|
||||
}
|
||||
|
||||
if (obj is SMesh mesh)
|
||||
{
|
||||
BakeMesh(famDoc, mesh, subcategory, speckleMatId, materialManager);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var geometries = _geometryConverter.Convert(obj);
|
||||
|
||||
if (geometries.Count > 0)
|
||||
{
|
||||
var solids = new List<Solid>(geometries.Count);
|
||||
var nonSolids = new List<GeometryObject>(geometries.Count);
|
||||
|
||||
foreach (var geom in geometries)
|
||||
{
|
||||
if (geom is Solid s && !s.Faces.IsEmpty)
|
||||
{
|
||||
solids.Add(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
nonSolids.Add(geom);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var solid in solids)
|
||||
{
|
||||
using var freeFormElement = FreeFormElement.Create(famDoc, solid);
|
||||
if (subcategory != null)
|
||||
{
|
||||
freeFormElement.Subcategory = subcategory;
|
||||
}
|
||||
|
||||
if (
|
||||
materialManager != null
|
||||
&& speckleMatId != null
|
||||
&& materialManager.FamilyParameters.TryGetValue(speckleMatId, out var famParam)
|
||||
)
|
||||
{
|
||||
Parameter ffeMatParam = freeFormElement.get_Parameter(BuiltInParameter.MATERIAL_ID_PARAM);
|
||||
if (ffeMatParam != null && famDoc.FamilyManager.CanElementParameterBeAssociated(ffeMatParam))
|
||||
{
|
||||
famDoc.FamilyManager.AssociateElementParameterToFamilyParameter(ffeMatParam, famParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nonSolids.Count > 0)
|
||||
{
|
||||
// try to use the Family's actual Category, otherwise default to Generic Model
|
||||
ElementId categoryId =
|
||||
famDoc.OwnerFamily.FamilyCategory?.Id ?? new ElementId(BuiltInCategory.OST_GenericModel);
|
||||
|
||||
if (!DirectShape.IsValidCategoryId(categoryId, famDoc))
|
||||
{
|
||||
categoryId = new ElementId(BuiltInCategory.OST_GenericModel);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var ds = DirectShape.CreateElement(famDoc, categoryId);
|
||||
ds.SetShape(nonSolids);
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ArgumentException ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"DirectShape rejected Category ID {CategoryId}, falling back to Generic Model",
|
||||
categoryId
|
||||
);
|
||||
|
||||
using var fallbackDs = DirectShape.CreateElement(famDoc, new ElementId(BuiltInCategory.OST_GenericModel));
|
||||
fallbackDs.SetShape(nonSolids);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (SpeckleException) { }
|
||||
|
||||
var displayValues = obj.TryGetDisplayValue();
|
||||
if (displayValues != null)
|
||||
{
|
||||
foreach (var item in displayValues)
|
||||
{
|
||||
BakeGeometry(famDoc, item, subcategory, materialManager, materialMap, placeNestedInstanceAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BakeMesh(
|
||||
Document famDoc,
|
||||
SMesh mesh,
|
||||
Category? subcategory,
|
||||
string? speckleMatId,
|
||||
FamilyMaterialManager? materialManager
|
||||
)
|
||||
{
|
||||
GeometryObject geomObject;
|
||||
try
|
||||
{
|
||||
geomObject = _freeformMeshConverter.Convert(mesh);
|
||||
}
|
||||
catch (SpeckleException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to convert Speckle Mesh into Revit freeform geometry.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (geomObject is Solid solid)
|
||||
{
|
||||
using var freeFormElement = FreeFormElement.Create(famDoc, solid);
|
||||
if (subcategory != null)
|
||||
{
|
||||
freeFormElement.Subcategory = subcategory;
|
||||
}
|
||||
|
||||
if (
|
||||
materialManager != null
|
||||
&& speckleMatId != null
|
||||
&& materialManager.FamilyParameters.TryGetValue(speckleMatId, out var famParam)
|
||||
)
|
||||
{
|
||||
Parameter ffeMatParam = freeFormElement.get_Parameter(BuiltInParameter.MATERIAL_ID_PARAM);
|
||||
if (ffeMatParam != null && famDoc.FamilyManager.CanElementParameterBeAssociated(ffeMatParam))
|
||||
{
|
||||
famDoc.FamilyManager.AssociateElementParameterToFamilyParameter(ffeMatParam, famParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (geomObject is Mesh revitMesh)
|
||||
{
|
||||
// try to use the Family's actual Category, otherwise default to Generic Model
|
||||
ElementId categoryId = famDoc.OwnerFamily.FamilyCategory?.Id ?? new ElementId(BuiltInCategory.OST_GenericModel);
|
||||
|
||||
if (!DirectShape.IsValidCategoryId(categoryId, famDoc))
|
||||
{
|
||||
categoryId = new ElementId(BuiltInCategory.OST_GenericModel);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var ds = DirectShape.CreateElement(famDoc, categoryId);
|
||||
ds.SetShape([revitMesh]);
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ArgumentException ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"DirectShape rejected Category ID {CategoryId}, falling back to Generic Model",
|
||||
categoryId
|
||||
);
|
||||
|
||||
using var fallbackDs = DirectShape.CreateElement(famDoc, new ElementId(BuiltInCategory.OST_GenericModel));
|
||||
fallbackDs.SetShape([revitMesh]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Objects.Other;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models.GraphTraversal;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the resolution and assignment of materials, subcategories, and parameters
|
||||
/// strictly within the context of a temporary Family Document.
|
||||
/// </summary>
|
||||
public class FamilyMaterialManager
|
||||
{
|
||||
private readonly RevitMaterialBaker _materialBaker;
|
||||
private readonly ILogger _logger;
|
||||
private static readonly char[] s_invalidRevitChars =
|
||||
[
|
||||
'\\',
|
||||
':',
|
||||
'{',
|
||||
'}',
|
||||
'[',
|
||||
']',
|
||||
'|',
|
||||
';',
|
||||
'<',
|
||||
'>',
|
||||
'?',
|
||||
'`',
|
||||
'~'
|
||||
];
|
||||
|
||||
public Dictionary<string, FamilyParameter> FamilyParameters { get; } = [];
|
||||
public Dictionary<string, ElementId> SubCategories { get; } = [];
|
||||
private Dictionary<string, ElementId> BakedMaterials { get; } = [];
|
||||
|
||||
public FamilyMaterialManager(RevitMaterialBaker materialBaker, ILogger logger)
|
||||
{
|
||||
_materialBaker = materialBaker;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes string to ensure it is valid for Revit Parameters and SubCategories.
|
||||
/// </summary>
|
||||
public static string GetSafeName(string rawName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawName))
|
||||
{
|
||||
return "Unnamed";
|
||||
}
|
||||
|
||||
char[] buffer = rawName.ToCharArray();
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (Array.IndexOf(s_invalidRevitChars, buffer[i]) >= 0)
|
||||
{
|
||||
buffer[i] = '_';
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? new string(buffer) : rawName;
|
||||
}
|
||||
|
||||
public void SetupFamilyMaterials(
|
||||
Document famDoc,
|
||||
InstanceDefinitionProxy definition,
|
||||
IReadOnlyDictionary<string, TraversalContext> objectLookup,
|
||||
IReadOnlyDictionary<string, RenderMaterial> materialMap
|
||||
)
|
||||
{
|
||||
Category baseCategory = famDoc.OwnerFamily.FamilyCategory;
|
||||
|
||||
foreach (var id in definition.objects)
|
||||
{
|
||||
if (!objectLookup.TryGetValue(id, out var tc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var obj = tc.Current;
|
||||
string objectId = obj.applicationId ?? obj.id.NotNull();
|
||||
|
||||
if (materialMap.TryGetValue(objectId, out var renderMat))
|
||||
{
|
||||
if (BakedMaterials.ContainsKey(renderMat.id.NotNullOrWhiteSpace()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Bake the material locally
|
||||
ElementId famMatId = _materialBaker.BakeMaterial(renderMat, famDoc);
|
||||
BakedMaterials[renderMat.id] = famMatId;
|
||||
|
||||
// 2. Setup Subcategory (for DirectShapes)
|
||||
string rawName = string.IsNullOrWhiteSpace(renderMat.name) ? renderMat.id : renderMat.name;
|
||||
string safeName = GetSafeName(rawName);
|
||||
|
||||
string subCatName = $"Mat_{safeName}";
|
||||
subCatName = subCatName.Length > 50 ? subCatName[..50] : subCatName;
|
||||
|
||||
if (baseCategory != null)
|
||||
{
|
||||
if (!baseCategory.SubCategories.Contains(subCatName))
|
||||
{
|
||||
Category subCat = famDoc.Settings.Categories.NewSubcategory(baseCategory, subCatName);
|
||||
subCat.Material = famDoc.GetElement(famMatId) as Material;
|
||||
SubCategories[renderMat.id] = subCat.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
SubCategories[renderMat.id] = baseCategory.SubCategories.get_Item(subCatName).Id;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Setup Family Parameter (for FreeFormElements)
|
||||
string paramName = $"Material_{safeName}";
|
||||
FamilyParameter? existingParam = famDoc.FamilyManager.get_Parameter(paramName);
|
||||
if (existingParam == null)
|
||||
{
|
||||
FamilyParameter famParam = famDoc.FamilyManager.AddParameter(
|
||||
paramName,
|
||||
GroupTypeId.Materials,
|
||||
SpecTypeId.Reference.Material,
|
||||
false
|
||||
);
|
||||
FamilyParameters[renderMat.id] = famParam;
|
||||
}
|
||||
else
|
||||
{
|
||||
FamilyParameters[renderMat.id] = existingParam;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to setup family material {MatName}", renderMat.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AssignProjectMaterialsToFamily(
|
||||
Document document,
|
||||
FamilySymbol symbol,
|
||||
IReadOnlyDictionary<string, ElementId> originalNameToProjectMatId
|
||||
)
|
||||
{
|
||||
Category? baseCategory = document.Settings.Categories.get_Item(BuiltInCategory.OST_GenericModel);
|
||||
|
||||
// Create a local map with sanitized keys so it perfectly matches the safeNames applied in the Family
|
||||
var sanitizedMatMap = new Dictionary<string, ElementId>();
|
||||
foreach (var kvp in originalNameToProjectMatId)
|
||||
{
|
||||
sanitizedMatMap[GetSafeName(kvp.Key)] = kvp.Value;
|
||||
}
|
||||
|
||||
foreach (Parameter p in symbol.Parameters)
|
||||
{
|
||||
if (p.Definition.Name.StartsWith("Material_") && p.StorageType == StorageType.ElementId)
|
||||
{
|
||||
string safeName = p.Definition.Name["Material_".Length..];
|
||||
|
||||
if (sanitizedMatMap.TryGetValue(safeName, out var projMatId) && !p.IsReadOnly)
|
||||
{
|
||||
p.Set(projMatId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (baseCategory != null)
|
||||
{
|
||||
foreach (var kvp in sanitizedMatMap)
|
||||
{
|
||||
string safeName = kvp.Key;
|
||||
ElementId projMatId = kvp.Value;
|
||||
|
||||
string subCatName = $"Mat_{safeName}";
|
||||
subCatName = subCatName.Length > 50 ? subCatName[..50] : subCatName;
|
||||
|
||||
if (baseCategory.SubCategories.Contains(subCatName))
|
||||
{
|
||||
Category projSubCat = baseCategory.SubCategories.get_Item(subCatName);
|
||||
if (projSubCat != null && document.GetElement(projMatId) is Material projMat)
|
||||
{
|
||||
projSubCat.Material = projMat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.DoubleNumerics;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
/// <summary>
|
||||
/// Pure math and transform utilities for Revit Family Instance placement.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Architecturally, these methods live in the Connector rather than the Converters project
|
||||
/// because stripping scale/skew and applying API mirroring are Host App-specific workarounds
|
||||
/// for Revit's shitty Family Instance rules, not general stateless conversion logic.
|
||||
/// </remarks>
|
||||
public class FamilyTransformUtils
|
||||
{
|
||||
private readonly ILogger<FamilyTransformUtils> _logger;
|
||||
|
||||
public FamilyTransformUtils(ILogger<FamilyTransformUtils> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a transform matrix contains non-uniform scaling or shear/skew.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Revit family instances natively reject matrices with scale or skew.
|
||||
/// We flag these matrices so we can sanitize them before placement.
|
||||
/// </remarks>
|
||||
public bool HasScaleOrSkew(Matrix4x4 matrix)
|
||||
{
|
||||
// Extract lengths of basis vectors
|
||||
var lenX = Math.Sqrt(matrix.M11 * matrix.M11 + matrix.M21 * matrix.M21 + matrix.M31 * matrix.M31);
|
||||
var lenY = Math.Sqrt(matrix.M12 * matrix.M12 + matrix.M22 * matrix.M22 + matrix.M32 * matrix.M32);
|
||||
var lenZ = Math.Sqrt(matrix.M13 * matrix.M13 + matrix.M23 * matrix.M23 + matrix.M33 * matrix.M33);
|
||||
|
||||
// Calculate dot products to check for orthogonality
|
||||
var dotXy = matrix.M11 * matrix.M12 + matrix.M21 * matrix.M22 + matrix.M31 * matrix.M32;
|
||||
var dotXz = matrix.M11 * matrix.M13 + matrix.M21 * matrix.M23 + matrix.M31 * matrix.M33;
|
||||
var dotYz = matrix.M12 * matrix.M13 + matrix.M22 * matrix.M23 + matrix.M32 * matrix.M33;
|
||||
|
||||
double tol = 1e-4;
|
||||
|
||||
bool isOrthogonal = Math.Abs(dotXy) < tol && Math.Abs(dotXz) < tol && Math.Abs(dotYz) < tol;
|
||||
bool isUnitScale = Math.Abs(lenX - 1.0) < tol && Math.Abs(lenY - 1.0) < tol && Math.Abs(lenZ - 1.0) < tol;
|
||||
|
||||
return !isOrthogonal || !isUnitScale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a transform matrix by stripping out any scale or skew, returning a rigid transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 RemoveScaleAndSkew(Matrix4x4 matrix)
|
||||
{
|
||||
// 1. Extract Z column and normalize
|
||||
double zX = matrix.M13,
|
||||
zY = matrix.M23,
|
||||
zZ = matrix.M33;
|
||||
double lenZ = Math.Sqrt(zX * zX + zY * zY + zZ * zZ);
|
||||
if (lenZ > 1e-6)
|
||||
{
|
||||
zX /= lenZ;
|
||||
zY /= lenZ;
|
||||
zZ /= lenZ;
|
||||
}
|
||||
else
|
||||
{
|
||||
zX = 0;
|
||||
zY = 0;
|
||||
zZ = 1;
|
||||
}
|
||||
|
||||
// 2. Extract Y column
|
||||
double yX = matrix.M12,
|
||||
yY = matrix.M22,
|
||||
yZ = matrix.M32;
|
||||
|
||||
// 3. Cross product Y and Z to get orthogonal X
|
||||
double xX = yY * zZ - yZ * zY;
|
||||
double xY = yZ * zX - yX * zZ;
|
||||
double xZ = yX * zY - yY * zX;
|
||||
double lenX = Math.Sqrt(xX * xX + xY * xY + xZ * xZ);
|
||||
if (lenX > 1e-6)
|
||||
{
|
||||
xX /= lenX;
|
||||
xY /= lenX;
|
||||
xZ /= lenX;
|
||||
}
|
||||
else
|
||||
{
|
||||
xX = 1;
|
||||
xY = 0;
|
||||
xZ = 0;
|
||||
}
|
||||
|
||||
// 4. Cross product Z and X to get orthogonal unit Y
|
||||
yX = zY * xZ - zZ * xY;
|
||||
yY = zZ * xX - zX * xZ;
|
||||
yZ = zX * xY - zY * xX;
|
||||
double lenY = Math.Sqrt(yX * yX + yY * yY + yZ * yZ);
|
||||
if (lenY > 1e-6)
|
||||
{
|
||||
yX /= lenY;
|
||||
yY /= lenY;
|
||||
yZ /= lenY;
|
||||
}
|
||||
|
||||
return new Matrix4x4(
|
||||
xX,
|
||||
yX,
|
||||
zX,
|
||||
matrix.M14,
|
||||
xY,
|
||||
yY,
|
||||
zY,
|
||||
matrix.M24,
|
||||
xZ,
|
||||
yZ,
|
||||
zZ,
|
||||
matrix.M34,
|
||||
matrix.M41,
|
||||
matrix.M42,
|
||||
matrix.M43,
|
||||
matrix.M44
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the determinant of a matrix to check if it encodes a mirrored state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A negative determinant implies a left-handed coordinate system (mirroring).
|
||||
/// We extract this so we can apply the mirror via Revit's native API instead.
|
||||
/// </remarks>
|
||||
public (bool X, bool Y, bool Z) GetMirrorState(Matrix4x4 matrix)
|
||||
{
|
||||
var det =
|
||||
matrix.M11 * (matrix.M22 * matrix.M33 - matrix.M23 * matrix.M32)
|
||||
- matrix.M12 * (matrix.M21 * matrix.M33 - matrix.M23 * matrix.M31)
|
||||
+ matrix.M13 * (matrix.M21 * matrix.M32 - matrix.M22 * matrix.M31);
|
||||
|
||||
return det < 0 ? (true, false, false) : (false, false, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies native Revit mirror operations to an element based on the evaluated mirror state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Because we strip the mirrored (left-handed) state from the initial transform to keep Revit happy,
|
||||
/// we must restore the mirrored geometry as a post-placement operation.
|
||||
/// </remarks>
|
||||
public void ApplyMirroring(
|
||||
Document document,
|
||||
ElementId elementId,
|
||||
Autodesk.Revit.DB.Plane plane,
|
||||
(bool X, bool Y, bool Z) mirrorState
|
||||
)
|
||||
{
|
||||
var mirrorOperations = new List<(string name, bool shouldMirror, Autodesk.Revit.DB.Plane mirrorPlane)>
|
||||
{
|
||||
("YZ", mirrorState.X, Autodesk.Revit.DB.Plane.CreateByOriginAndBasis(plane.Origin, plane.YVec, plane.Normal)),
|
||||
("XZ", mirrorState.Y, Autodesk.Revit.DB.Plane.CreateByOriginAndBasis(plane.Origin, plane.XVec, plane.Normal)),
|
||||
("XY", mirrorState.Z, Autodesk.Revit.DB.Plane.CreateByOriginAndBasis(plane.Origin, plane.XVec, plane.YVec))
|
||||
};
|
||||
|
||||
foreach (var (name, _, mirrorPlane) in mirrorOperations.Where(op => op.shouldMirror))
|
||||
{
|
||||
try
|
||||
{
|
||||
document.Regenerate();
|
||||
ElementTransformUtils.MirrorElements(document, [elementId], mirrorPlane, false);
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ApplicationException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Failed to mirror element on {PlaneName} plane", name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mirrorPlane.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
public class ParameterChangeRequest
|
||||
{
|
||||
public required string ApplicationId { get; init; }
|
||||
public required string Path { get; init; }
|
||||
public object? To { get; init; }
|
||||
public string? InternalDefinitionName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Converters.RevitShared.Services;
|
||||
using Speckle.Sdk;
|
||||
using DB = Autodesk.Revit.DB;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
/// <summary>
|
||||
/// Updates parameter values on Revit elements. Mirrors the structure from ParameterExtractor.
|
||||
/// Path format: ["Instance Parameters" | "Type Parameters" | "System Type Parameters", "GroupName", "ParameterName"]
|
||||
/// </summary>
|
||||
public class ParameterUpdater
|
||||
{
|
||||
private readonly RevitContext _revitContext;
|
||||
private readonly ScalingServiceToHost _scalingServiceToHost;
|
||||
private readonly ILogger<ParameterUpdater> _logger;
|
||||
|
||||
public ParameterUpdater(
|
||||
RevitContext revitContext,
|
||||
ScalingServiceToHost scalingServiceToHost,
|
||||
ILogger<ParameterUpdater> logger
|
||||
)
|
||||
{
|
||||
_revitContext = revitContext;
|
||||
_scalingServiceToHost = scalingServiceToHost;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public UpdateResult Update(DB.Element element, string[] path, object? newValue, string? internalDefinitionName = null)
|
||||
{
|
||||
// path = ["Instance Parameters", "Identity Data", "Mark"]
|
||||
if (path.Length != 3)
|
||||
{
|
||||
return UpdateResult.Fail(
|
||||
$"Path must have exactly 3 segments: [scope, group, parameter]. Got: {string.Join(" → ", path)}"
|
||||
);
|
||||
}
|
||||
|
||||
var parameterScope = path[0]; // "Instance Parameters" | "Type Parameters" | "System Type Parameters"
|
||||
var groupName = path[1]; // "Identity Data", "Dimensions", etc.
|
||||
var parameterKey = path[2]; // human-readable name (or internalDefinitionName if collision)
|
||||
|
||||
// get target element based on scope
|
||||
var targetElement = GetTargetElement(element, parameterScope);
|
||||
if (targetElement == null)
|
||||
{
|
||||
return UpdateResult.Fail($"Could not resolve target for scope: {parameterScope}");
|
||||
}
|
||||
|
||||
// find the parameter (now using the robust lookup)
|
||||
var parameter = FindParameter(targetElement, groupName, parameterKey, internalDefinitionName);
|
||||
if (parameter == null)
|
||||
{
|
||||
return UpdateResult.Fail($"Parameter not found: {parameterKey} in group {groupName}");
|
||||
}
|
||||
|
||||
if (parameter.IsReadOnly)
|
||||
{
|
||||
return UpdateResult.Fail($"Parameter '{parameterKey}' is readonly in Revit");
|
||||
}
|
||||
|
||||
return SetParameterValue(parameter, newValue);
|
||||
}
|
||||
|
||||
private DB.Element? GetTargetElement(DB.Element element, string scope) =>
|
||||
scope switch
|
||||
{
|
||||
"Instance Parameters" => element,
|
||||
"Type Parameters" => GetTypeElement(element),
|
||||
"System Type Parameters" => GetSystemTypeElement(element),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private DB.Element? GetTypeElement(DB.Element element)
|
||||
{
|
||||
var typeId = element.GetTypeId();
|
||||
if (typeId == DB.ElementId.InvalidElementId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _revitContext.UIApplication?.ActiveUIDocument.Document.GetElement(typeId);
|
||||
}
|
||||
|
||||
private DB.Element? GetSystemTypeElement(DB.Element element)
|
||||
{
|
||||
var system = GetMEPSystem(element);
|
||||
if (system == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _revitContext.UIApplication?.ActiveUIDocument.Document.GetElement(system.GetTypeId());
|
||||
}
|
||||
|
||||
private DB.MEPSystem? GetMEPSystem(DB.Element element)
|
||||
{
|
||||
if (element is DB.MEPCurve curve)
|
||||
{
|
||||
return curve.MEPSystem;
|
||||
}
|
||||
|
||||
if (element is DB.FamilyInstance fi)
|
||||
{
|
||||
var cm = fi.MEPModel?.ConnectorManager;
|
||||
if (cm != null)
|
||||
{
|
||||
foreach (DB.Connector conn in cm.Connectors)
|
||||
{
|
||||
if (conn.ConnectorType == DB.ConnectorType.Physical && conn.IsConnected && conn.MEPSystem != null)
|
||||
{
|
||||
return conn.MEPSystem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DB.Parameter? FindParameter(
|
||||
DB.Element element,
|
||||
string groupName,
|
||||
string parameterKey,
|
||||
string? internalDefinitionName
|
||||
)
|
||||
{
|
||||
// fast path: direct lookup using the internal definition name
|
||||
if (!string.IsNullOrEmpty(internalDefinitionName))
|
||||
{
|
||||
// try as BuiltInParameter enum
|
||||
if (Enum.TryParse(internalDefinitionName, out DB.BuiltInParameter bip) && bip != DB.BuiltInParameter.INVALID)
|
||||
{
|
||||
var param = element.get_Parameter(bip);
|
||||
if (param != null)
|
||||
{
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
// try as shared parameter Guid
|
||||
if (Guid.TryParse(internalDefinitionName, out Guid guid))
|
||||
{
|
||||
var param = element.get_Parameter(guid);
|
||||
if (param != null)
|
||||
{
|
||||
return param;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: iteration for project parameters or missing internal names
|
||||
DB.Parameter? fallbackParameter = null;
|
||||
|
||||
foreach (DB.Parameter parameter in element.Parameters)
|
||||
{
|
||||
var definition = parameter.Definition;
|
||||
if (definition == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentInternalName = GetInternalDefinitionName(parameter);
|
||||
var humanName = definition.Name;
|
||||
|
||||
// exact internal name match (covers project params that aren't BuiltIn/Shared)
|
||||
if (!string.IsNullOrEmpty(internalDefinitionName) && currentInternalName == internalDefinitionName)
|
||||
{
|
||||
return parameter;
|
||||
}
|
||||
|
||||
// fallback human-readable name matching
|
||||
if (humanName == parameterKey || currentInternalName == parameterKey)
|
||||
{
|
||||
var paramGroup = definition.GetGroupTypeId();
|
||||
var groupLabel = DB.LabelUtils.GetLabelForGroup(paramGroup);
|
||||
|
||||
if (groupLabel == groupName)
|
||||
{
|
||||
return parameter;
|
||||
}
|
||||
fallbackParameter ??= parameter;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackParameter;
|
||||
}
|
||||
|
||||
private string GetInternalDefinitionName(DB.Parameter parameter)
|
||||
{
|
||||
if (parameter.Definition is DB.InternalDefinition internalDef)
|
||||
{
|
||||
var bip = internalDef.BuiltInParameter;
|
||||
if (bip != DB.BuiltInParameter.INVALID)
|
||||
{
|
||||
return bip.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return parameter.Definition.Name;
|
||||
}
|
||||
|
||||
private UpdateResult SetParameterValue(DB.Parameter parameter, object? newValue)
|
||||
{
|
||||
var paramName = parameter.Definition.Name;
|
||||
if (newValue == null)
|
||||
{
|
||||
if (parameter.StorageType == DB.StorageType.String)
|
||||
{
|
||||
return parameter.Set(string.Empty)
|
||||
? UpdateResult.Success()
|
||||
: UpdateResult.Fail("Failed to clear string parameter");
|
||||
}
|
||||
return UpdateResult.Fail("Cannot set non-string parameter to null");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var success = parameter.StorageType switch
|
||||
{
|
||||
DB.StorageType.String => parameter.Set(newValue.ToString()),
|
||||
DB.StorageType.Integer => SetIntegerValue(parameter, newValue),
|
||||
DB.StorageType.Double => SetDoubleValue(parameter, newValue),
|
||||
DB.StorageType.ElementId => SetElementIdValue(parameter, newValue),
|
||||
_ => false
|
||||
};
|
||||
|
||||
return success ? UpdateResult.Success() : UpdateResult.Fail($"Failed to set parameter value to: {newValue}");
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to set parameter value");
|
||||
return UpdateResult.Fail($"Exception for '{paramName}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool SetIntegerValue(DB.Parameter parameter, object newValue)
|
||||
{
|
||||
if (newValue is int i)
|
||||
{
|
||||
return parameter.Set(i);
|
||||
}
|
||||
|
||||
if (newValue is bool b)
|
||||
{
|
||||
return parameter.Set(b ? 1 : 0);
|
||||
}
|
||||
|
||||
if (int.TryParse(newValue.ToString(), out var parsed))
|
||||
{
|
||||
return parameter.Set(parsed);
|
||||
}
|
||||
|
||||
var strValue = newValue.ToString();
|
||||
if (strValue == "Yes")
|
||||
{
|
||||
return parameter.Set(1);
|
||||
}
|
||||
if (strValue == "No")
|
||||
{
|
||||
return parameter.Set(0);
|
||||
}
|
||||
|
||||
return parameter.SetValueString(strValue);
|
||||
}
|
||||
|
||||
private bool SetDoubleValue(DB.Parameter parameter, object newValue)
|
||||
{
|
||||
double doubleValue;
|
||||
|
||||
if (newValue is double d)
|
||||
{
|
||||
doubleValue = d;
|
||||
}
|
||||
else if (newValue is int intVal)
|
||||
{
|
||||
doubleValue = intVal;
|
||||
}
|
||||
else if (double.TryParse(newValue.ToString(), out var parsed))
|
||||
{
|
||||
doubleValue = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var internalValue = _scalingServiceToHost.ScaleToNative(doubleValue, parameter.GetUnitTypeId());
|
||||
return parameter.Set(internalValue);
|
||||
}
|
||||
|
||||
private bool SetElementIdValue(DB.Parameter parameter, object newValue)
|
||||
{
|
||||
if (newValue is DB.ElementId eid)
|
||||
{
|
||||
return parameter.Set(eid);
|
||||
}
|
||||
|
||||
// TODO: check this fckr later
|
||||
|
||||
// if (newValue is long idInt)
|
||||
// {
|
||||
// #if REVIT2024_OR_GREATER
|
||||
// return parameter.Set(new DB.ElementId(idInt));
|
||||
// #else
|
||||
// return parameter.Set(new DB.ElementId((long)idInt));
|
||||
// #endif
|
||||
// }
|
||||
//
|
||||
// if (long.TryParse(newValue.ToString(), out var parsedId))
|
||||
// {
|
||||
// #if REVIT2024_OR_GREATER
|
||||
// return parameter.Set(new DB.ElementId(parsedId));
|
||||
// #else
|
||||
// return parameter.Set(new DB.ElementId((long)parsedId));
|
||||
// #endif
|
||||
// }
|
||||
|
||||
var elementName = newValue.ToString();
|
||||
if (elementName != null)
|
||||
{
|
||||
var foundElement = FindElementByName(elementName);
|
||||
if (foundElement != null)
|
||||
{
|
||||
return parameter.Set(foundElement.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private DB.Element? FindElementByName(string name)
|
||||
{
|
||||
var doc = _revitContext.UIApplication?.ActiveUIDocument.Document;
|
||||
|
||||
using var materialCollector = new DB.FilteredElementCollector(doc);
|
||||
var material = materialCollector.OfClass(typeof(DB.Material)).FirstOrDefault(e => e.Name == name);
|
||||
if (material != null)
|
||||
{
|
||||
return material;
|
||||
}
|
||||
|
||||
using var levelCollector = new DB.FilteredElementCollector(doc);
|
||||
var level = levelCollector.OfClass(typeof(DB.Level)).FirstOrDefault(e => e.Name == name);
|
||||
if (level != null)
|
||||
{
|
||||
return level;
|
||||
}
|
||||
using var phaseCollector = new DB.FilteredElementCollector(doc);
|
||||
var phase = phaseCollector.OfClass(typeof(DB.Phase)).FirstOrDefault(e => e.Name == name);
|
||||
if (phase != null)
|
||||
{
|
||||
return phase;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we will see, extract this guy out
|
||||
public readonly struct UpdateResult
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
private UpdateResult(bool success, string? error)
|
||||
{
|
||||
IsSuccess = success;
|
||||
ErrorMessage = error;
|
||||
}
|
||||
|
||||
public static UpdateResult Success() => new(true, null);
|
||||
|
||||
public static UpdateResult Fail(string message) => new(false, message);
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
using System.IO;
|
||||
using Autodesk.Revit.Creation;
|
||||
using Autodesk.Revit.DB;
|
||||
using Autodesk.Revit.DB.Structure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connectors.Common.Conversion;
|
||||
using Speckle.Connectors.Common.Operations;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Objects.Other;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Common.Exceptions;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.GraphTraversal;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
using DB = Autodesk.Revit.DB;
|
||||
using Document = Autodesk.Revit.DB.Document;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
public sealed class RevitFamilyBaker : IDisposable
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly RevitToHostCacheSingleton _cache;
|
||||
private readonly ILogger<RevitFamilyBaker> _logger;
|
||||
private readonly ITypedConverter<(Matrix4x4 matrix, string units), DB.Transform> _transformConverter;
|
||||
private readonly RevitMaterialBaker _materialBaker;
|
||||
private readonly FamilyGeometryBaker _familyGeometryBaker;
|
||||
private readonly FamilyCategoryUtils _familyCategoryUtils;
|
||||
private readonly FamilyTransformUtils _familyTransformUtils;
|
||||
|
||||
private string? _cachedTemplatePath;
|
||||
private readonly Dictionary<string, string> _bakedFamilyPaths = [];
|
||||
|
||||
private readonly string _tempDirectory;
|
||||
private static readonly char[] s_invalidChars = Path.GetInvalidFileNameChars();
|
||||
|
||||
public RevitFamilyBaker(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
RevitToHostCacheSingleton cache,
|
||||
ILogger<RevitFamilyBaker> logger,
|
||||
ITypedConverter<(Matrix4x4 matrix, string units), DB.Transform> transformConverter,
|
||||
RevitMaterialBaker materialBaker,
|
||||
FamilyGeometryBaker familyGeometryBaker,
|
||||
FamilyCategoryUtils familyCategoryUtils,
|
||||
FamilyTransformUtils familyTransformUtils
|
||||
)
|
||||
{
|
||||
_converterSettings = converterSettings;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_transformConverter = transformConverter;
|
||||
_materialBaker = materialBaker;
|
||||
_familyGeometryBaker = familyGeometryBaker;
|
||||
_familyCategoryUtils = familyCategoryUtils;
|
||||
_familyTransformUtils = familyTransformUtils;
|
||||
_tempDirectory = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString("N")[..8]}");
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
|
||||
public (List<ReceiveConversionResult> results, List<string> createdElementIds) BakeInstances(
|
||||
ICollection<(Collection[] collectionPath, IInstanceComponent component)> instanceComponents,
|
||||
IReadOnlyDictionary<string, TraversalContext> speckleObjectLookup,
|
||||
IReadOnlyCollection<RenderMaterialProxy> materialProxies,
|
||||
IProgress<CardProgress> onOperationProgressed
|
||||
)
|
||||
{
|
||||
var document = _converterSettings.Current.Document;
|
||||
var results = new List<ReceiveConversionResult>();
|
||||
var createdElementIds = new List<string>();
|
||||
|
||||
var (objectToMaterialMap, safeNameToProjectMatId) = BuildMaterialMaps(materialProxies);
|
||||
var consumedIds = BuildConsumedIdsSet(instanceComponents, speckleObjectLookup);
|
||||
var sortedComponents = SortComponentsForBaking(instanceComponents);
|
||||
|
||||
var count = 0;
|
||||
foreach (var (_, component) in sortedComponents)
|
||||
{
|
||||
onOperationProgressed.Report(new("Creating families", (double)++count / sortedComponents.Count));
|
||||
|
||||
try
|
||||
{
|
||||
if (component is InstanceDefinitionProxy definitionProxy)
|
||||
{
|
||||
var categoryString = _familyCategoryUtils.ExtractCategoryForDefinition(
|
||||
definitionProxy,
|
||||
instanceComponents,
|
||||
speckleObjectLookup
|
||||
);
|
||||
var result = CreateFamilyFromDefinition(
|
||||
document,
|
||||
definitionProxy,
|
||||
speckleObjectLookup,
|
||||
objectToMaterialMap,
|
||||
safeNameToProjectMatId,
|
||||
categoryString
|
||||
);
|
||||
|
||||
if (result.HasValue)
|
||||
{
|
||||
results.Add(
|
||||
new ReceiveConversionResult(Status.SUCCESS, definitionProxy, result.Value.family.Id.ToString(), "Family")
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (component is InstanceProxy instanceProxy)
|
||||
{
|
||||
bool isConsumed =
|
||||
(instanceProxy.id != null && consumedIds.Contains(instanceProxy.id))
|
||||
|| (instanceProxy.applicationId != null && consumedIds.Contains(instanceProxy.applicationId));
|
||||
|
||||
if (isConsumed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var instance = PlaceFamilyInstance(document, instanceProxy);
|
||||
|
||||
if (instance != null)
|
||||
{
|
||||
createdElementIds.Add(instance.UniqueId);
|
||||
|
||||
if (_familyTransformUtils.HasScaleOrSkew(instanceProxy.transform))
|
||||
{
|
||||
var warningEx = new SpeckleException(
|
||||
"Block instance placed with its original position and rotation, but the unsupported scale/skew was dropped"
|
||||
);
|
||||
results.Add(
|
||||
new ReceiveConversionResult(
|
||||
Status.WARNING,
|
||||
instanceProxy,
|
||||
instance.UniqueId,
|
||||
"FamilyInstance",
|
||||
warningEx
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(
|
||||
new ReceiveConversionResult(Status.SUCCESS, instanceProxy, instance.UniqueId, "FamilyInstance")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
string componentId = component switch
|
||||
{
|
||||
InstanceDefinitionProxy d => d.applicationId ?? d.id.NotNull(),
|
||||
InstanceProxy i => i.applicationId ?? i.id.NotNull(),
|
||||
_ => "unknown"
|
||||
};
|
||||
_logger.LogError(ex, "Failed to process instance component {ComponentId}", componentId);
|
||||
|
||||
if (component is Base b)
|
||||
{
|
||||
results.Add(new ReceiveConversionResult(Status.ERROR, b, null, null, ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (results, createdElementIds);
|
||||
}
|
||||
|
||||
private (
|
||||
Dictionary<string, RenderMaterial> objectToMaterialMap,
|
||||
Dictionary<string, ElementId> safeNameToProjectMatId
|
||||
) BuildMaterialMaps(IReadOnlyCollection<RenderMaterialProxy> materialProxies)
|
||||
{
|
||||
Dictionary<string, RenderMaterial> objectToMaterialMap = new();
|
||||
Dictionary<string, ElementId> safeNameToProjectMatId = new();
|
||||
|
||||
foreach (var proxy in materialProxies)
|
||||
{
|
||||
string matId = proxy.value.id.NotNullOrWhiteSpace();
|
||||
string safeName = string.IsNullOrWhiteSpace(proxy.value.name) ? matId : proxy.value.name;
|
||||
|
||||
foreach (var objId in proxy.objects)
|
||||
{
|
||||
objectToMaterialMap[objId] = proxy.value;
|
||||
}
|
||||
|
||||
if (proxy.objects.Count > 0)
|
||||
{
|
||||
foreach (var objId in proxy.objects)
|
||||
{
|
||||
if (_cache.MaterialsByObjectId.TryGetValue(objId, out var projMatId))
|
||||
{
|
||||
safeNameToProjectMatId[safeName] = projMatId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (objectToMaterialMap, safeNameToProjectMatId);
|
||||
}
|
||||
|
||||
private static HashSet<string> BuildConsumedIdsSet(
|
||||
ICollection<(Collection[] collectionPath, IInstanceComponent component)> instanceComponents,
|
||||
IReadOnlyDictionary<string, TraversalContext> speckleObjectLookup
|
||||
)
|
||||
{
|
||||
var consumedIds = new HashSet<string>();
|
||||
|
||||
foreach (var (_, component) in instanceComponents)
|
||||
{
|
||||
if (component is InstanceDefinitionProxy definition)
|
||||
{
|
||||
foreach (var childId in definition.objects ?? Enumerable.Empty<string>())
|
||||
{
|
||||
consumedIds.Add(childId);
|
||||
if (speckleObjectLookup.TryGetValue(childId, out var childTc))
|
||||
{
|
||||
var childObj = childTc.Current;
|
||||
if (childObj.id != null)
|
||||
{
|
||||
consumedIds.Add(childObj.id);
|
||||
}
|
||||
|
||||
if (childObj.applicationId != null)
|
||||
{
|
||||
consumedIds.Add(childObj.applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return consumedIds;
|
||||
}
|
||||
|
||||
private static List<(Collection[] collectionPath, IInstanceComponent component)> SortComponentsForBaking(
|
||||
ICollection<(Collection[] collectionPath, IInstanceComponent component)> instanceComponents
|
||||
) =>
|
||||
instanceComponents
|
||||
.OrderByDescending(x => x.component.maxDepth)
|
||||
.ThenBy(x => x.component is InstanceDefinitionProxy ? 0 : 1)
|
||||
.ToList();
|
||||
|
||||
private (Family family, FamilySymbol symbol)? CreateFamilyFromDefinition(
|
||||
Document document,
|
||||
InstanceDefinitionProxy definitionProxy,
|
||||
IReadOnlyDictionary<string, TraversalContext> objectLookup,
|
||||
IReadOnlyDictionary<string, RenderMaterial> materialMap,
|
||||
IReadOnlyDictionary<string, ElementId> safeNameToProjectMatId,
|
||||
string? categoryString
|
||||
)
|
||||
{
|
||||
var definitionId = definitionProxy.applicationId ?? definitionProxy.id.NotNull();
|
||||
|
||||
if (_cache.FamiliesByDefinitionId.TryGetValue(definitionId, out var existingFamily))
|
||||
{
|
||||
var existingSymbol = _cache.SymbolsByDefinitionId[definitionId];
|
||||
return (existingFamily, existingSymbol);
|
||||
}
|
||||
|
||||
var familyName = GetFamilyName(definitionProxy);
|
||||
|
||||
bool isNewFamily = false;
|
||||
var family = FindFamilyByName(document, familyName);
|
||||
|
||||
if (family == null)
|
||||
{
|
||||
family = CreateFamily(document, familyName, definitionProxy, objectLookup, materialMap, categoryString);
|
||||
isNewFamily = true;
|
||||
}
|
||||
|
||||
if (family == null)
|
||||
{
|
||||
_logger.LogWarning("Failed to create family for definition {DefinitionId}", definitionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var symbolId = family.GetFamilySymbolIds().FirstOrDefault();
|
||||
if (symbolId == null || symbolId == ElementId.InvalidElementId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (document.GetElement(symbolId) is not FamilySymbol symbol)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!symbol.IsActive)
|
||||
{
|
||||
symbol.Activate();
|
||||
document.Regenerate();
|
||||
}
|
||||
|
||||
if (isNewFamily)
|
||||
{
|
||||
FamilyMaterialManager.AssignProjectMaterialsToFamily(document, symbol, safeNameToProjectMatId);
|
||||
}
|
||||
|
||||
_cache.FamiliesByDefinitionId[definitionId] = family;
|
||||
_cache.SymbolsByDefinitionId[definitionId] = symbol;
|
||||
|
||||
return (family, symbol);
|
||||
}
|
||||
|
||||
private Family? CreateFamily(
|
||||
Document document,
|
||||
string familyName,
|
||||
InstanceDefinitionProxy definition,
|
||||
IReadOnlyDictionary<string, TraversalContext> objectLookup,
|
||||
IReadOnlyDictionary<string, RenderMaterial> materialMap,
|
||||
string? categoryString
|
||||
)
|
||||
{
|
||||
var templatePath = GetFamilyTemplatePath(document);
|
||||
var famDoc = document.Application.NewFamilyDocument(templatePath);
|
||||
var tempPath = Path.Combine(_tempDirectory, $"{familyName}.rfa");
|
||||
|
||||
try
|
||||
{
|
||||
using (var t = new Transaction(famDoc, "Populate Family"))
|
||||
{
|
||||
t.Start();
|
||||
|
||||
var materialManager = new FamilyMaterialManager(_materialBaker, _logger);
|
||||
materialManager.SetupFamilyMaterials(famDoc, definition, objectLookup, materialMap);
|
||||
|
||||
_familyGeometryBaker.BakeFamilyGeometry(
|
||||
famDoc,
|
||||
definition,
|
||||
objectLookup,
|
||||
materialMap,
|
||||
materialManager,
|
||||
PlaceNestedInstance
|
||||
);
|
||||
|
||||
SetFamilyWorkPlaneBased(famDoc, true);
|
||||
_familyCategoryUtils.SetFamilyCategory(famDoc, categoryString);
|
||||
t.Commit();
|
||||
}
|
||||
|
||||
var saveOptions = new SaveAsOptions { OverwriteExistingFile = true };
|
||||
famDoc.SaveAs(tempPath, saveOptions);
|
||||
famDoc.Close(false);
|
||||
|
||||
var definitionId = definition.applicationId ?? definition.id.NotNull();
|
||||
_bakedFamilyPaths[definitionId] = tempPath;
|
||||
|
||||
document.LoadFamily(tempPath, new FamilyLoadOptions(), out var loadedFamily);
|
||||
return loadedFamily;
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ApplicationException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Revit API error creating family {FamilyName}", familyName);
|
||||
famDoc.Close(false);
|
||||
SafeDelete(tempPath);
|
||||
throw;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "IO error creating family {FamilyName}", familyName);
|
||||
famDoc.Close(false);
|
||||
SafeDelete(tempPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlaceNestedInstance(Document famDoc, InstanceProxy instanceProxy, FamilyMaterialManager? materialManager)
|
||||
{
|
||||
var childDefinitionId = instanceProxy.definitionId;
|
||||
|
||||
if (!_bakedFamilyPaths.TryGetValue(childDefinitionId, out var rfaPath) || !File.Exists(rfaPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var familyName = Path.GetFileNameWithoutExtension(rfaPath);
|
||||
Family? childFamily = FindFamilyByName(famDoc, familyName) ?? LoadFamilyWrapper(famDoc, rfaPath);
|
||||
|
||||
using var _ = childFamily;
|
||||
if (childFamily == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var symbolId = childFamily.GetFamilySymbolIds().FirstOrDefault();
|
||||
if (symbolId == null || famDoc.GetElement(symbolId) is not FamilySymbol symbol)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!symbol.IsActive)
|
||||
{
|
||||
symbol.Activate();
|
||||
}
|
||||
|
||||
var instance = CreateAndPlaceFamilyInstance(famDoc, instanceProxy, symbol);
|
||||
|
||||
if (instance != null && materialManager != null)
|
||||
{
|
||||
foreach (Parameter childParam in symbol.Parameters)
|
||||
{
|
||||
if (childParam.Definition.Name.StartsWith("Material_") && childParam.StorageType == StorageType.ElementId)
|
||||
{
|
||||
string paramName = childParam.Definition.Name;
|
||||
|
||||
FamilyParameter? parentFamParam =
|
||||
famDoc.FamilyManager.get_Parameter(paramName)
|
||||
?? famDoc.FamilyManager.AddParameter(
|
||||
paramName,
|
||||
GroupTypeId.Materials,
|
||||
SpecTypeId.Reference.Material,
|
||||
false
|
||||
);
|
||||
|
||||
if (famDoc.FamilyManager.CanElementParameterBeAssociated(childParam))
|
||||
{
|
||||
try
|
||||
{
|
||||
famDoc.FamilyManager.AssociateElementParameterToFamilyParameter(childParam, parentFamParam);
|
||||
}
|
||||
catch (Autodesk.Revit.Exceptions.ArgumentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to associate material parameter {ParamName}", paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Family? LoadFamilyWrapper(Document doc, string path)
|
||||
{
|
||||
doc.LoadFamily(path, new FamilyLoadOptions(), out var family);
|
||||
return family;
|
||||
}
|
||||
|
||||
private FamilyInstance? CreateAndPlaceFamilyInstance(Document doc, InstanceProxy instanceProxy, FamilySymbol symbol)
|
||||
{
|
||||
var isMirrored = _familyTransformUtils.GetMirrorState(instanceProxy.transform).X;
|
||||
var hasScaleOrSkew = _familyTransformUtils.HasScaleOrSkew(instanceProxy.transform);
|
||||
|
||||
var cleanMatrix =
|
||||
(hasScaleOrSkew || isMirrored)
|
||||
? _familyTransformUtils.RemoveScaleAndSkew(instanceProxy.transform)
|
||||
: instanceProxy.transform;
|
||||
|
||||
var revitTransform = _transformConverter.Convert((cleanMatrix, instanceProxy.units));
|
||||
|
||||
XYZ origin = revitTransform.Origin;
|
||||
XYZ basisX = revitTransform.BasisX.Normalize();
|
||||
XYZ basisY = revitTransform.BasisY.Normalize();
|
||||
|
||||
var plane = DB.Plane.CreateByOriginAndBasis(origin, basisX, basisY);
|
||||
using var sketchPlane = SketchPlane.Create(doc, plane);
|
||||
|
||||
var creationData = new FamilyInstanceCreationData(
|
||||
location: origin,
|
||||
symbol: symbol,
|
||||
host: sketchPlane,
|
||||
level: null,
|
||||
structuralType: StructuralType.NonStructural
|
||||
);
|
||||
|
||||
ICollection<ElementId> ids = doc.IsFamilyDocument
|
||||
? doc.FamilyCreate.NewFamilyInstances2([creationData])
|
||||
: doc.Create.NewFamilyInstances2([creationData]);
|
||||
|
||||
if (ids.Count == 0 || doc.GetElement(ids.First()) is not FamilyInstance instance)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
doc.Regenerate();
|
||||
var mirrorState = _familyTransformUtils.GetMirrorState(instanceProxy.transform);
|
||||
_familyTransformUtils.ApplyMirroring(doc, instance.Id, plane, mirrorState);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private FamilyInstance? PlaceFamilyInstance(Document document, InstanceProxy instanceProxy)
|
||||
{
|
||||
var definitionId = instanceProxy.definitionId;
|
||||
|
||||
if (_cache.SymbolsByDefinitionId.TryGetValue(definitionId, out var symbol))
|
||||
{
|
||||
return CreateAndPlaceFamilyInstance(document, instanceProxy, symbol);
|
||||
}
|
||||
|
||||
_logger.LogWarning("No family symbol found for definition {DefinitionId}", definitionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetFamilyWorkPlaneBased(Document famDoc, bool enabled)
|
||||
{
|
||||
var workPlaneBasedParam = famDoc.OwnerFamily.get_Parameter(BuiltInParameter.FAMILY_WORK_PLANE_BASED);
|
||||
if (workPlaneBasedParam != null && !workPlaneBasedParam.IsReadOnly)
|
||||
{
|
||||
workPlaneBasedParam.Set(enabled ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFamilyTemplatePath(Document document)
|
||||
{
|
||||
if (_cachedTemplatePath != null)
|
||||
{
|
||||
return _cachedTemplatePath;
|
||||
}
|
||||
|
||||
var version = document.Application.VersionNumber;
|
||||
var isMetric = document.DisplayUnitSystem == DisplayUnit.METRIC;
|
||||
var templateName = isMetric ? "Metric Generic Model.rft" : "Generic Model.rft";
|
||||
var assemblyLocation = typeof(RevitFamilyBaker).Assembly.Location;
|
||||
var assemblyDir =
|
||||
Path.GetDirectoryName(assemblyLocation) ?? throw new ConversionException("Could not resolve assembly directory");
|
||||
|
||||
var templatePath = Path.Combine(assemblyDir, "Resources", "Templates", version, templateName);
|
||||
|
||||
if (!File.Exists(templatePath))
|
||||
{
|
||||
_logger.LogError("Revit Family Template missing. Searched path: {templatePath}", templatePath);
|
||||
throw new ConversionException($"Could not find required family template: {templateName}");
|
||||
}
|
||||
|
||||
_cachedTemplatePath = templatePath;
|
||||
return templatePath;
|
||||
}
|
||||
|
||||
private static string GetFamilyName(InstanceDefinitionProxy definitionProxy)
|
||||
{
|
||||
var baseName = definitionProxy.name;
|
||||
if (string.IsNullOrWhiteSpace(baseName))
|
||||
{
|
||||
return "Unnamed_Block";
|
||||
}
|
||||
|
||||
char[] buffer = baseName.ToCharArray();
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (Array.IndexOf(s_invalidChars, buffer[i]) >= 0)
|
||||
{
|
||||
buffer[i] = '_';
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
var safeName = changed ? new string(buffer) : baseName;
|
||||
|
||||
// truncate to avoid MAX_PATH exceptions. 100 chars should be very safe.
|
||||
if (safeName.Length > 100)
|
||||
{
|
||||
// Append a short hash of the definition ID to guarantee uniqueness after truncation
|
||||
var shortId = definitionProxy.id?[..8] ?? Guid.NewGuid().ToString("N")[..8];
|
||||
return $"{safeName[..90]}_{shortId}";
|
||||
}
|
||||
|
||||
return safeName;
|
||||
}
|
||||
|
||||
private static Family? FindFamilyByName(Document document, string familyName)
|
||||
{
|
||||
using var collector = new FilteredElementCollector(document);
|
||||
return collector.OfClass(typeof(Family)).OfType<Family>().FirstOrDefault(f => f.Name == familyName);
|
||||
}
|
||||
|
||||
private static void SafeDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_bakedFamilyPaths.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempDirectory))
|
||||
{
|
||||
Directory.Delete(_tempDirectory, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to clean up temporary family directory at {TempDir}", _tempDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FamilyLoadOptions : IFamilyLoadOptions
|
||||
{
|
||||
public bool OnFamilyFound(bool familyInUse, out bool overwriteParameterValues)
|
||||
{
|
||||
overwriteParameterValues = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OnSharedFamilyFound(
|
||||
Family sharedFamily,
|
||||
bool familyInUse,
|
||||
out FamilySource source,
|
||||
out bool overwriteParameterValues
|
||||
)
|
||||
{
|
||||
source = FamilySource.Family;
|
||||
overwriteParameterValues = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class RevitMaterialBaker
|
||||
_converterSettings = converterSettings;
|
||||
}
|
||||
|
||||
private ElementId? FindExistingMaterialByName(string? materialName)
|
||||
private ElementId? FindExistingMaterialByName(string? materialName, Document document)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(materialName))
|
||||
{
|
||||
@@ -40,7 +40,7 @@ public class RevitMaterialBaker
|
||||
|
||||
string sanitizedName = _revitUtils.RemoveInvalidChars(materialName!);
|
||||
|
||||
using var collector = new FilteredElementCollector(_converterSettings.Current.Document);
|
||||
using var collector = new FilteredElementCollector(document);
|
||||
var existingMaterial = collector
|
||||
.OfClass(typeof(Material))
|
||||
.Cast<Material>()
|
||||
@@ -120,53 +120,54 @@ public class RevitMaterialBaker
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will bake render materials in the revit document.
|
||||
/// Bakes a single Speckle RenderMaterial into the provided document.
|
||||
/// Used both for project-level baking and isolated family-level baking.
|
||||
/// </summary>
|
||||
public ElementId BakeMaterial(RenderMaterial speckleRenderMaterial, Document document)
|
||||
{
|
||||
ElementId? existingMaterialId = FindExistingMaterialByName(speckleRenderMaterial.name, document);
|
||||
|
||||
if (existingMaterialId != null)
|
||||
{
|
||||
return existingMaterialId;
|
||||
}
|
||||
|
||||
// create new material
|
||||
// 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 - opacity;
|
||||
double smoothness = 1 - roughness;
|
||||
string matName = _revitUtils.RemoveInvalidChars($"{speckleRenderMaterial.name}");
|
||||
|
||||
var newMaterialId = Material.Create(document, matName);
|
||||
var revitMaterial = (Material)document.GetElement(newMaterialId);
|
||||
revitMaterial.Color = new Color(diffuse.R, diffuse.G, diffuse.B);
|
||||
revitMaterial.Transparency = (int)(transparency * 100);
|
||||
revitMaterial.Shininess = (int)(metalness * 128);
|
||||
revitMaterial.Smoothness = (int)(smoothness * 100);
|
||||
|
||||
return revitMaterial.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will bake render materials in the project document.
|
||||
/// </summary>
|
||||
/// <param name="speckleRenderMaterialProxies"></param>
|
||||
/// <returns></returns>
|
||||
public Dictionary<string, ElementId> BakeMaterials(
|
||||
IReadOnlyCollection<RenderMaterialProxy> speckleRenderMaterialProxies
|
||||
)
|
||||
{
|
||||
Dictionary<string, ElementId> objectIdAndMaterialIndexMap = new();
|
||||
var document = _converterSettings.Current.Document;
|
||||
|
||||
foreach (var proxy in speckleRenderMaterialProxies)
|
||||
{
|
||||
var speckleRenderMaterial = proxy.value;
|
||||
|
||||
try
|
||||
{
|
||||
// first try to match existing material by name
|
||||
ElementId? existingMaterialId = FindExistingMaterialByName(speckleRenderMaterial.name);
|
||||
|
||||
ElementId materialIdToUse;
|
||||
|
||||
if (existingMaterialId != null)
|
||||
{
|
||||
// Use existing material
|
||||
materialIdToUse = existingMaterialId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// create new material
|
||||
// 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 - opacity;
|
||||
double smoothness = 1 - roughness;
|
||||
string matName = _revitUtils.RemoveInvalidChars($"{speckleRenderMaterial.name}");
|
||||
|
||||
var newMaterialId = Material.Create(_converterSettings.Current.Document, matName);
|
||||
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)(metalness * 128);
|
||||
revitMaterial.Smoothness = (int)(smoothness * 100);
|
||||
|
||||
materialIdToUse = revitMaterial.Id;
|
||||
}
|
||||
ElementId materialIdToUse = BakeMaterial(proxy.value, document);
|
||||
|
||||
foreach (var objectId in proxy.objects)
|
||||
{
|
||||
@@ -187,27 +188,20 @@ public class RevitMaterialBaker
|
||||
var validBaseGroupName = _revitUtils.RemoveInvalidChars(baseGroupName);
|
||||
var document = _converterSettings.Current.Document;
|
||||
|
||||
using (var collector = new FilteredElementCollector(document))
|
||||
{
|
||||
var materialIds = collector
|
||||
.OfClass(typeof(Material))
|
||||
.Where(m => m.Name.Contains(validBaseGroupName))
|
||||
.Select(m => m.Id)
|
||||
.ToList();
|
||||
using var collector = new FilteredElementCollector(document);
|
||||
var materialIds = collector
|
||||
.OfClass(typeof(Material))
|
||||
.Where(m => m.Name.Contains(validBaseGroupName))
|
||||
.Select(m => m.Id)
|
||||
.ToList();
|
||||
|
||||
document.Delete(materialIds);
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -45,6 +45,8 @@ public static class SupportedCategoriesUtils
|
||||
#else
|
||||
category.Name == "OST_Grids";
|
||||
#endif
|
||||
case CategoryType.AnalyticalModel:
|
||||
return true;
|
||||
|
||||
case CategoryType.Model:
|
||||
return
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
using Speckle.Objects.Data;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
|
||||
public class DirectShapeUnpackStrategy : RevitUnpackStrategyBase
|
||||
{
|
||||
private readonly ILocalToGlobalUnpacker _localToGlobalUnpacker;
|
||||
|
||||
public DirectShapeUnpackStrategy(ILocalToGlobalUnpacker localToGlobalUnpacker)
|
||||
{
|
||||
_localToGlobalUnpacker = localToGlobalUnpacker;
|
||||
}
|
||||
|
||||
public override UnpackStrategyResult Unpack(RootObjectUnpackerResult unpackedRoot)
|
||||
{
|
||||
// 1. Build the parent map so we don't lose metadata
|
||||
var parentDataObjectMap = new Dictionary<string, DataObject>();
|
||||
PopulateParentDataObjectMap(unpackedRoot, parentDataObjectMap);
|
||||
|
||||
// 2. Flatten everything, including instances
|
||||
var maps = _localToGlobalUnpacker.Unpack(unpackedRoot.DefinitionProxies, unpackedRoot.ObjectsToConvert.ToList());
|
||||
|
||||
// 3. Filter out DataObjects to avoid converter crashes
|
||||
var cleanedMaps = FilterUnpackedDataObjects(maps);
|
||||
|
||||
return new UnpackStrategyResult(cleanedMaps, null, parentDataObjectMap);
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
|
||||
public class FamilyUnpackStrategy : RevitUnpackStrategyBase
|
||||
{
|
||||
private readonly ILocalToGlobalUnpacker _localToGlobalUnpacker;
|
||||
private readonly RootObjectUnpacker _rootObjectUnpacker;
|
||||
|
||||
public FamilyUnpackStrategy(ILocalToGlobalUnpacker localToGlobalUnpacker, RootObjectUnpacker rootObjectUnpacker)
|
||||
{
|
||||
_localToGlobalUnpacker = localToGlobalUnpacker;
|
||||
_rootObjectUnpacker = rootObjectUnpacker;
|
||||
}
|
||||
|
||||
public override UnpackStrategyResult Unpack(RootObjectUnpackerResult unpackedRoot)
|
||||
{
|
||||
var parentDataObjectMap = new Dictionary<string, DataObject>();
|
||||
var displayValueDefinitionIds = new HashSet<string>();
|
||||
|
||||
// 1. Build parent maps and identify definitions used purely for DataObject display values
|
||||
PopulateParentDataObjectMap(unpackedRoot, parentDataObjectMap, displayValueDefinitionIds);
|
||||
|
||||
// 2. Split out standard atomic objects from instance components
|
||||
var (atomicObjects, instanceComponents) = _rootObjectUnpacker.SplitAtomicObjectsAndInstances(
|
||||
unpackedRoot.ObjectsToConvert
|
||||
);
|
||||
|
||||
// 3. Collect true definition geometries to filter out
|
||||
var consumedObjectIds = new HashSet<string>();
|
||||
if (unpackedRoot.DefinitionProxies != null)
|
||||
{
|
||||
foreach (var dp in unpackedRoot.DefinitionProxies)
|
||||
{
|
||||
var defId = dp.applicationId ?? dp.id.NotNull();
|
||||
if (!displayValueDefinitionIds.Contains(defId) && (dp.id == null || !displayValueDefinitionIds.Contains(dp.id)))
|
||||
{
|
||||
foreach (var objId in dp.objects)
|
||||
{
|
||||
consumedObjectIds.Add(objId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Filter out consumed objects
|
||||
var filteredAtomicObjects = atomicObjects
|
||||
.Where(tc =>
|
||||
{
|
||||
var appId = tc.Current.applicationId;
|
||||
var id = tc.Current.id;
|
||||
return (appId == null || !consumedObjectIds.Contains(appId)) && (id == null || !consumedObjectIds.Contains(id));
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 5. Prepare true Family instances (ignore the display value proxies)
|
||||
var instanceComponentsWithPath = instanceComponents
|
||||
.Where(tc => tc.Current is not InstanceProxy proxy || !displayValueDefinitionIds.Contains(proxy.definitionId))
|
||||
.Select(tc => (Array.Empty<Collection>(), tc.Current as IInstanceComponent))
|
||||
.Where(x => x.Item2 != null)
|
||||
.Select(x => (x.Item1, x.Item2!))
|
||||
.ToList();
|
||||
|
||||
// 6. Add true definition proxies
|
||||
if (unpackedRoot.DefinitionProxies != null)
|
||||
{
|
||||
var definitions = unpackedRoot
|
||||
.DefinitionProxies.Where(proxy =>
|
||||
{
|
||||
var defId = proxy.applicationId ?? proxy.id.NotNull();
|
||||
return !displayValueDefinitionIds.Contains(defId)
|
||||
&& (proxy.id == null || !displayValueDefinitionIds.Contains(proxy.id));
|
||||
})
|
||||
.Select(proxy => (Array.Empty<Collection>(), proxy as IInstanceComponent));
|
||||
|
||||
instanceComponentsWithPath.AddRange(definitions);
|
||||
}
|
||||
|
||||
// 7. Flatten surviving atomic objects
|
||||
var localToGlobalMaps = _localToGlobalUnpacker.Unpack(unpackedRoot.DefinitionProxies, filteredAtomicObjects);
|
||||
|
||||
// 8. Clean out DataObjects using the shared base logic!
|
||||
var cleanedMaps = FilterUnpackedDataObjects(localToGlobalMaps);
|
||||
|
||||
return new UnpackStrategyResult(cleanedMaps, instanceComponentsWithPath, parentDataObjectMap);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
|
||||
public record UnpackStrategyResult(
|
||||
IReadOnlyCollection<LocalToGlobalMap> LocalToGlobalMaps,
|
||||
List<(Collection[] path, IInstanceComponent component)>? InstanceComponents,
|
||||
Dictionary<string, DataObject> ParentDataObjectMap
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the strategy for unpacking a commit into bakeable Revit objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Depending on the setting, we either blindly flatten everything into DirectShapes, or we carefully
|
||||
/// split out instance components to bake as native Revit Families.
|
||||
/// </remarks>
|
||||
public interface IRevitUnpackStrategy
|
||||
{
|
||||
UnpackStrategyResult Unpack(RootObjectUnpackerResult unpackedRoot);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
using Speckle.Connectors.DUI.Settings;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
|
||||
public class ReceiveInstancesAsFamiliesSetting(bool value = ReceiveInstancesAsFamiliesSetting.DEFAULT_VALUE)
|
||||
: ICardSetting
|
||||
{
|
||||
public const string SETTING_ID = "receiveInstancesAsFamiliesSetting";
|
||||
public const bool DEFAULT_VALUE = false;
|
||||
|
||||
public string? Id { get; set; } = SETTING_ID;
|
||||
public string? Title { get; set; } = "Receive Blocks as Families";
|
||||
public string? Type { get; set; } = "boolean";
|
||||
public object? Value { get; set; } = value;
|
||||
public List<string>? Enum { get; set; }
|
||||
}
|
||||
+104
-189
@@ -13,16 +13,17 @@ using Speckle.Converters.RevitShared;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Objects;
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Objects.Geometry;
|
||||
using Speckle.Objects.Other;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Common.Exceptions;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.GraphTraversal;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
using Transform = Speckle.Objects.Other.Transform;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
|
||||
@@ -31,10 +32,8 @@ public sealed class RevitHostObjectBuilder(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
ITransactionManager transactionManager,
|
||||
ISdkActivityFactory activityFactory,
|
||||
ILocalToGlobalUnpacker localToGlobalUnpacker,
|
||||
RevitGroupBaker groupManager,
|
||||
RevitMaterialBaker materialBaker,
|
||||
RevitViewBaker viewBaker,
|
||||
RootObjectUnpacker rootObjectUnpacker,
|
||||
ILogger<RevitHostObjectBuilder> logger,
|
||||
IThreadContext threadContext,
|
||||
@@ -43,12 +42,13 @@ public sealed class RevitHostObjectBuilder(
|
||||
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject),
|
||||
DirectShape
|
||||
> localToGlobalDirectShapeConverter,
|
||||
IReceiveConversionHandler conversionHandler
|
||||
IReceiveConversionHandler conversionHandler,
|
||||
RevitFamilyBaker familyBaker,
|
||||
DirectShapeUnpackStrategy directShapeUnpackStrategy,
|
||||
FamilyUnpackStrategy familyUnpackStrategy,
|
||||
RevitPreBakeSetupService preBakeSetupService
|
||||
) : IHostObjectBuilder, IDisposable
|
||||
{
|
||||
// Maps atomic object applicationId -> parent DataObject
|
||||
private readonly Dictionary<string, DataObject> _atomicObjectToParentDataObject = new();
|
||||
|
||||
public Task<HostObjectBuilderResult> Build(
|
||||
Base rootObject,
|
||||
string projectName,
|
||||
@@ -102,113 +102,18 @@ public sealed class RevitHostObjectBuilder(
|
||||
|
||||
// 1 - Unpack objects and proxies from root commit object
|
||||
var unpackedRoot = rootObjectUnpacker.Unpack(rootObject);
|
||||
var localToGlobalMaps = localToGlobalUnpacker.Unpack(
|
||||
unpackedRoot.DefinitionProxies,
|
||||
unpackedRoot.ObjectsToConvert.ToList()
|
||||
);
|
||||
|
||||
// Register DataObjects with InstanceProxy displayValues
|
||||
RegisterDataObjectsWithInstanceProxies(unpackedRoot);
|
||||
// 2 - Determine conversion path based on setting
|
||||
var receiveInstancesAsFamilies = converterSettings.Current.ReceiveInstancesAsFamilies;
|
||||
IRevitUnpackStrategy unpackStrategy = receiveInstancesAsFamilies ? familyUnpackStrategy : directShapeUnpackStrategy;
|
||||
|
||||
// NOTE: below is 💩... https://github.com/specklesystems/speckle-sharp-connectors/pull/813 broke sketchup to revit workflow
|
||||
// ids were modified to fix receiving instances [CNX-1707](https://linear.app/speckle/issue/CNX-1707/revit-curves-and-meshes-in-blocks-come-as-duplicated)
|
||||
// but we then broke sketchup to revit because applicationIds in proxies didn't match modified application ids which cam from #813 hack
|
||||
// given urgency to get sketchup to revit workflow back up and running, temp fix involves setting modified ids before material baking, mapping original app ids to modified ids and using those
|
||||
// this way, CNX-1707 fix stays in tact and we fix sketchup to revit
|
||||
// TODO: TransformTo and material baking needs to be fixed in Revit!!
|
||||
// 3 - Split objects/Flatten objects based on strategy
|
||||
var unpackResult = unpackStrategy.Unpack(unpackedRoot);
|
||||
|
||||
// create a mapping from original to modified IDs <- so that we can actually map ids in the proxies to the objects
|
||||
// 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();
|
||||
// 4 - Apply ID modifications and bake materials
|
||||
preBakeSetupService.ApplyIdModificationsAndBakeMaterials(unpackResult, unpackedRoot);
|
||||
|
||||
// modify application IDs BEFORE material baking
|
||||
foreach (LocalToGlobalMap localToGlobalMap in localToGlobalMaps)
|
||||
{
|
||||
if (
|
||||
localToGlobalMap.AtomicObject is ITransformable transformable
|
||||
&& localToGlobalMap.Matrix.Count > 0
|
||||
&& localToGlobalMap.AtomicObject["units"] is string units
|
||||
)
|
||||
{
|
||||
var id = localToGlobalMap.AtomicObject.id;
|
||||
var originalAppId = localToGlobalMap.AtomicObject.applicationId ?? id;
|
||||
|
||||
// Apply transformations...
|
||||
ITransformable? newTransformable = null;
|
||||
foreach (var mat in localToGlobalMap.Matrix)
|
||||
{
|
||||
transformable.TransformTo(new Transform() { matrix = mat, units = units }, out newTransformable);
|
||||
transformable = newTransformable;
|
||||
}
|
||||
|
||||
localToGlobalMap.AtomicObject = (newTransformable as Base)!;
|
||||
localToGlobalMap.AtomicObject.id = id;
|
||||
|
||||
// create modified ID and store mapping <- fixes CNX-1707 but causes us material mapping headache!!!
|
||||
string modifiedAppId = $"{originalAppId}_{Guid.NewGuid().ToString("N")[..8]}";
|
||||
if (originalAppId != null)
|
||||
{
|
||||
if (!originalToModifiedIds.TryGetValue(originalAppId, out List<string>? modifiedIds))
|
||||
{
|
||||
modifiedIds = new List<string>();
|
||||
originalToModifiedIds[originalAppId] = modifiedIds;
|
||||
}
|
||||
|
||||
modifiedIds.Add(modifiedAppId);
|
||||
}
|
||||
|
||||
localToGlobalMap.AtomicObject.applicationId = modifiedAppId;
|
||||
localToGlobalMap.Matrix = new HashSet<Matrix4x4>();
|
||||
}
|
||||
}
|
||||
|
||||
// Update the RenderMaterialProxies with the "new" (aka hacked) application IDs
|
||||
if (unpackedRoot.RenderMaterialProxies != null)
|
||||
{
|
||||
foreach (var proxy in unpackedRoot.RenderMaterialProxies)
|
||||
{
|
||||
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
|
||||
if (originalToModifiedIds.TryGetValue(objectId, out var modifiedIds))
|
||||
{
|
||||
objectIdsToUse.AddRange(modifiedIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
objectIdsToUse.Add(objectId);
|
||||
}
|
||||
}
|
||||
proxy.objects = objectIdsToUse;
|
||||
}
|
||||
}
|
||||
|
||||
// Update DataObject lookup IDs
|
||||
UpdateAtomicObjectLookupWithModifiedIds(originalToModifiedIds);
|
||||
|
||||
// 2 - Bake materials (now with the updated IDs)
|
||||
if (unpackedRoot.RenderMaterialProxies != null)
|
||||
{
|
||||
transactionManager.StartTransaction(true, "Baking materials");
|
||||
materialBaker.MapLayersRenderMaterials(unpackedRoot);
|
||||
var map = materialBaker.BakeMaterials(unpackedRoot.RenderMaterialProxies);
|
||||
foreach (var kvp in map)
|
||||
{
|
||||
revitToHostCacheSingleton.MaterialsByObjectId.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
transactionManager.CommitTransaction();
|
||||
}
|
||||
|
||||
// 2.1 - Bake views
|
||||
if (unpackedRoot.Cameras is not null)
|
||||
{
|
||||
transactionManager.StartTransaction(true, "Baking views");
|
||||
viewBaker.BakeViews(unpackedRoot.Cameras);
|
||||
transactionManager.CommitTransaction();
|
||||
}
|
||||
|
||||
// 3 - Bake objects
|
||||
// 5 - Bake objects
|
||||
(
|
||||
HostObjectBuilderResult builderResult,
|
||||
List<(DirectShape res, string applicationId)> postBakePaintTargets
|
||||
@@ -229,12 +134,52 @@ public sealed class RevitHostObjectBuilder(
|
||||
)
|
||||
)
|
||||
{
|
||||
conversionResults = BakeObjects(localToGlobalMaps, onOperationProgressed, cancellationToken);
|
||||
conversionResults = BakeObjects(
|
||||
unpackResult.LocalToGlobalMaps,
|
||||
unpackResult.ParentDataObjectMap,
|
||||
onOperationProgressed,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
transactionManager.CommitTransaction();
|
||||
}
|
||||
|
||||
// 4 - Paint solids
|
||||
// Bakes instances as families (if setting is enabled Count > 0)
|
||||
if (receiveInstancesAsFamilies && unpackResult.InstanceComponents is { Count: > 0 })
|
||||
{
|
||||
var speckleObjectLookup = new Dictionary<string, TraversalContext>();
|
||||
foreach (var tc in unpackedRoot.ObjectsToConvert)
|
||||
{
|
||||
var obj = tc.Current;
|
||||
|
||||
// 1. Primary Index: our Hash
|
||||
// TODO: investigate. this should never be null? but i (Björn) had some weird edge-cases
|
||||
if (!string.IsNullOrEmpty(obj.id))
|
||||
{
|
||||
speckleObjectLookup[obj.id.NotNullOrWhiteSpace()] = tc;
|
||||
}
|
||||
|
||||
// 2. Secondary Index: Application ID (kinda fallback)
|
||||
if (!string.IsNullOrEmpty(obj.applicationId))
|
||||
{
|
||||
speckleObjectLookup[obj.applicationId.NotNullOrWhiteSpace()] = tc;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the unpacked material proxies down to the family baker, defaulting to empty if null
|
||||
var materialProxies = unpackedRoot.RenderMaterialProxies ?? [];
|
||||
|
||||
conversionResults = BakeInstancesAsFamilies(
|
||||
unpackResult.InstanceComponents,
|
||||
conversionResults,
|
||||
speckleObjectLookup,
|
||||
materialProxies,
|
||||
onOperationProgressed
|
||||
);
|
||||
}
|
||||
|
||||
// 6 - Paint solids
|
||||
{
|
||||
using var _ = activityFactory.Start("Painting solids");
|
||||
transactionManager.StartTransaction(true, "Painting solids");
|
||||
@@ -242,7 +187,7 @@ public sealed class RevitHostObjectBuilder(
|
||||
transactionManager.CommitTransaction();
|
||||
}
|
||||
|
||||
// 5 - Create group
|
||||
// 7 - Create group
|
||||
{
|
||||
using var _ = activityFactory.Start("Grouping");
|
||||
transactionManager.StartTransaction(true, "Grouping");
|
||||
@@ -253,85 +198,53 @@ public sealed class RevitHostObjectBuilder(
|
||||
return conversionResults.builderResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers DataObjects that have InstanceProxy displayValues and builds the lookup.
|
||||
/// </summary>
|
||||
private void RegisterDataObjectsWithInstanceProxies(RootObjectUnpackerResult unpackedRoot)
|
||||
private (
|
||||
HostObjectBuilderResult builderResult,
|
||||
List<(DirectShape res, string applicationId)> postBakePaintTargets
|
||||
) BakeInstancesAsFamilies(
|
||||
List<(Collection[] path, IInstanceComponent component)> instanceComponents,
|
||||
(
|
||||
HostObjectBuilderResult builderResult,
|
||||
List<(DirectShape res, string applicationId)> postBakePaintTargets
|
||||
) currentResults,
|
||||
Dictionary<string, TraversalContext> speckleObjectLookup,
|
||||
IReadOnlyCollection<RenderMaterialProxy> materialProxies,
|
||||
IProgress<CardProgress> onOperationProgressed
|
||||
)
|
||||
{
|
||||
var definitionToDataObject = new Dictionary<string, DataObject>();
|
||||
using var _ = activityFactory.Start("Creating families");
|
||||
transactionManager.StartTransaction(true, "Creating families");
|
||||
|
||||
foreach (var tc in unpackedRoot.ObjectsToConvert)
|
||||
(List<ReceiveConversionResult> familyResults, List<string> familyElementIds) = familyBaker.BakeInstances(
|
||||
instanceComponents,
|
||||
speckleObjectLookup,
|
||||
materialProxies,
|
||||
onOperationProgressed
|
||||
);
|
||||
|
||||
// Merge results
|
||||
var mergedConversionResults = currentResults.builderResult.ConversionResults.ToList();
|
||||
mergedConversionResults.AddRange(familyResults);
|
||||
|
||||
var mergedBakedObjectIds = currentResults.builderResult.BakedObjectIds.ToList();
|
||||
mergedBakedObjectIds.AddRange(familyElementIds);
|
||||
|
||||
// Add created elements to group
|
||||
foreach (var elementId in familyElementIds)
|
||||
{
|
||||
if (tc.Current is DataObject dataObject)
|
||||
var element = converterSettings.Current.Document.GetElement(elementId);
|
||||
if (element != null)
|
||||
{
|
||||
var instanceProxies = dataObject.displayValue.OfType<InstanceProxy>().ToList();
|
||||
if (instanceProxies.Count > 0)
|
||||
{
|
||||
foreach (var ip in instanceProxies)
|
||||
{
|
||||
definitionToDataObject[ip.definitionId] = dataObject;
|
||||
}
|
||||
}
|
||||
groupManager.AddToTopLevelGroup(element);
|
||||
}
|
||||
}
|
||||
|
||||
// Build lookup: definition object applicationId -> parent DataObject
|
||||
_atomicObjectToParentDataObject.Clear();
|
||||
if (unpackedRoot.DefinitionProxies is not null)
|
||||
{
|
||||
foreach (var defProxy in unpackedRoot.DefinitionProxies)
|
||||
{
|
||||
if (
|
||||
defProxy.applicationId is not null
|
||||
&& definitionToDataObject.TryGetValue(defProxy.applicationId, out var parentDataObject)
|
||||
)
|
||||
{
|
||||
foreach (var objectId in defProxy.objects)
|
||||
{
|
||||
_atomicObjectToParentDataObject[objectId] = parentDataObject;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError(
|
||||
"Could not find parent DataObject for DefinitionProxy {ApplicationId}",
|
||||
defProxy.applicationId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transactionManager.CommitTransaction();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the atomic object lookup with modified IDs
|
||||
/// </summary>
|
||||
private void UpdateAtomicObjectLookupWithModifiedIds(Dictionary<string, List<string>> originalToModifiedIds)
|
||||
{
|
||||
// Build updated entries first to avoid modifying collection during iteration
|
||||
var entriesToAdd = new List<KeyValuePair<string, DataObject>>();
|
||||
var keysToRemove = new List<string>();
|
||||
|
||||
foreach (var kvp in _atomicObjectToParentDataObject)
|
||||
{
|
||||
if (originalToModifiedIds.TryGetValue(kvp.Key, out var modifiedIds))
|
||||
{
|
||||
keysToRemove.Add(kvp.Key);
|
||||
foreach (var modifiedId in modifiedIds)
|
||||
{
|
||||
entriesToAdd.Add(new(modifiedId, kvp.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_atomicObjectToParentDataObject.Remove(key);
|
||||
}
|
||||
|
||||
foreach (var entry in entriesToAdd)
|
||||
{
|
||||
_atomicObjectToParentDataObject[entry.Key] = entry.Value;
|
||||
}
|
||||
return (
|
||||
new HostObjectBuilderResult(mergedBakedObjectIds, mergedConversionResults),
|
||||
currentResults.postBakePaintTargets
|
||||
);
|
||||
}
|
||||
|
||||
private Autodesk.Revit.DB.Transform? CalculateNewTransform(
|
||||
@@ -357,6 +270,7 @@ public sealed class RevitHostObjectBuilder(
|
||||
List<(DirectShape res, string applicationId)> postBakePaintTargets
|
||||
) BakeObjects(
|
||||
IReadOnlyCollection<LocalToGlobalMap> localToGlobalMaps,
|
||||
Dictionary<string, DataObject> parentDataObjectMap,
|
||||
IProgress<CardProgress> onOperationProgressed,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
@@ -383,7 +297,7 @@ public sealed class RevitHostObjectBuilder(
|
||||
DataObject? parentDataObject = null;
|
||||
if (atomicId is not null)
|
||||
{
|
||||
_atomicObjectToParentDataObject.TryGetValue(atomicId, out parentDataObject);
|
||||
parentDataObjectMap.TryGetValue(atomicId, out parentDataObject);
|
||||
}
|
||||
|
||||
// direct shape creation happens here
|
||||
@@ -417,6 +331,7 @@ public sealed class RevitHostObjectBuilder(
|
||||
conversionResults.Add(new(Status.ERROR, localToGlobalMap.AtomicObject, null, null, ex));
|
||||
}
|
||||
}
|
||||
|
||||
return (new(bakedObjectIds, conversionResults), postBakePaintTargets);
|
||||
}
|
||||
|
||||
@@ -458,13 +373,12 @@ public sealed class RevitHostObjectBuilder(
|
||||
{
|
||||
DirectShapeLibrary.GetDirectShapeLibrary(converterSettings.Current.Document).Reset(); // Note: this needs to be cleared, as it is being used in the converter
|
||||
|
||||
revitToHostCacheSingleton.MaterialsByObjectId.Clear(); // Massive hack!
|
||||
_atomicObjectToParentDataObject.Clear();
|
||||
revitToHostCacheSingleton.Clear(); // "Massive hack!" - Anonymous. Ogu and Björn: it looks legit
|
||||
groupManager.PurgeGroups(baseGroupName);
|
||||
materialBaker.PurgeMaterials(baseGroupName);
|
||||
}
|
||||
|
||||
public void Dispose() => transactionManager?.Dispose();
|
||||
public void Dispose() => transactionManager.Dispose();
|
||||
|
||||
// NOTE: temp poc HACK!
|
||||
// this hack only works if we are only assuming one material applied to the solids inside DataObject displayValue. as soon as we have multiple solids with multiple materials it will break again.
|
||||
@@ -482,6 +396,7 @@ public sealed class RevitHostObjectBuilder(
|
||||
{
|
||||
SetSolidPostBakePaintTargets(item, directShapes, targets);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Objects;
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Models;
|
||||
using Transform = Speckle.Objects.Other.Transform;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
|
||||
public class RevitPreBakeSetupService
|
||||
{
|
||||
private readonly ITransactionManager _transactionManager;
|
||||
private readonly RevitMaterialBaker _materialBaker;
|
||||
private readonly RevitViewBaker _viewBaker;
|
||||
private readonly RevitToHostCacheSingleton _revitToHostCacheSingleton;
|
||||
|
||||
public RevitPreBakeSetupService(
|
||||
ITransactionManager transactionManager,
|
||||
RevitMaterialBaker materialBaker,
|
||||
RevitViewBaker viewBaker,
|
||||
RevitToHostCacheSingleton revitToHostCacheSingleton
|
||||
)
|
||||
{
|
||||
_transactionManager = transactionManager;
|
||||
_materialBaker = materialBaker;
|
||||
_viewBaker = viewBaker;
|
||||
_revitToHostCacheSingleton = revitToHostCacheSingleton;
|
||||
}
|
||||
|
||||
public void ApplyIdModificationsAndBakeMaterials(
|
||||
UnpackStrategyResult unpackResult,
|
||||
RootObjectUnpackerResult unpackedRoot
|
||||
)
|
||||
{
|
||||
Dictionary<string, List<string>> originalToModifiedIds = new();
|
||||
|
||||
foreach (LocalToGlobalMap localToGlobalMap in unpackResult.LocalToGlobalMaps)
|
||||
{
|
||||
if (
|
||||
localToGlobalMap.AtomicObject is ITransformable transformable
|
||||
&& localToGlobalMap.Matrix.Count > 0
|
||||
&& localToGlobalMap.AtomicObject["units"] is string units
|
||||
)
|
||||
{
|
||||
var id = localToGlobalMap.AtomicObject.id;
|
||||
var originalAppId = localToGlobalMap.AtomicObject.applicationId ?? id;
|
||||
|
||||
ITransformable? newTransformable = null;
|
||||
foreach (var mat in localToGlobalMap.Matrix)
|
||||
{
|
||||
transformable.TransformTo(new Transform() { matrix = mat, units = units }, out newTransformable);
|
||||
transformable = newTransformable;
|
||||
}
|
||||
|
||||
localToGlobalMap.AtomicObject = (newTransformable as Base)!;
|
||||
localToGlobalMap.AtomicObject.id = id;
|
||||
|
||||
string modifiedAppId = $"{originalAppId}_{Guid.NewGuid().ToString("N")[..8]}";
|
||||
if (originalAppId != null)
|
||||
{
|
||||
if (!originalToModifiedIds.TryGetValue(originalAppId, out List<string>? modifiedIds))
|
||||
{
|
||||
modifiedIds = new List<string>();
|
||||
originalToModifiedIds[originalAppId] = modifiedIds;
|
||||
}
|
||||
modifiedIds.Add(modifiedAppId);
|
||||
}
|
||||
|
||||
localToGlobalMap.AtomicObject.applicationId = modifiedAppId;
|
||||
localToGlobalMap.Matrix = new HashSet<Matrix4x4>();
|
||||
}
|
||||
}
|
||||
|
||||
if (unpackedRoot.RenderMaterialProxies != null)
|
||||
{
|
||||
foreach (var proxy in unpackedRoot.RenderMaterialProxies)
|
||||
{
|
||||
var objectIdsToUse = new List<string>();
|
||||
foreach (var objectId in proxy.objects)
|
||||
{
|
||||
if (originalToModifiedIds.TryGetValue(objectId, out var modifiedIds))
|
||||
{
|
||||
objectIdsToUse.AddRange(modifiedIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
objectIdsToUse.Add(objectId);
|
||||
}
|
||||
}
|
||||
proxy.objects = objectIdsToUse;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateAtomicObjectLookupWithModifiedIds(unpackResult.ParentDataObjectMap, originalToModifiedIds);
|
||||
|
||||
if (unpackedRoot.RenderMaterialProxies != null)
|
||||
{
|
||||
_transactionManager.StartTransaction(true, "Baking materials");
|
||||
_materialBaker.MapLayersRenderMaterials(unpackedRoot);
|
||||
var map = _materialBaker.BakeMaterials(unpackedRoot.RenderMaterialProxies);
|
||||
foreach (var kvp in map)
|
||||
{
|
||||
_revitToHostCacheSingleton.MaterialsByObjectId.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
_transactionManager.CommitTransaction();
|
||||
}
|
||||
|
||||
if (unpackedRoot.Cameras is not null)
|
||||
{
|
||||
_transactionManager.StartTransaction(true, "Baking views");
|
||||
_viewBaker.BakeViews(unpackedRoot.Cameras);
|
||||
_transactionManager.CommitTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAtomicObjectLookupWithModifiedIds(
|
||||
Dictionary<string, DataObject> map,
|
||||
Dictionary<string, List<string>> originalToModifiedIds
|
||||
)
|
||||
{
|
||||
var entriesToAdd = new List<KeyValuePair<string, DataObject>>();
|
||||
var keysToRemove = new List<string>();
|
||||
|
||||
foreach (var kvp in map)
|
||||
{
|
||||
if (originalToModifiedIds.TryGetValue(kvp.Key, out var modifiedIds))
|
||||
{
|
||||
keysToRemove.Add(kvp.Key);
|
||||
foreach (var modifiedId in modifiedIds)
|
||||
{
|
||||
entriesToAdd.Add(new KeyValuePair<string, DataObject>(modifiedId, kvp.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
map.Remove(key);
|
||||
}
|
||||
|
||||
foreach (var entry in entriesToAdd)
|
||||
{
|
||||
map[entry.Key] = entry.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
using Speckle.Connectors.Common.Instances;
|
||||
using Speckle.Connectors.Common.Operations.Receive;
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
|
||||
public abstract class RevitUnpackStrategyBase : IRevitUnpackStrategy
|
||||
{
|
||||
public abstract UnpackStrategyResult Unpack(RootObjectUnpackerResult unpackedRoot);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a map of definition IDs and geometry IDs to their parent DataObject to preserve metadata.
|
||||
/// </summary>
|
||||
protected void PopulateParentDataObjectMap(
|
||||
RootObjectUnpackerResult unpackedRoot,
|
||||
Dictionary<string, DataObject> map,
|
||||
HashSet<string>? displayValueDefinitionIds = null
|
||||
)
|
||||
{
|
||||
var definitionToDataObject = new Dictionary<string, DataObject>();
|
||||
|
||||
foreach (var tc in unpackedRoot.ObjectsToConvert)
|
||||
{
|
||||
if (tc.Current is DataObject dataObject)
|
||||
{
|
||||
var instanceProxies = dataObject.displayValue.OfType<InstanceProxy>().ToList();
|
||||
if (instanceProxies.Count > 0)
|
||||
{
|
||||
foreach (var ip in instanceProxies)
|
||||
{
|
||||
definitionToDataObject[ip.definitionId] = dataObject;
|
||||
displayValueDefinitionIds?.Add(ip.definitionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unpackedRoot.DefinitionProxies is not null)
|
||||
{
|
||||
foreach (var defProxy in unpackedRoot.DefinitionProxies)
|
||||
{
|
||||
var defId = defProxy.applicationId ?? defProxy.id.NotNull();
|
||||
if (
|
||||
definitionToDataObject.TryGetValue(defId, out var parentDataObject)
|
||||
|| (defProxy.id != null && definitionToDataObject.TryGetValue(defProxy.id, out parentDataObject))
|
||||
)
|
||||
{
|
||||
foreach (var objectId in defProxy.objects)
|
||||
{
|
||||
map[objectId] = parentDataObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes DataObjects that use InstanceProxies as display values from the map list.
|
||||
/// Their geometries are already flattened, and this prevents the geometry converter from crashing.
|
||||
/// </summary>
|
||||
protected IReadOnlyCollection<LocalToGlobalMap> FilterUnpackedDataObjects(
|
||||
IReadOnlyCollection<LocalToGlobalMap> maps
|
||||
) =>
|
||||
maps.Where(map =>
|
||||
{
|
||||
if (map.AtomicObject is DataObject dataObject && dataObject.displayValue.Any(dv => dv is InstanceProxy))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
+19
@@ -50,6 +50,25 @@ public class ToHostSettingsManager : IToHostSettingsManager
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool GetReceiveInstancesAsFamiliesSetting(ModelCard modelCard)
|
||||
{
|
||||
var settingValue =
|
||||
modelCard.Settings?.FirstOrDefault(s => s.Id == ReceiveInstancesAsFamiliesSetting.SETTING_ID)?.Value as bool?;
|
||||
|
||||
if (settingValue is not null)
|
||||
{
|
||||
return settingValue.Value;
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Receive instances as families setting was null for model {ModelCardId}, using default: {DefaultValue}",
|
||||
modelCard.ModelCardId,
|
||||
ReceiveInstancesAsFamiliesSetting.DEFAULT_VALUE
|
||||
);
|
||||
|
||||
return ReceiveInstancesAsFamiliesSetting.DEFAULT_VALUE;
|
||||
}
|
||||
|
||||
private Transform? GetTransform(ReceiveReferencePointType referencePointType)
|
||||
{
|
||||
Transform? referencePointTransform = null;
|
||||
|
||||
+21
-5
@@ -82,11 +82,10 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
|
||||
|
||||
IEnumerable<Element> elementsInView = GetFilteredElementsForView(document, view);
|
||||
|
||||
// NOTE: FilteredElementCollector() includes sweeps and reveals from a wall family's definition and includes them as additional objects
|
||||
// on this return. displayValue for Wall already includes these, therefore we end up with duplicate elements on wall sweeps
|
||||
// related to [CNX-1482](https://linear.app/speckle/issue/CNX-1482/wall-sweeps-published-duplicated)
|
||||
// i (björn) noticed that all these elements have an empty string as Name parameter, hence below exclusion. tested as much as possible, seems like legit fix
|
||||
var objectIds = elementsInView.Where(e => !string.IsNullOrEmpty(e.Name)).Select(e => e.UniqueId).ToList();
|
||||
// Filter out wall sweep/reveal sub-elements with empty names to avoid duplicates (CNX-1482).
|
||||
// Only these specific categories are excluded — other unnamed elements like steel connections
|
||||
// must be kept (CNX-3130).
|
||||
var objectIds = elementsInView.Where(e => !IsEmptyNameWallSubElement(e)).Select(e => e.UniqueId).ToList();
|
||||
// we need the view uniqueId among the objectIds
|
||||
// to expire the modelCards with viewFilters when the user changes category visibility
|
||||
// a change in category visibility will trigger DocChangeHandler in RevitSendBinding
|
||||
@@ -176,4 +175,21 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
|
||||
|
||||
return allElements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects wall sweep/reveal sub-elements that have empty names when returned by the
|
||||
/// view-scoped FilteredElementCollector. These are duplicates of geometry already included
|
||||
/// in the parent wall's displayValue.
|
||||
/// See <a href="https://linear.app/speckle/issue/CNX-1482">CNX-1482</a>.
|
||||
/// </summary>
|
||||
private static bool IsEmptyNameWallSubElement(Element e)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bic = e.Category?.GetBuiltInCategory();
|
||||
return bic is BuiltInCategory.OST_Cornices or BuiltInCategory.OST_Reveals;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+20
@@ -15,19 +15,29 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\BasicConnectorBindingRevit.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitBaseBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitParametersBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitReceiveBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\SelectionBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitSendBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ElementIdHelper.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\DocumentToConvert.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Elements.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\FamilyCategoryUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\FamilyGeometryBaker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\FamilyMaterialManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\FamilyTransformUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LevelUnpacker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LinkedModelHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ParameterChangeRequest.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ParameterUpdater.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitFamilyBaker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitMaterialBaker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitViewBaker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ViewUnpacker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\SupportedCategoriesUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitViewManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\DirectShapeUnpackStrategy.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\FamilyUnpackStrategy.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\HideWarningsFailuresPreprocessor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\IdStorageSchema.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\IStorageSchema.cs" />
|
||||
@@ -37,9 +47,13 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\SendCollectionManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ElementUnpacker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\IRevitUnpackingStrategy.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\ITransactionManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\ReceiveInstancesAsFamiliesSetting.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\ReceiveReferencePointSetting.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\RevitHostObjectBuilder.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\RevitPreBakeSetupService.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\RevitUnpackStrategyBase.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\ToHostSettingsManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\TransactionManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Filters\IRevitSendFilter.cs" />
|
||||
@@ -63,4 +77,10 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="$(MSBuildThisFileDirectory)Resources\Templates\$(RevitVersion)\**\*.rft">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>Resources\Templates\$(RevitVersion)\%(RecursiveDir)%(FileName)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+1
-1
@@ -10,4 +10,4 @@
|
||||
<PropertyGroup />
|
||||
<Import Project="Speckle.Connectors.RevitShared.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
+58
-19
@@ -26,8 +26,7 @@ public class ExpandCollection : GH_Component, IGH_VariableParameterComponent
|
||||
public override Guid ComponentGuid => GetType().GUID;
|
||||
protected override Bitmap Icon => Resources.speckle_collections_expand;
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
{
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager) =>
|
||||
pManager.AddParameter(
|
||||
new SpeckleCollectionParam(GH_ParamAccess.item),
|
||||
"Collection",
|
||||
@@ -35,7 +34,6 @@ public class ExpandCollection : GH_Component, IGH_VariableParameterComponent
|
||||
"The Collection you want to expand",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager) { }
|
||||
|
||||
@@ -180,27 +178,68 @@ public class ExpandCollection : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
private void CreateOutputs(List<OutputParamWrapper> outputParams)
|
||||
{
|
||||
// TODO: better, nicer handling of creation/removal
|
||||
while (Params.Output.Count > 0)
|
||||
{
|
||||
Params.UnregisterOutputParameter(Params.Output[^1]);
|
||||
}
|
||||
bool needsMaintenance = false;
|
||||
|
||||
foreach (var newParam in outputParams)
|
||||
// remove old parameters that are no longer present
|
||||
for (int i = Params.Output.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var param = new SpeckleOutputParam
|
||||
var existingParam = Params.Output[i];
|
||||
if (outputParams.All(p => p.Param.Name != existingParam.Name))
|
||||
{
|
||||
Name = newParam.Param.Name,
|
||||
NickName = newParam.Param.NickName,
|
||||
MutableNickName = false,
|
||||
Access = newParam.Param.Access
|
||||
};
|
||||
Params.RegisterOutputParam(param);
|
||||
Params.UnregisterOutputParameter(existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
// add new parameters and update existing ones in place
|
||||
for (int i = 0; i < outputParams.Count; i++)
|
||||
{
|
||||
var targetParam = outputParams[i].Param;
|
||||
var existingParam = Params.Output.FirstOrDefault(p => p.Name == targetParam.Name);
|
||||
|
||||
if (existingParam != null)
|
||||
{
|
||||
if (existingParam.Access != targetParam.Access)
|
||||
{
|
||||
existingParam.Access = targetParam.Access;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
if (existingParam.NickName != targetParam.NickName)
|
||||
{
|
||||
existingParam.NickName = targetParam.NickName;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
int currentIndex = Params.Output.IndexOf(existingParam);
|
||||
if (currentIndex != i)
|
||||
{
|
||||
Params.Output.RemoveAt(currentIndex);
|
||||
Params.Output.Insert(i, existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newParam = new SpeckleOutputParam
|
||||
{
|
||||
Name = targetParam.Name,
|
||||
NickName = targetParam.NickName,
|
||||
MutableNickName = false,
|
||||
Access = targetParam.Access,
|
||||
Description = targetParam.Description
|
||||
};
|
||||
Params.RegisterOutputParam(newParam, i);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMaintenance)
|
||||
{
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void VariableParameterMaintenance() { }
|
||||
|
||||
+4
-5
@@ -45,13 +45,13 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
{
|
||||
var properties = new Dictionary<string, ISpecklePropertyGoo>();
|
||||
var groupGoo = new SpecklePropertyGroupGoo();
|
||||
|
||||
// Validate for duplicate names
|
||||
var paramNames = Params.Input.Select(p => p.NickName).ToList();
|
||||
var duplicates = paramNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key);
|
||||
var duplicates = paramNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
|
||||
if (duplicates.Any())
|
||||
if (duplicates.Count != 0)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
@@ -77,11 +77,10 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
|
||||
|
||||
if (propertyValue != null)
|
||||
{
|
||||
properties[paramName] = propertyValue;
|
||||
groupGoo.SetValueByPath(paramName, propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
var groupGoo = new SpecklePropertyGroupGoo(properties);
|
||||
da.SetData(0, groupGoo);
|
||||
}
|
||||
|
||||
|
||||
+132
-59
@@ -40,57 +40,91 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
{
|
||||
// ALWAYS run port generation on the first iteration, BEFORE validating the current item
|
||||
// ensure that a null at index 0 doesn't prevent ports from being created.
|
||||
if (da.Iteration == 0)
|
||||
{
|
||||
// gather all property groups from the input (skipNulls = true)
|
||||
var allData = Params.Input[0].VolatileData.AllData(true).OfType<SpecklePropertyGroupGoo>().ToList();
|
||||
|
||||
// guard against empty data on file load / async operations to prevent stale ports from dropping (CNX-3245)
|
||||
if (allData.Count > 0)
|
||||
{
|
||||
var outputParamsDict = new Dictionary<string, OutputParamWrapper>();
|
||||
|
||||
foreach (var propGroup in allData)
|
||||
{
|
||||
if (propGroup?.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var key in propGroup.Value.Keys)
|
||||
{
|
||||
ISpecklePropertyGoo value = propGroup.Value[key];
|
||||
object? outputValue = value switch
|
||||
{
|
||||
SpecklePropertyGoo prop => prop.Value,
|
||||
SpecklePropertyGroupGoo pg => pg,
|
||||
_ => value
|
||||
};
|
||||
|
||||
if (!outputParamsDict.TryGetValue(key, out var existingWrapper))
|
||||
{
|
||||
var param = new SpeckleOutputParam
|
||||
{
|
||||
Name = key,
|
||||
NickName = key,
|
||||
Access = outputValue is IList ? GH_ParamAccess.list : GH_ParamAccess.item
|
||||
};
|
||||
outputParamsDict[key] = new OutputParamWrapper(param, outputValue);
|
||||
}
|
||||
else if (existingWrapper.Param.Access == GH_ParamAccess.item && outputValue is IList)
|
||||
{
|
||||
existingWrapper.Param.Access = GH_ParamAccess.list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var outputParams = outputParamsDict.Values.ToList();
|
||||
|
||||
Name = $"Properties ({outputParams.Count})";
|
||||
NickName = Name;
|
||||
|
||||
if (OutputMismatch(outputParams))
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(outputParams));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpecklePropertyGroupGoo? properties = null;
|
||||
if (!da.GetData(0, ref properties) || properties?.Value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Name = $"Properties ({properties.Value.Count})";
|
||||
NickName = Name;
|
||||
|
||||
var outputParams = new List<OutputParamWrapper>();
|
||||
|
||||
foreach (var key in properties.Value.Keys)
|
||||
for (int i = 0; i < Params.Output.Count; i++)
|
||||
{
|
||||
ISpecklePropertyGoo value = properties.Value[key];
|
||||
object? outputValue = value switch
|
||||
{
|
||||
SpecklePropertyGoo prop => prop.Value,
|
||||
SpecklePropertyGroupGoo propGroup => propGroup,
|
||||
_ => value
|
||||
};
|
||||
var outParam = Params.Output[i];
|
||||
|
||||
var param = new SpeckleOutputParam
|
||||
if (properties.Value.TryGetValue(outParam.Name, out ISpecklePropertyGoo? value))
|
||||
{
|
||||
Name = key,
|
||||
NickName = key,
|
||||
Access = outputValue is IList ? GH_ParamAccess.list : GH_ParamAccess.item
|
||||
};
|
||||
|
||||
outputParams.Add(new OutputParamWrapper(param, outputValue));
|
||||
}
|
||||
|
||||
// handle parameter creation/update (only on first iteration)
|
||||
if (da.Iteration == 0 && OutputMismatch(outputParams))
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(outputParams));
|
||||
return; // exit early
|
||||
}
|
||||
// only set data if we have the correct parameter structure
|
||||
if (Params.Output.Count == outputParams.Count)
|
||||
{
|
||||
for (int i = 0; i < outputParams.Count; i++)
|
||||
{
|
||||
var outputParam = outputParams[i];
|
||||
switch (outputParam.Param.Access)
|
||||
object? outputValue = value switch
|
||||
{
|
||||
case GH_ParamAccess.item:
|
||||
da.SetData(i, outputParam.Value);
|
||||
break;
|
||||
case GH_ParamAccess.list:
|
||||
da.SetDataList(i, outputParam.Value as IList ?? new List<object?>());
|
||||
break;
|
||||
SpecklePropertyGoo prop => prop.Value,
|
||||
SpecklePropertyGroupGoo propGroup => propGroup,
|
||||
_ => value
|
||||
};
|
||||
|
||||
if (outParam.Access == GH_ParamAccess.item)
|
||||
{
|
||||
da.SetData(i, outputValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
da.SetDataList(i, outputValue as IList ?? new List<object?>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,29 +135,68 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
|
||||
/// </summary>
|
||||
private void CreateOutputs(List<OutputParamWrapper> outputParams)
|
||||
{
|
||||
// remove all existing output parameters
|
||||
while (Params.Output.Count > 0)
|
||||
{
|
||||
Params.UnregisterOutputParameter(Params.Output[^1]);
|
||||
}
|
||||
bool needsMaintenance = false;
|
||||
|
||||
// add new output parameters
|
||||
foreach (var newParam in outputParams)
|
||||
// remove old parameters that are no longer present
|
||||
for (int i = Params.Output.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var param = new SpeckleOutputParam
|
||||
var existingParam = Params.Output[i];
|
||||
if (outputParams.All(p => p.Param.Name != existingParam.Name))
|
||||
{
|
||||
Name = newParam.Param.Name,
|
||||
NickName = newParam.Param.NickName,
|
||||
MutableNickName = false,
|
||||
Access = newParam.Param.Access
|
||||
};
|
||||
Params.RegisterOutputParam(param);
|
||||
Params.UnregisterOutputParameter(existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
// notify gh of parameter changes
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
// add new parameters and update existing ones in place to preserve wires
|
||||
for (int i = 0; i < outputParams.Count; i++)
|
||||
{
|
||||
var targetParam = outputParams[i].Param;
|
||||
var existingParam = Params.Output.FirstOrDefault(p => p.Name == targetParam.Name);
|
||||
|
||||
if (existingParam != null)
|
||||
{
|
||||
if (existingParam.Access != targetParam.Access)
|
||||
{
|
||||
existingParam.Access = targetParam.Access;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
if (existingParam.NickName != targetParam.NickName)
|
||||
{
|
||||
existingParam.NickName = targetParam.NickName;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
int currentIndex = Params.Output.IndexOf(existingParam);
|
||||
if (currentIndex != i)
|
||||
{
|
||||
Params.Output.RemoveAt(currentIndex);
|
||||
Params.Output.Insert(i, existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newParam = new SpeckleOutputParam
|
||||
{
|
||||
Name = targetParam.Name,
|
||||
NickName = targetParam.NickName,
|
||||
MutableNickName = false,
|
||||
Access = targetParam.Access
|
||||
};
|
||||
Params.RegisterOutputParam(newParam, i);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMaintenance)
|
||||
{
|
||||
// notify gh of parameter changes
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+68
-20
@@ -37,19 +37,27 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
pManager.AddTextParameter(
|
||||
"Property Key",
|
||||
"P",
|
||||
"K",
|
||||
"Find objects with a property that has a matching key",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[2].Optional = true;
|
||||
|
||||
pManager.AddTextParameter(
|
||||
"Property Value",
|
||||
"V",
|
||||
"Find objects with a property that has a matching value",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[3].Optional = true;
|
||||
|
||||
pManager.AddTextParameter(
|
||||
"Material Name",
|
||||
"M",
|
||||
"Find objects with a render material that has a matching name",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[3].Optional = true;
|
||||
Params.Input[4].Optional = true;
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
|
||||
@@ -89,10 +97,12 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
string name = "";
|
||||
dataAccess.GetData(1, ref name);
|
||||
string property = "";
|
||||
dataAccess.GetData(2, ref property);
|
||||
string propertyKey = "";
|
||||
dataAccess.GetData(2, ref propertyKey);
|
||||
string propertyValue = "";
|
||||
dataAccess.GetData(3, ref propertyValue);
|
||||
string material = "";
|
||||
dataAccess.GetData(3, ref material);
|
||||
dataAccess.GetData(4, ref material);
|
||||
|
||||
// optional parameters - only read if they've been added via ⊕
|
||||
string appId = "";
|
||||
@@ -118,7 +128,19 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
foreach (SpeckleWrapper wrapper in objects.Cast<SpeckleWrapper>())
|
||||
{
|
||||
if (MatchesAllFilters(wrapper, name, property, material, appId, filterByAppId, speckleId, filterBySpeckleId))
|
||||
if (
|
||||
MatchesAllFilters(
|
||||
wrapper,
|
||||
name,
|
||||
propertyKey,
|
||||
propertyValue,
|
||||
material,
|
||||
appId,
|
||||
filterByAppId,
|
||||
speckleId,
|
||||
filterBySpeckleId
|
||||
)
|
||||
)
|
||||
{
|
||||
matchedObjects.Add(wrapper);
|
||||
}
|
||||
@@ -149,7 +171,8 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
private bool MatchesAllFilters(
|
||||
SpeckleWrapper wrapper,
|
||||
string name,
|
||||
string property,
|
||||
string propertyKey,
|
||||
string propertyValue,
|
||||
string material,
|
||||
string appId,
|
||||
bool filterByAppId,
|
||||
@@ -164,7 +187,7 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
}
|
||||
|
||||
// filter by property
|
||||
if (!MatchesPropertyFilter(wrapper, property))
|
||||
if (!MatchesPropertyFilter(wrapper, propertyKey, propertyValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -190,9 +213,12 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MatchesPropertyFilter(SpeckleWrapper wrapper, string property)
|
||||
private bool MatchesPropertyFilter(SpeckleWrapper wrapper, string propertyKey, string propertyValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(property))
|
||||
bool hasKeyFilter = !string.IsNullOrEmpty(propertyKey);
|
||||
bool hasValueFilter = !string.IsNullOrEmpty(propertyValue);
|
||||
|
||||
if (!hasKeyFilter && !hasValueFilter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -208,8 +234,30 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return false;
|
||||
}
|
||||
|
||||
// use flattened properties to search ALL nested property keys
|
||||
return properties.Flatten().Keys.Any(key => MatchesSearchPattern(property, key));
|
||||
var flattenedProps = properties.Flatten();
|
||||
|
||||
// Check both property key and value simultaneously
|
||||
if (hasKeyFilter && hasValueFilter)
|
||||
{
|
||||
return flattenedProps.Any(kvp =>
|
||||
MatchesSearchPattern(propertyKey, kvp.Key)
|
||||
&& MatchesSearchPattern(propertyValue, kvp.Value.Value?.ToString() ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
// Check just property key
|
||||
if (hasKeyFilter)
|
||||
{
|
||||
return flattenedProps.Keys.Any(key => MatchesSearchPattern(propertyKey, key));
|
||||
}
|
||||
|
||||
// Check just property value
|
||||
if (hasValueFilter)
|
||||
{
|
||||
return flattenedProps.Values.Any(val => MatchesSearchPattern(propertyValue, val.Value?.ToString() ?? ""));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool MatchesMaterialFilter(SpeckleWrapper wrapper, string material)
|
||||
@@ -258,23 +306,23 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return false;
|
||||
}
|
||||
|
||||
// only allow inserting after the fixed parameters (index 4+)
|
||||
if (index < 4)
|
||||
// only allow inserting after the fixed parameters (index 5+)
|
||||
if (index < 5)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// check how many optional params are already added (total inputs - 4 fixed)
|
||||
int addedOptionalCount = Params.Input.Count - 4;
|
||||
// check how many optional params are already added (total inputs - 5 fixed)
|
||||
int addedOptionalCount = Params.Input.Count - 5;
|
||||
|
||||
// we have 2 optional parameters available
|
||||
return addedOptionalCount < 2;
|
||||
}
|
||||
|
||||
public bool CanRemoveParameter(GH_ParameterSide side, int index) =>
|
||||
// only allow removing optional input parameters (index 4+)
|
||||
// only allow removing optional input parameters (index 5+)
|
||||
side == GH_ParameterSide.Input
|
||||
&& index >= 4;
|
||||
&& index >= 5;
|
||||
|
||||
/// <remarks>
|
||||
/// The ternary operator for NickName is needed due to a Grasshopper quirk where
|
||||
@@ -316,12 +364,12 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return new Param_String();
|
||||
}
|
||||
|
||||
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input && index >= 4;
|
||||
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input && index >= 5;
|
||||
|
||||
public void VariableParameterMaintenance()
|
||||
{
|
||||
// ensure all optional parameters stay marked as optional
|
||||
for (int i = 4; i < Params.Input.Count; i++)
|
||||
for (int i = 5; i < Params.Input.Count; i++)
|
||||
{
|
||||
Params.Input[i].Optional = true;
|
||||
}
|
||||
|
||||
+6
-6
@@ -24,14 +24,14 @@ public class PropertyGroupPathsSelector : ValueSet<IGH_Goo>
|
||||
|
||||
protected override void LoadVolatileData()
|
||||
{
|
||||
List<SpecklePropertyGroupGoo> propertyGroups = VolatileData
|
||||
.AllData(true)
|
||||
.OfType<SpecklePropertyGroupGoo>()
|
||||
.ToList();
|
||||
var allData = VolatileData.AllData(true).ToList();
|
||||
|
||||
if (VolatileDataCount > propertyGroups.Count)
|
||||
List<SpecklePropertyGroupGoo> propertyGroups = allData.OfType<SpecklePropertyGroupGoo>().ToList();
|
||||
|
||||
// compare against allData.Count to safely ignore nulls (CNX-3176)
|
||||
if (allData.Count > propertyGroups.Count)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Only Speckle Properties are accepted as inputs.");
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Only Speckle Properties are accepted as inputs.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+2
-31
@@ -35,10 +35,8 @@ public class QueryProperties : GH_Component
|
||||
pManager.AddTextParameter("Keys", "K", "Property keys to filter by", GH_ParamAccess.list);
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
|
||||
{
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
|
||||
pManager.AddGenericParameter("Values", "V", "The values of the specified keys", GH_ParamAccess.list);
|
||||
}
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
{
|
||||
@@ -64,7 +62,7 @@ public class QueryProperties : GH_Component
|
||||
List<object?> values = [];
|
||||
foreach (string key in keys)
|
||||
{
|
||||
var value = GetValueByPath(properties, key);
|
||||
var value = properties.GetValueByPath(key);
|
||||
var extractedValue = (value as SpecklePropertyGoo)?.Value ?? value;
|
||||
|
||||
// NOTE: if property is a list, flatten into individual items for native gh list access
|
||||
@@ -80,31 +78,4 @@ public class QueryProperties : GH_Component
|
||||
|
||||
da.SetDataList(0, values);
|
||||
}
|
||||
|
||||
public static ISpecklePropertyGoo? GetValueByPath(SpecklePropertyGroupGoo data, string path)
|
||||
{
|
||||
string[] keys = path.Split('.');
|
||||
ISpecklePropertyGoo? current = data;
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (current is SpecklePropertyGroupGoo dict)
|
||||
{
|
||||
if (dict.Value.TryGetValue(key, out ISpecklePropertyGoo? next))
|
||||
{
|
||||
current = next;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return null; // Current is not a dictionary, path is invalid
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
+33
-20
@@ -104,9 +104,19 @@ public class SpeckleDataObjectPassthrough()
|
||||
}
|
||||
|
||||
List<SpeckleGeometryWrapperGoo> inputGeometry = new();
|
||||
if (!da.GetDataList(1, inputGeometry) && result == null)
|
||||
bool hasGeometries = da.GetDataList(1, inputGeometry);
|
||||
|
||||
string? inputName = null;
|
||||
da.GetData(2, ref inputName);
|
||||
|
||||
SpecklePropertyGroupGoo? inputProperties = null;
|
||||
da.GetData(3, ref inputProperties);
|
||||
|
||||
bool hasAppId = TryGetApplicationIdInput(da, out string? inputAppId);
|
||||
|
||||
if (result == null && !hasGeometries && inputName == null && inputProperties == null && !hasAppId)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a Speckle DataObject or Geometries");
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a DataObject or at least one input.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -119,19 +129,25 @@ public class SpeckleDataObjectPassthrough()
|
||||
}
|
||||
}
|
||||
|
||||
string? inputName = null;
|
||||
da.GetData(2, ref inputName);
|
||||
|
||||
SpecklePropertyGroupGoo? inputProperties = null;
|
||||
da.GetData(3, ref inputProperties);
|
||||
|
||||
// process geometry
|
||||
if (result == null)
|
||||
{
|
||||
result = new SpeckleDataObjectWrapperGoo().Value;
|
||||
}
|
||||
|
||||
if (inputGeometry.Count > 0)
|
||||
// process name first (geometry loop must use the final name)
|
||||
if (inputName != null)
|
||||
{
|
||||
result.Name = inputName;
|
||||
}
|
||||
|
||||
// process properties first (geometry loop must use the final properties)
|
||||
if (inputProperties != null)
|
||||
{
|
||||
result.Properties = inputProperties;
|
||||
}
|
||||
|
||||
if (hasGeometries)
|
||||
{
|
||||
result.Geometries.Clear();
|
||||
foreach (var inputGeo in inputGeometry)
|
||||
@@ -148,21 +164,18 @@ public class SpeckleDataObjectPassthrough()
|
||||
result.Geometries.Add(mutatingGeo);
|
||||
}
|
||||
}
|
||||
|
||||
// process name
|
||||
if (inputName != null)
|
||||
else if (inputName != null || inputProperties != null)
|
||||
{
|
||||
result.Name = inputName;
|
||||
}
|
||||
|
||||
// process properties
|
||||
if (inputProperties != null)
|
||||
{
|
||||
result.Properties = inputProperties;
|
||||
// keep existing geometries in sync when only name/properties are overridden
|
||||
foreach (var geo in result.Geometries)
|
||||
{
|
||||
geo.Base[Constants.NAME_PROP] = result.Name;
|
||||
geo.Properties = result.Properties;
|
||||
}
|
||||
}
|
||||
|
||||
// process application id (only if user provided one)
|
||||
if (TryGetApplicationIdInput(da, out string? inputAppId))
|
||||
if (hasAppId)
|
||||
{
|
||||
result.ApplicationId = inputAppId;
|
||||
}
|
||||
|
||||
+39
-32
@@ -30,17 +30,30 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
|
||||
private enum PropertyMode
|
||||
{
|
||||
Merge, // this should be default mode
|
||||
Merge,
|
||||
Replace,
|
||||
Remove
|
||||
Remove,
|
||||
Update, // pre rewording (cnx-3177), keeping for scripts with settings saved
|
||||
Overwrite // pre rewording (cnx-3177), keeping for scripts with settings saved
|
||||
}
|
||||
|
||||
private PropertyMode _mode = PropertyMode.Merge;
|
||||
private PropertyMode _mode = PropertyMode.Update;
|
||||
private PropertyMode Mode
|
||||
{
|
||||
get => _mode;
|
||||
set
|
||||
{
|
||||
// auto-migrate legacy modes to new modes
|
||||
if (value == PropertyMode.Update)
|
||||
{
|
||||
value = PropertyMode.Merge;
|
||||
}
|
||||
|
||||
if (value == PropertyMode.Overwrite)
|
||||
{
|
||||
value = PropertyMode.Replace;
|
||||
}
|
||||
|
||||
if (_mode != value)
|
||||
{
|
||||
_mode = value;
|
||||
@@ -96,7 +109,7 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
}
|
||||
|
||||
// validate that keys and values are of valid length
|
||||
if ((Mode == PropertyMode.Merge || Mode == PropertyMode.Replace) && inputKeys.Count != inputValues.Count)
|
||||
if ((Mode == PropertyMode.Update || Mode == PropertyMode.Overwrite) && inputKeys.Count != inputValues.Count)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Keys and values are mismatched in length");
|
||||
return;
|
||||
@@ -108,18 +121,16 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
return;
|
||||
}
|
||||
|
||||
// process the properties
|
||||
Dictionary<string, ISpecklePropertyGoo> result =
|
||||
inputProperties is null || Mode == PropertyMode.Replace
|
||||
? new()
|
||||
: inputProperties.Value.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||
// deep clone to prevent mutating upstream grasshopper data
|
||||
SpecklePropertyGroupGoo resultGoo =
|
||||
inputProperties is null || Mode == PropertyMode.Replace ? new SpecklePropertyGroupGoo() : inputProperties.Clone();
|
||||
|
||||
// process keys and values
|
||||
if (hasKeys)
|
||||
{
|
||||
// check for duplicate keys
|
||||
var duplicates = inputKeys.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key);
|
||||
if (duplicates.Any())
|
||||
var duplicates = inputKeys.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
if (duplicates.Count != 0)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
@@ -128,12 +139,12 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
return;
|
||||
}
|
||||
|
||||
// set keyvalue pairs
|
||||
// set key-value pairs
|
||||
for (int i = 0; i < inputKeys.Count; i++)
|
||||
{
|
||||
string key = inputKeys[i];
|
||||
object? value = Mode == PropertyMode.Remove ? null : inputValues[i];
|
||||
ISpecklePropertyGoo? convertedValue = null;
|
||||
ISpecklePropertyGoo? convertedValue;
|
||||
switch (value)
|
||||
{
|
||||
case SpecklePropertyGroupGoo propGoo:
|
||||
@@ -148,7 +159,7 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
$"Values contain an invalid data type. Only strings, numbers, booleans, planes, vectors, intervals, and other Speckle properties are supported."
|
||||
"Values contain an invalid data type. Only strings, numbers, booleans, planes, vectors, intervals, and other Speckle properties are supported."
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -160,29 +171,19 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
switch (Mode)
|
||||
{
|
||||
case PropertyMode.Merge:
|
||||
if (result.ContainsKey(key))
|
||||
{
|
||||
result[key] = convertedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(key, convertedValue);
|
||||
}
|
||||
break;
|
||||
case PropertyMode.Replace:
|
||||
result.Add(key, convertedValue);
|
||||
resultGoo.SetValueByPath(key, convertedValue);
|
||||
break;
|
||||
case PropertyMode.Remove:
|
||||
result.Remove(key);
|
||||
resultGoo.RemoveValueByPath(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groupGoo = new SpecklePropertyGroupGoo(result);
|
||||
da.SetData(0, groupGoo);
|
||||
da.SetDataList(1, result.Keys);
|
||||
da.SetDataList(2, result.Values);
|
||||
da.SetData(0, resultGoo);
|
||||
da.SetDataList(1, resultGoo.Value.Keys);
|
||||
da.SetDataList(2, resultGoo.Value.Values);
|
||||
}
|
||||
|
||||
public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
|
||||
@@ -192,18 +193,24 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
Menu_AppendSeparator(menu); // modes section
|
||||
foreach (PropertyMode mode in Enum.GetValues(typeof(PropertyMode)))
|
||||
{
|
||||
// hide "legacy modes" (before cnx-3177 rewording) from the dropdown
|
||||
if (mode is PropertyMode.Update or PropertyMode.Overwrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var modeItem = Menu_AppendItem(menu, mode.ToString(), (_, _) => Mode = mode, true, mode == Mode);
|
||||
switch (mode)
|
||||
{
|
||||
case PropertyMode.Merge:
|
||||
modeItem.ToolTipText =
|
||||
"Input keyvalue pairs will be merged with existing properties. Any existing keys will be updated with new values.";
|
||||
@"Input key-value pairs will be merged with existing properties. Any existing keys will be updated with new values.";
|
||||
break;
|
||||
case PropertyMode.Replace:
|
||||
modeItem.ToolTipText = "Existing properties will be cleared and replaced by input keyvalue pairs.";
|
||||
modeItem.ToolTipText = @"Existing properties will be cleared and replaced by input key-value pairs.";
|
||||
break;
|
||||
case PropertyMode.Remove:
|
||||
modeItem.ToolTipText = "Existing keyvalue pairs that match the input keys will be removed from properties.";
|
||||
modeItem.ToolTipText = @"Existing key-value pairs that match the input keys will be removed from properties.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -36,13 +36,14 @@ public class AccountManagerComponent : GH_Component, IDisposable
|
||||
ComponentCategories.OPERATIONS
|
||||
)
|
||||
{
|
||||
Attributes = new AccountManagerComponentAttributes(this);
|
||||
_accountManager = PriorityLoader.Container.GetRequiredService<IAccountManager>();
|
||||
Accounts = _accountManager.GetAccounts().ToList();
|
||||
|
||||
SignInButton = new GhContextMenuButton("Sign In", "Sign In", "Click to sign into Speckle account.", AuthFlow);
|
||||
}
|
||||
|
||||
public override void CreateAttributes() => m_attributes = new AccountManagerComponentAttributes(this);
|
||||
|
||||
private bool AuthFlow(ToolStripDropDown menu)
|
||||
{
|
||||
_isAddingAccount = true;
|
||||
|
||||
+2
-9
@@ -32,9 +32,10 @@ public class ReceiveAsyncComponent : GH_AsyncComponent<ReceiveAsyncComponent>
|
||||
: base("Load", "L", "Load a model from Speckle", ComponentCategories.PRIMARY_RIBBON, ComponentCategories.OPERATIONS)
|
||||
{
|
||||
BaseWorker = new ReceiveComponentWorker(this);
|
||||
Attributes = new ReceiveAsyncComponentAttributes(this);
|
||||
}
|
||||
|
||||
public override void CreateAttributes() => m_attributes = new ReceiveAsyncComponentAttributes(this);
|
||||
|
||||
public override Guid ComponentGuid => GetType().GUID;
|
||||
protected override Bitmap Icon => Resources.speckle_operations_load;
|
||||
|
||||
@@ -535,19 +536,11 @@ public sealed class ReceiveComponentWorker : WorkerInstance<ReceiveAsyncComponen
|
||||
|
||||
public class ReceiveAsyncComponentAttributes : GH_ComponentAttributes
|
||||
{
|
||||
private bool _selected;
|
||||
|
||||
public ReceiveAsyncComponentAttributes(GH_Component owner)
|
||||
: base(owner) { }
|
||||
|
||||
private Rectangle ButtonBounds { get; set; }
|
||||
|
||||
public override bool Selected
|
||||
{
|
||||
get => _selected;
|
||||
set => _selected = value;
|
||||
}
|
||||
|
||||
protected override void Layout()
|
||||
{
|
||||
base.Layout();
|
||||
|
||||
+2
-9
@@ -35,9 +35,10 @@ public class SendAsyncComponent : GH_AsyncComponent<SendAsyncComponent>
|
||||
)
|
||||
{
|
||||
BaseWorker = new SendComponentWorker(this);
|
||||
Attributes = new SendAsyncComponentAttributes(this);
|
||||
}
|
||||
|
||||
public override void CreateAttributes() => m_attributes = new SendAsyncComponentAttributes(this);
|
||||
|
||||
public override Guid ComponentGuid => GetType().GUID;
|
||||
|
||||
protected override Bitmap Icon => Resources.speckle_operations_publish;
|
||||
@@ -477,19 +478,11 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
|
||||
|
||||
public class SendAsyncComponentAttributes : GH_ComponentAttributes
|
||||
{
|
||||
private bool _selected;
|
||||
|
||||
public SendAsyncComponentAttributes(GH_Component owner)
|
||||
: base(owner) { }
|
||||
|
||||
private Rectangle ButtonBounds { get; set; }
|
||||
|
||||
public override bool Selected
|
||||
{
|
||||
get => _selected;
|
||||
set => _selected = value;
|
||||
}
|
||||
|
||||
protected override void Layout()
|
||||
{
|
||||
base.Layout();
|
||||
|
||||
+2
-1
@@ -43,7 +43,6 @@ public class SpeckleSelectModelComponent : GH_Component
|
||||
ComponentCategories.OPERATIONS
|
||||
)
|
||||
{
|
||||
Attributes = new SpeckleSelectModelComponentAttributes(this);
|
||||
SpeckleOperationWizard = new SpeckleOperationWizard(RefreshComponent, UpdateComponentMessage, false);
|
||||
|
||||
WorkspaceContextMenuButton = SpeckleOperationWizard.WorkspaceMenuHandler.WorkspaceContextMenuButton;
|
||||
@@ -52,6 +51,8 @@ public class SpeckleSelectModelComponent : GH_Component
|
||||
VersionContextMenuButton = SpeckleOperationWizard!.VersionMenuHandler!.VersionContextMenuButton; // TODO: fix this shit later when we split
|
||||
}
|
||||
|
||||
public override void CreateAttributes() => m_attributes = new SpeckleSelectModelComponentAttributes(this);
|
||||
|
||||
private Task RefreshComponent()
|
||||
{
|
||||
ExpireSolution(true);
|
||||
|
||||
+21
-9
@@ -26,7 +26,8 @@ namespace Speckle.Connectors.GrasshopperShared.Operations.Receive;
|
||||
/// </remarks>
|
||||
internal sealed class LocalToGlobalMapHandler
|
||||
{
|
||||
public Dictionary<string, SpeckleGeometryWrapper> ConvertedObjectsMap { get; } = new();
|
||||
public Dictionary<string, SpeckleGeometryWrapper> ConvertedObjectsMap { get; } = [];
|
||||
private readonly HashSet<string> _processedDataObjects = [];
|
||||
|
||||
// injected via constructor (DI-managed)
|
||||
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
|
||||
@@ -113,7 +114,7 @@ internal sealed class LocalToGlobalMapHandler
|
||||
var obj = atomicContext.Current;
|
||||
var objId = obj.applicationId ?? obj.id;
|
||||
|
||||
if (objId is null || ConvertedObjectsMap.ContainsKey(objId))
|
||||
if (objId is null || ConvertedObjectsMap.ContainsKey(objId) || _processedDataObjects.Contains(objId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -132,6 +133,15 @@ internal sealed class LocalToGlobalMapHandler
|
||||
{
|
||||
List<(object, Base)> converted = SpeckleConversionContext.Current.ConvertToHost(obj);
|
||||
|
||||
// geometry-less data objects (cnx-2522)
|
||||
bool isMetadataOnly = obj is DataObject { displayValue.Count: 0 };
|
||||
|
||||
// bypass the early return if this genuinely is a metadata-only DataObject (cnx-3237)
|
||||
if (converted.Count == 0 && !isMetadataOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// get path and collection
|
||||
var path = _traversalContextUnpacker.GetCollectionPath(atomicContext).ToList();
|
||||
var objectCollection = CollectionRebuilder.GetOrCreateSpeckleCollectionFromPath(
|
||||
@@ -140,15 +150,11 @@ internal sealed class LocalToGlobalMapHandler
|
||||
_materialUnpacker
|
||||
);
|
||||
|
||||
// nothing converted - nothing to do
|
||||
if (converted.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// handle normal DataObject (has converted geometry)
|
||||
// handle all DataObjects
|
||||
if (obj is DataObject normalDataObject)
|
||||
{
|
||||
_processedDataObjects.Add(objId);
|
||||
|
||||
var geometries = ConvertToGeometryWrappers(converted);
|
||||
var dataObjectWrapper = CreateDataObjectWrapper(normalDataObject, geometries, path, objectCollection);
|
||||
|
||||
@@ -214,6 +220,12 @@ internal sealed class LocalToGlobalMapHandler
|
||||
return;
|
||||
}
|
||||
|
||||
// ensures we don't process the same registered DataObject multiple times due to multiple traversal encounters.
|
||||
if (!_processedDataObjects.Add(dataObjectId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = _traversalContextUnpacker.GetCollectionPath(atomicContext).ToList();
|
||||
|
||||
+96
-1
@@ -10,7 +10,7 @@ namespace Speckle.Connectors.GrasshopperShared.Parameters;
|
||||
/// </summary>
|
||||
public partial class SpecklePropertyGroupGoo : GH_Goo<Dictionary<string, ISpecklePropertyGoo>>, ISpecklePropertyGoo
|
||||
{
|
||||
public override IGH_Goo Duplicate() => throw new NotImplementedException();
|
||||
public override IGH_Goo Duplicate() => Clone();
|
||||
|
||||
public override string ToString() => $"Speckle Properties : ({Value.Count})";
|
||||
|
||||
@@ -213,5 +213,100 @@ public partial class SpecklePropertyGroupGoo : GH_Goo<Dictionary<string, ISpeckl
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a deep clone of the property group to prevent mutating upstream Grasshopper data.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A new SpecklePropertyGroupGoo instance with cloned nested properties.
|
||||
/// Needed since adding support for dot notation [CNX-3179]
|
||||
/// </returns>
|
||||
public SpecklePropertyGroupGoo Clone()
|
||||
{
|
||||
var newDict = new Dictionary<string, ISpecklePropertyGoo>();
|
||||
foreach (var kvp in Value)
|
||||
{
|
||||
newDict[kvp.Key] = kvp.Value is SpecklePropertyGroupGoo group ? group.Clone() : kvp.Value;
|
||||
}
|
||||
return new SpecklePropertyGroupGoo(newDict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property value using dot-notation path traversal. Creates nested groups if they do not exist.
|
||||
/// </summary>
|
||||
/// <param name="path">The dot-notation property path.</param>
|
||||
/// <param name="value">The Speckle property to set.</param>
|
||||
public void SetValueByPath(string path, ISpecklePropertyGoo value)
|
||||
{
|
||||
string[] parts = path.Split('.');
|
||||
var current = Value;
|
||||
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
string part = parts[i];
|
||||
if (!current.TryGetValue(part, out var existing) || existing is not SpecklePropertyGroupGoo group)
|
||||
{
|
||||
group = new SpecklePropertyGroupGoo();
|
||||
current[part] = group;
|
||||
}
|
||||
current = group.Value;
|
||||
}
|
||||
|
||||
current[parts[^1]] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a property value using dot-notation path traversal.
|
||||
/// </summary>
|
||||
/// <param name="path">The dot-notation property path.</param>
|
||||
public void RemoveValueByPath(string path)
|
||||
{
|
||||
string[] parts = path.Split('.');
|
||||
var current = Value;
|
||||
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
string part = parts[i];
|
||||
if (!current.TryGetValue(part, out var existing) || existing is not SpecklePropertyGroupGoo group)
|
||||
{
|
||||
return; // path does not exist
|
||||
}
|
||||
|
||||
current = group.Value;
|
||||
}
|
||||
|
||||
current.Remove(parts[^1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a property value using dot-notation path traversal. Attempts exact match first.
|
||||
/// </summary>
|
||||
/// <param name="path">The dot-notation property path.</param>
|
||||
/// <returns>The matching property goo if found, otherwise null.</returns>
|
||||
public ISpecklePropertyGoo? GetValueByPath(string path)
|
||||
{
|
||||
// attempt exact match first for literal dots in native keys
|
||||
if (Value.TryGetValue(path, out var exactMatch))
|
||||
{
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
string[] parts = path.Split('.');
|
||||
ISpecklePropertyGoo? current = this;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (current is SpecklePropertyGroupGoo group && group.Value.TryGetValue(part, out var next))
|
||||
{
|
||||
current = next;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null; // current is not a dictionary, or path is invalid
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => base.GetHashCode();
|
||||
}
|
||||
|
||||
@@ -92,36 +92,105 @@ public class RhinoMapperBinding : IBinding
|
||||
/// <summary>
|
||||
/// Assigns selected objects to a specific Revit category.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If block instance is selected, mapping is applied uniformly to all instances of that block definition in the doc.
|
||||
/// </remarks>
|
||||
public void AssignObjectsToCategory(string[] objectIds, string categoryValue)
|
||||
{
|
||||
var objectsToModify = new HashSet<RhinoObject>();
|
||||
var processedDefinitions = new HashSet<int>(); // Tracks unique block definitions
|
||||
|
||||
foreach (var objectIdString in objectIds)
|
||||
{
|
||||
// NOTE: should we be checking if key already exists?
|
||||
// For POC, straightforward set on object
|
||||
var rhinoObject = _rhinoObjectHelper.GetRhinoObject(objectIdString);
|
||||
var attrs = rhinoObject?.Attributes.Duplicate();
|
||||
attrs?.SetUserString(RevitMappingConstants.CATEGORY_USER_STRING_KEY, categoryValue);
|
||||
RhinoDoc.ActiveDoc.Objects.ModifyAttributes(rhinoObject, attrs, true);
|
||||
switch (rhinoObject)
|
||||
{
|
||||
case null:
|
||||
continue;
|
||||
case InstanceObject instanceObj:
|
||||
{
|
||||
var defIndex = instanceObj.InstanceDefinition.Index;
|
||||
|
||||
// HashSet.Add returns true only if the index wasn't already in the set.
|
||||
// we only want to fetch and loop through siblings once per definition.
|
||||
if (processedDefinitions.Add(defIndex))
|
||||
{
|
||||
var siblingInstances = instanceObj.InstanceDefinition.GetReferences(0);
|
||||
foreach (var sibling in siblingInstances)
|
||||
{
|
||||
objectsToModify.Add(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
objectsToModify.Add(rhinoObject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger single update after all changes
|
||||
// Apply the mapping uniformly to all collected objects
|
||||
foreach (var obj in objectsToModify)
|
||||
{
|
||||
var attrs = obj.Attributes.Duplicate();
|
||||
attrs.SetUserString(RevitMappingConstants.CATEGORY_USER_STRING_KEY, categoryValue);
|
||||
RhinoDoc.ActiveDoc.Objects.ModifyAttributes(obj, attrs, true);
|
||||
}
|
||||
|
||||
// Trigger single UI update
|
||||
_idleManager.SubscribeToIdle(nameof(NotifyMappingsChanged), NotifyMappingsChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes category assignments from specific objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If block instance is selected, assignment is cleared uniformly from all instances of that block definition in the doc.
|
||||
/// </remarks>
|
||||
public void ClearObjectsCategoryAssignment(string[] objectIds)
|
||||
{
|
||||
var objectsToModify = new HashSet<RhinoObject>();
|
||||
var processedDefinitions = new HashSet<int>(); // Tracks unique block definitions
|
||||
|
||||
foreach (var objectIdString in objectIds)
|
||||
{
|
||||
var rhinoObject = _rhinoObjectHelper.GetRhinoObject(objectIdString);
|
||||
var attrs = rhinoObject?.Attributes.Duplicate();
|
||||
attrs?.DeleteUserString(RevitMappingConstants.CATEGORY_USER_STRING_KEY);
|
||||
RhinoDoc.ActiveDoc.Objects.ModifyAttributes(rhinoObject, attrs, true);
|
||||
switch (rhinoObject)
|
||||
{
|
||||
case null:
|
||||
continue;
|
||||
case InstanceObject instanceObj:
|
||||
{
|
||||
var defIndex = instanceObj.InstanceDefinition.Index;
|
||||
|
||||
// Deduplicate definition processing
|
||||
if (processedDefinitions.Add(defIndex))
|
||||
{
|
||||
var siblingInstances = instanceObj.InstanceDefinition.GetReferences(0);
|
||||
foreach (var sibling in siblingInstances)
|
||||
{
|
||||
objectsToModify.Add(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
objectsToModify.Add(rhinoObject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger single update after all changes
|
||||
// Clear the mapping from all collected objects
|
||||
foreach (var obj in objectsToModify)
|
||||
{
|
||||
var attrs = obj.Attributes.Duplicate();
|
||||
attrs.DeleteUserString(RevitMappingConstants.CATEGORY_USER_STRING_KEY);
|
||||
RhinoDoc.ActiveDoc.Objects.ModifyAttributes(obj, attrs, true);
|
||||
}
|
||||
|
||||
// Trigger single UI update
|
||||
_idleManager.SubscribeToIdle(nameof(NotifyMappingsChanged), NotifyMappingsChanged);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
|
||||
unpackResults = _instanceUnpacker.UnpackSelection(rhinoObjects);
|
||||
}
|
||||
|
||||
var (atomicObjects, instanceProxies, instanceDefinitionProxies) = unpackResults;
|
||||
var (atomicObjects, atomicDefinitionObjectIds, instanceProxies, instanceDefinitionProxies) = unpackResults;
|
||||
// POC: we should formalise this, sooner or later - or somehow fix it a bit more
|
||||
rootObjectCollection[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies; // this won't work re traversal on receive
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Speckle.Converters.Autocad;
|
||||
namespace Speckle.Converters.Autocad;
|
||||
|
||||
public record AutocadConversionSettings(Document Document, string SpeckleUnits);
|
||||
public record AutocadConversionSettings(Document Document, AG.Matrix3d? ReferencePointTransform, string SpeckleUnits);
|
||||
|
||||
+9
-3
@@ -1,4 +1,4 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.InterfaceGenerator;
|
||||
|
||||
namespace Speckle.Converters.Autocad;
|
||||
@@ -7,6 +7,12 @@ namespace Speckle.Converters.Autocad;
|
||||
public class AutocadConversionSettingsFactory(IHostToSpeckleUnitConverter<ADB.UnitsValue> unitsConverter)
|
||||
: IAutocadConversionSettingsFactory
|
||||
{
|
||||
public AutocadConversionSettings Create(Document document) =>
|
||||
new(document, unitsConverter.ConvertOrThrow(document.Database.Insunits));
|
||||
public AutocadConversionSettings Create(Document document)
|
||||
{
|
||||
AG.Matrix3d? m =
|
||||
document.Editor.CurrentUserCoordinateSystem == AG.Matrix3d.Identity
|
||||
? null
|
||||
: document.Editor.CurrentUserCoordinateSystem;
|
||||
return new(document, m, unitsConverter.ConvertOrThrow(document.Database.Insunits));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,34 +2,6 @@ namespace Speckle.Converters.Autocad.Extensions;
|
||||
|
||||
public static class ListExtensions
|
||||
{
|
||||
public static SOG.Polyline ConvertToSpecklePolyline(this List<double> pointList, string speckleUnits)
|
||||
{
|
||||
// throw if list is malformed
|
||||
if (pointList.Count % 3 != 0)
|
||||
{
|
||||
throw new ArgumentException("Point list of xyz values is malformed", nameof(pointList));
|
||||
}
|
||||
|
||||
return new() { value = pointList, units = speckleUnits };
|
||||
}
|
||||
|
||||
public static List<AG.Point2d> ConvertToPoint2d(this List<double> pointList, double conversionFactor = 1)
|
||||
{
|
||||
// throw if list is malformed
|
||||
if (pointList.Count % 2 != 0)
|
||||
{
|
||||
throw new ArgumentException("Point list of xy values is malformed", nameof(pointList));
|
||||
}
|
||||
|
||||
List<AG.Point2d> points2d = new(pointList.Count / 2);
|
||||
for (int i = 1; i < pointList.Count; i += 2)
|
||||
{
|
||||
points2d.Add(new AG.Point2d(pointList[i - 1] * conversionFactor, pointList[i] * conversionFactor));
|
||||
}
|
||||
|
||||
return points2d;
|
||||
}
|
||||
|
||||
public static List<AG.Point3d> ConvertToPoint3d(this List<double> pointList, double conversionFactor = 1)
|
||||
{
|
||||
// throw if list is malformed
|
||||
@@ -52,4 +24,18 @@ public static class ListExtensions
|
||||
|
||||
return points3d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of doubles to Point3d objects and transforms them to OCS (Object Coordinate System)
|
||||
/// based on the provided normal vector
|
||||
/// </summary>
|
||||
public static List<AG.Point3d> ConvertToPoint3dInOcs(
|
||||
this List<double> pointList,
|
||||
AG.Vector3d normal,
|
||||
double conversionFactor = 1
|
||||
)
|
||||
{
|
||||
AG.Matrix3d matrixOcs = AG.Matrix3d.WorldToPlane(normal);
|
||||
return pointList.ConvertToPoint3d(conversionFactor).Select(p => p.TransformBy(matrixOcs)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Speckle.Converters.Autocad.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for working with reference points
|
||||
/// </summary>
|
||||
public static class ReferencePointHelper
|
||||
{
|
||||
public const string REFERENCE_POINT_TRANSFORM_KEY = "referencePointTransform";
|
||||
|
||||
/// <summary>
|
||||
/// Changes Autocad Matrix3d Transform to a double array.
|
||||
/// Uses a 16-element column-major matrix representation. See https://speckle.guide/dev/objects.html
|
||||
/// </summary>
|
||||
public static Dictionary<string, object> CreateTransformDataForRootObject(AG.Matrix3d transform)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{
|
||||
"transform", // TODO: it would also be nice to include the key-value pair for reference point type as a string
|
||||
new[]
|
||||
{
|
||||
transform.CoordinateSystem3d.Xaxis.X,
|
||||
transform.CoordinateSystem3d.Xaxis.Y,
|
||||
transform.CoordinateSystem3d.Xaxis.Z,
|
||||
0,
|
||||
transform.CoordinateSystem3d.Yaxis.X,
|
||||
transform.CoordinateSystem3d.Yaxis.Y,
|
||||
transform.CoordinateSystem3d.Yaxis.Z,
|
||||
0,
|
||||
transform.CoordinateSystem3d.Zaxis.X,
|
||||
transform.CoordinateSystem3d.Zaxis.Y,
|
||||
transform.CoordinateSystem3d.Zaxis.Z,
|
||||
0,
|
||||
transform.CoordinateSystem3d.Origin.X,
|
||||
transform.CoordinateSystem3d.Origin.Y,
|
||||
transform.CoordinateSystem3d.Origin.Z,
|
||||
1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Speckle.DoubleNumerics;
|
||||
|
||||
namespace Speckle.Converters.Autocad.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for working with transforms
|
||||
/// </summary>
|
||||
public static class TransformHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an AutoCAD matrix3d to a row-dominant Speckle Matrix4x4
|
||||
/// </summary>
|
||||
/// <param name="m"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// Use for System Numerics operations, eg matrix and vector multiplication
|
||||
/// </remarks>
|
||||
public static Matrix4x4 ConvertToMatrix4x4(AG.Matrix3d m) =>
|
||||
new(
|
||||
m[0, 0],
|
||||
m[1, 0],
|
||||
m[2, 0],
|
||||
m[3, 0],
|
||||
m[0, 1],
|
||||
m[1, 1],
|
||||
m[2, 1],
|
||||
m[3, 1],
|
||||
m[0, 2],
|
||||
m[1, 2],
|
||||
m[2, 2],
|
||||
m[3, 2],
|
||||
m[0, 3],
|
||||
m[1, 3],
|
||||
m[2, 3],
|
||||
m[3, 3]
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Speckle Instances use a transform that is column-dominant, not row dominant.
|
||||
/// </summary>
|
||||
/// <param name="m"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>Use only for Speckle Instance object transforms.</remarks>
|
||||
public static Matrix4x4 ConvertToInstanceMatrix4x4(AG.Matrix3d m) =>
|
||||
new(
|
||||
m[0, 0],
|
||||
m[0, 1],
|
||||
m[0, 2],
|
||||
m[0, 3],
|
||||
m[1, 0],
|
||||
m[1, 1],
|
||||
m[1, 2],
|
||||
m[1, 3],
|
||||
m[2, 0],
|
||||
m[2, 1],
|
||||
m[2, 2],
|
||||
m[2, 3],
|
||||
m[3, 0],
|
||||
m[3, 1],
|
||||
m[3, 2],
|
||||
m[3, 3]
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get the transform matrix from an entity's OCS to the WCS
|
||||
/// </summary>
|
||||
/// <param name="normal"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// Use this method for certain properties or methods on entities that return values in OCS
|
||||
/// </remarks>
|
||||
public static AG.Matrix3d GetTransformFromOCSToWCS(AG.Vector3d normal) => AG.Matrix3d.WorldToPlane(normal);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Speckle.Converters.Autocad;
|
||||
|
||||
public interface IReferencePointConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a list of doubles representing point3ds in WCS coordinates to the current active coordinate system
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <returns></returns>
|
||||
List<double> ConvertWCSDoublesToExternalCoordinates(List<double> d);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Point in WCS coordinates to the current active coordinate system
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
/// <returns></returns>
|
||||
AG.Point3d ConvertWCSPointToExternalCoordinates(AG.Point3d p);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Vector in WCS coordinates to the current active coordinate system
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
/// <returns></returns>
|
||||
AG.Vector3d ConvertWCSVectorToExternalCoordinates(AG.Vector3d v);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an elevation in OCS coordinates to the current active coordinate system
|
||||
/// </summary>
|
||||
/// <param name="e"> elevation in OCS</param>
|
||||
/// <param name="normal">OCS plane normal in WCS</param>
|
||||
/// <returns></returns>
|
||||
double ConvertOCSElevationDoubleToExternalCoordinates(double e, AG.Vector3d normal);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Speckle.Converters.Autocad.Helpers;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.DoubleNumerics;
|
||||
|
||||
namespace Speckle.Converters.Autocad;
|
||||
|
||||
/// <summary>
|
||||
/// POC: reference point functionality needs to be revisited (we are currently baking in these transforms into all geometry using the point and vector converters, and losing the transform).
|
||||
/// This converter uses the transform from the converter settings (from the current doc)
|
||||
/// </summary>
|
||||
public class ReferencePointConverter(IConverterSettingsStore<AutocadConversionSettings> converterSettings)
|
||||
: IReferencePointConverter
|
||||
{
|
||||
public List<double> ConvertWCSDoublesToExternalCoordinates(List<double> d)
|
||||
{
|
||||
if (d.Count % 3 != 0)
|
||||
{
|
||||
throw new ArgumentException("Point list of xyz values is malformed", nameof(d));
|
||||
}
|
||||
|
||||
if (converterSettings.Current.ReferencePointTransform is AG.Matrix3d m)
|
||||
{
|
||||
Matrix4x4 transform = TransformHelper.ConvertToMatrix4x4(m.Inverse());
|
||||
|
||||
var transformed = new List<double>(d.Count);
|
||||
|
||||
for (int i = 0; i < d.Count; i += 3)
|
||||
{
|
||||
Vector3 p = Vector3.Transform(new(d[i], d[i + 1], d[i + 2]), transform);
|
||||
|
||||
transformed.Add(p.X);
|
||||
transformed.Add(p.Y);
|
||||
transformed.Add(p.Z);
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
public AG.Point3d ConvertWCSPointToExternalCoordinates(AG.Point3d p)
|
||||
{
|
||||
if (converterSettings.Current.ReferencePointTransform is AG.Matrix3d transform)
|
||||
{
|
||||
return p.TransformBy(transform.Inverse());
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
public AG.Vector3d ConvertWCSVectorToExternalCoordinates(AG.Vector3d v)
|
||||
{
|
||||
if (converterSettings.Current.ReferencePointTransform is AG.Matrix3d transform)
|
||||
{
|
||||
return v.TransformBy(transform.Inverse());
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public double ConvertOCSElevationDoubleToExternalCoordinates(double elevation, AG.Vector3d normal)
|
||||
{
|
||||
// get a point on the plane in WCS
|
||||
AG.Point3d wcsPoint = AG.Point3d.Origin + normal * elevation;
|
||||
|
||||
// transform to external coords
|
||||
AG.Point3d extPoint = ConvertWCSPointToExternalCoordinates(wcsPoint);
|
||||
AG.Vector3d extNormal = ConvertWCSVectorToExternalCoordinates(normal);
|
||||
|
||||
// calculate elevation as perpendicular distance in external coords
|
||||
return extPoint.GetAsVector().DotProduct(extNormal);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ public static class ServiceRegistration
|
||||
>();
|
||||
serviceCollection.AddMatchingInterfacesAsTransient(Assembly.GetExecutingAssembly());
|
||||
|
||||
serviceCollection.AddScoped<IReferencePointConverter, ReferencePointConverter>();
|
||||
|
||||
// add other classes
|
||||
serviceCollection.AddScoped<PropertiesExtractor>();
|
||||
serviceCollection.AddScoped<IPropertiesExtractor, PropertiesExtractor>();
|
||||
|
||||
+11
-4
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' < '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
@@ -16,6 +16,10 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)AutocadToSpeckleUnitConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\EntityExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ListExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\TransformHelper.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ReferencePointHelper.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)IReferencePointConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ReferencePointConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ServiceRegistration.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Geometry\AutocadPolycurveToHostConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Geometry\BrepToHostConverter.cs" />
|
||||
@@ -54,6 +58,7 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\HatchToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\MTextToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\RegionToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\SubDMeshToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\SurfaceToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\Solid3dToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\DBTextToSpeckleConverter.cs" />
|
||||
@@ -62,6 +67,7 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Properties\IPropertiesExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Properties\PropertiesExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Solid3dToRawEncodingConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\DBSubDMeshToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BrepToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\CircularArc2dToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\DBArcToSpeckleRawConverter.cs" />
|
||||
@@ -80,7 +86,6 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\Polyline3dToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\PolylineToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\SplineToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Geometry\SubDMeshToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoxToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\CircularArc3dToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\DBTextToSpeckleRawConverter.cs" />
|
||||
@@ -88,9 +93,11 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\LineSegment3dToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\MTextToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\PlaneToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\PointToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Point2dToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\DoublesToSpecklePolylineRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Point3dToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\DBSplineToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\VectorToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Vector3dToSpeckleRawConverter.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="$(MSBuildThisFileDirectory)ToHost\Helpers\" />
|
||||
|
||||
+30
-2
@@ -11,16 +11,19 @@ public class AutocadPolycurveToHostConverter : IToHostTopLevelConverter
|
||||
private readonly ITypedConverter<SOG.Autocad.AutocadPolycurve, ADB.Polyline> _polylineConverter;
|
||||
private readonly ITypedConverter<SOG.Autocad.AutocadPolycurve, ADB.Polyline2d> _polyline2dConverter;
|
||||
private readonly ITypedConverter<SOG.Autocad.AutocadPolycurve, ADB.Polyline3d> _polyline3dConverter;
|
||||
private readonly ITypedConverter<SOG.Polycurve, List<(ADB.Entity, Base)>> _polycurveConverter;
|
||||
|
||||
public AutocadPolycurveToHostConverter(
|
||||
ITypedConverter<SOG.Autocad.AutocadPolycurve, ADB.Polyline> polylineConverter,
|
||||
ITypedConverter<SOG.Autocad.AutocadPolycurve, ADB.Polyline2d> polyline2dConverter,
|
||||
ITypedConverter<SOG.Autocad.AutocadPolycurve, ADB.Polyline3d> polyline3dConverter
|
||||
ITypedConverter<SOG.Autocad.AutocadPolycurve, ADB.Polyline3d> polyline3dConverter,
|
||||
ITypedConverter<SOG.Polycurve, List<(ADB.Entity, Base)>> polycurveConverter
|
||||
)
|
||||
{
|
||||
_polylineConverter = polylineConverter;
|
||||
_polyline2dConverter = polyline2dConverter;
|
||||
_polyline3dConverter = polyline3dConverter;
|
||||
_polycurveConverter = polycurveConverter;
|
||||
}
|
||||
|
||||
public object Convert(Base target)
|
||||
@@ -30,7 +33,7 @@ public class AutocadPolycurveToHostConverter : IToHostTopLevelConverter
|
||||
switch (polycurve.polyType)
|
||||
{
|
||||
case SOG.Autocad.AutocadPolyType.Light:
|
||||
return _polylineConverter.Convert(polycurve);
|
||||
return Has2DValue(polycurve) ? _polycurveConverter.Convert(polycurve) : _polylineConverter.Convert(polycurve);
|
||||
|
||||
case SOG.Autocad.AutocadPolyType.Simple2d:
|
||||
case SOG.Autocad.AutocadPolyType.FitCurve2d:
|
||||
@@ -47,4 +50,29 @@ public class AutocadPolycurveToHostConverter : IToHostTopLevelConverter
|
||||
throw new ValidationException("Unknown poly type for AutocadPolycurve");
|
||||
}
|
||||
}
|
||||
|
||||
// Method for backwards compatibility: polylines from 3.10 and before had point2d values in OCS instead of point3d values in WCS/UCS
|
||||
private bool Has2DValue(SOG.Autocad.AutocadPolycurve polycurve)
|
||||
{
|
||||
int pointListCount = polycurve.value.Count;
|
||||
if (pointListCount % 3 == 0 && pointListCount % 2 != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pointListCount % 2 != 0)
|
||||
{
|
||||
throw new ValidationException(
|
||||
"Polycurve value list was deformed, could not translate into 2d or 3d coordinates."
|
||||
);
|
||||
}
|
||||
|
||||
int segmentVertexCount = polycurve.closed ? polycurve.segments.Count : polycurve.segments.Count + 1;
|
||||
if (pointListCount / 2 == segmentVertexCount)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -26,33 +26,33 @@ public class AutocadPolycurveToHostPolyline2dRawConverter
|
||||
// check for normal
|
||||
if (target.normal is not SOG.Vector normal)
|
||||
{
|
||||
throw new System.ArgumentException($"Autocad polycurve of type {target.polyType} did not have a normal");
|
||||
throw new ArgumentException($"Autocad polycurve of type {target.polyType} did not have a normal");
|
||||
}
|
||||
|
||||
// check for elevation
|
||||
if (target.elevation is not double elevation)
|
||||
{
|
||||
throw new System.ArgumentException($"Autocad polycurve of type {target.polyType} did not have an elevation");
|
||||
throw new ArgumentException($"Autocad polycurve of type {target.polyType} did not have an elevation");
|
||||
}
|
||||
|
||||
// get vertices
|
||||
// convert the normal, get vertices and transform them to ocs
|
||||
var convertedNormal = _vectorConverter.Convert(normal);
|
||||
double f = Units.GetConversionFactor(target.units, _settingsStore.Current.SpeckleUnits);
|
||||
List<AG.Point3d> points = target.value.ConvertToPoint3d(f);
|
||||
List<AG.Point3d> points = target.value.ConvertToPoint3dInOcs(convertedNormal, f);
|
||||
|
||||
// check for invalid bulges
|
||||
if (target.bulges is null || target.bulges.Count < points.Count)
|
||||
{
|
||||
throw new System.ArgumentException($"Autocad polycurve of type {target.polyType} had null or malformed bulges");
|
||||
throw new ArgumentException($"Autocad polycurve of type {target.polyType} had null or malformed bulges");
|
||||
}
|
||||
|
||||
// check for invalid tangents
|
||||
if (target.tangents is null || target.tangents.Count < points.Count)
|
||||
{
|
||||
throw new System.ArgumentException($"Autocad polycurve of type {target.polyType} had null or malformed tangents");
|
||||
throw new ArgumentException($"Autocad polycurve of type {target.polyType} had null or malformed tangents");
|
||||
}
|
||||
|
||||
// create the polyline2d using the empty constructor
|
||||
AG.Vector3d convertedNormal = _vectorConverter.Convert(normal);
|
||||
double convertedElevation = elevation * f;
|
||||
ADB.Polyline2d polyline =
|
||||
new()
|
||||
|
||||
+7
-7
@@ -24,26 +24,26 @@ public class AutocadPolycurveToHostPolylineRawConverter : ITypedConverter<SOG.Au
|
||||
{
|
||||
if (target.normal is null || target.elevation is null)
|
||||
{
|
||||
throw new System.ArgumentException(
|
||||
"Autocad polycurve of type light did not have a valid normal and/or elevation"
|
||||
);
|
||||
throw new ArgumentException("Autocad polycurve of type light did not have a valid normal and/or elevation");
|
||||
}
|
||||
|
||||
// convert the normal, get vertices and transform them to ocs
|
||||
AG.Vector3d normal = _vectorConverter.Convert(target.normal);
|
||||
double f = Units.GetConversionFactor(target.units, _settingsStore.Current.SpeckleUnits);
|
||||
List<AG.Point2d> points2d = target.value.ConvertToPoint2d(f);
|
||||
List<AG.Point3d> points3d = target.value.ConvertToPoint3dInOcs(normal, f);
|
||||
|
||||
ADB.Polyline polyline =
|
||||
new()
|
||||
{
|
||||
Normal = _vectorConverter.Convert(target.normal),
|
||||
Normal = normal,
|
||||
Elevation = (double)target.elevation * f,
|
||||
Closed = target.closed
|
||||
};
|
||||
|
||||
for (int i = 0; i < points2d.Count; i++)
|
||||
for (int i = 0; i < points3d.Count; i++)
|
||||
{
|
||||
var bulge = target.bulges is null ? 0 : target.bulges[i];
|
||||
polyline.AddVertexAt(i, points2d[i], bulge, 0, 0);
|
||||
polyline.AddVertexAt(i, new(points3d[i].X, points3d[i].Y), bulge, 0, 0);
|
||||
}
|
||||
|
||||
return polyline;
|
||||
|
||||
+8
-19
@@ -1,4 +1,3 @@
|
||||
using Autodesk.AutoCAD.Geometry;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Sdk.Models;
|
||||
@@ -14,18 +13,15 @@ namespace Speckle.Converters.Autocad.Geometry;
|
||||
[NameAndRankValue(typeof(ADB.PolyFaceMesh), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
|
||||
public class DBPolyfaceMeshToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
{
|
||||
private readonly ITypedConverter<AG.Point3d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBPolyfaceMeshToSpeckleConverter(
|
||||
ITypedConverter<AG.Point3d, SOG.Point> pointConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IReferencePointConverter referencePointConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_pointConverter = pointConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -33,7 +29,7 @@ public class DBPolyfaceMeshToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
|
||||
public SOG.Mesh RawConvert(ADB.PolyFaceMesh target)
|
||||
{
|
||||
List<Point3d> dbVertices = new();
|
||||
List<double> vertices = new();
|
||||
List<int> faces = new();
|
||||
List<int> faceVisibility = new();
|
||||
List<int> colors = new();
|
||||
@@ -45,7 +41,9 @@ public class DBPolyfaceMeshToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
switch (obj)
|
||||
{
|
||||
case ADB.PolyFaceMeshVertex o:
|
||||
dbVertices.Add(o.Position);
|
||||
vertices.Add(o.Position.X);
|
||||
vertices.Add(o.Position.Y);
|
||||
vertices.Add(o.Position.Z);
|
||||
colors.Add(o.Color.ColorValue.ToArgb());
|
||||
break;
|
||||
case ADB.FaceRecord o:
|
||||
@@ -84,22 +82,13 @@ public class DBPolyfaceMeshToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
tr.Commit();
|
||||
}
|
||||
|
||||
List<double> vertices = new(dbVertices.Count * 3);
|
||||
foreach (Point3d vert in dbVertices)
|
||||
{
|
||||
vertices.AddRange(_pointConverter.Convert(vert).ToList());
|
||||
}
|
||||
|
||||
SOG.Box bbox = _boxConverter.Convert(target.GeometricExtents);
|
||||
|
||||
SOG.Mesh speckleMesh =
|
||||
new()
|
||||
{
|
||||
vertices = vertices,
|
||||
vertices = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(vertices), // transform by reference point
|
||||
faces = faces,
|
||||
colors = colors,
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
bbox = bbox,
|
||||
["faceVisibility"] = faceVisibility
|
||||
};
|
||||
|
||||
|
||||
+22
-20
@@ -19,30 +19,30 @@ public class Polyline2dToSpeckleConverter
|
||||
: IToSpeckleTopLevelConverter,
|
||||
ITypedConverter<ADB.Polyline2d, SOG.Autocad.AutocadPolycurve>
|
||||
{
|
||||
private readonly ITypedConverter<List<double>, SOG.Polyline> _doublesConverter;
|
||||
private readonly ITypedConverter<ADB.Arc, SOG.Arc> _arcConverter;
|
||||
private readonly ITypedConverter<ADB.Line, SOG.Line> _lineConverter;
|
||||
private readonly ITypedConverter<ADB.Polyline, SOG.Autocad.AutocadPolycurve> _polylineConverter;
|
||||
private readonly ITypedConverter<ADB.Spline, SOG.Curve> _splineConverter;
|
||||
private readonly ITypedConverter<AG.Vector3d, SOG.Vector> _vectorConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public Polyline2dToSpeckleConverter(
|
||||
ITypedConverter<List<double>, SOG.Polyline> doublesConverter,
|
||||
ITypedConverter<ADB.Arc, SOG.Arc> arcConverter,
|
||||
ITypedConverter<ADB.Line, SOG.Line> lineConverter,
|
||||
ITypedConverter<ADB.Polyline, SOG.Autocad.AutocadPolycurve> polylineConverter,
|
||||
ITypedConverter<ADB.Spline, SOG.Curve> splineConverter,
|
||||
ITypedConverter<AG.Vector3d, SOG.Vector> vectorConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IReferencePointConverter referencePointConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_doublesConverter = doublesConverter;
|
||||
_arcConverter = arcConverter;
|
||||
_lineConverter = lineConverter;
|
||||
_polylineConverter = polylineConverter;
|
||||
_splineConverter = splineConverter;
|
||||
_vectorConverter = vectorConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -85,11 +85,13 @@ public class Polyline2dToSpeckleConverter
|
||||
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
ADB.Vertex2d vertex = vertices[i];
|
||||
ADB.Vertex2d vertex = vertices[i]; // this is in OCS
|
||||
|
||||
// get vertex value in the Global Coordinate System (GCS).
|
||||
// NOTE: for some reason, the z value of the position for rotated polyline2ds doesn't seem to match the exploded segment endpoint values
|
||||
value.AddRange(vertex.Position.ToArray());
|
||||
AG.Point3d vertexGCS = target.VertexPosition(vertex);
|
||||
value.Add(vertexGCS.X);
|
||||
value.Add(vertexGCS.Y);
|
||||
value.Add(vertexGCS.Z);
|
||||
|
||||
// get the bulge and tangent
|
||||
bulges.Add(vertex.Bulge);
|
||||
@@ -160,31 +162,31 @@ public class Polyline2dToSpeckleConverter
|
||||
if (isSpline)
|
||||
{
|
||||
SOG.Curve spline = _splineConverter.Convert(target.Spline);
|
||||
SOG.Polyline displayValue = segmentValues.ConvertToSpecklePolyline(_settingsStore.Current.SpeckleUnits);
|
||||
if (displayValue != null)
|
||||
{
|
||||
spline.displayValue = displayValue;
|
||||
}
|
||||
|
||||
spline.displayValue = _doublesConverter.Convert(segmentValues);
|
||||
segments.Add(spline);
|
||||
}
|
||||
|
||||
SOG.Vector normal = _vectorConverter.Convert(target.Normal);
|
||||
SOG.Box bbox = _boxConverter.Convert(target.GeometricExtents);
|
||||
SOG.Vector normal = _vectorConverter.Convert(target.Normal); // wcs
|
||||
|
||||
// get the elevation transformed by ucs
|
||||
double elevation = _referencePointConverter.ConvertOCSElevationDoubleToExternalCoordinates(
|
||||
target.Elevation,
|
||||
target.Normal
|
||||
);
|
||||
|
||||
SOG.Autocad.AutocadPolycurve polycurve =
|
||||
new()
|
||||
{
|
||||
segments = segments,
|
||||
value = value,
|
||||
value = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(value), // convert with reference point
|
||||
bulges = bulges,
|
||||
tangents = tangents,
|
||||
normal = normal,
|
||||
elevation = target.Elevation,
|
||||
elevation = elevation,
|
||||
polyType = polyType,
|
||||
closed = target.Closed,
|
||||
length = target.Length,
|
||||
area = target.Area,
|
||||
bbox = bbox,
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
|
||||
+14
-18
@@ -18,21 +18,21 @@ public class Polyline3dToSpeckleConverter
|
||||
: IToSpeckleTopLevelConverter,
|
||||
ITypedConverter<ADB.Polyline3d, SOG.Autocad.AutocadPolycurve>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Point3d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<List<double>, SOG.Polyline> _doublesConverter;
|
||||
private readonly ITypedConverter<ADB.Spline, SOG.Curve> _splineConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public Polyline3dToSpeckleConverter(
|
||||
ITypedConverter<AG.Point3d, SOG.Point> pointConverter,
|
||||
ITypedConverter<List<double>, SOG.Polyline> doublesConverter,
|
||||
ITypedConverter<ADB.Spline, SOG.Curve> splineConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IReferencePointConverter referencePointConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_pointConverter = pointConverter;
|
||||
_doublesConverter = doublesConverter;
|
||||
_splineConverter = splineConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ public class Polyline3dToSpeckleConverter
|
||||
}
|
||||
|
||||
// get all vertex data except control vertices
|
||||
List<double> value = new();
|
||||
List<ADB.PolylineVertex3d> vertices = target
|
||||
.GetSubEntities<ADB.PolylineVertex3d>(
|
||||
ADB.OpenMode.ForRead,
|
||||
@@ -64,10 +63,13 @@ public class Polyline3dToSpeckleConverter
|
||||
)
|
||||
.Where(e => e.VertexType != ADB.Vertex3dType.FitVertex) // Do not collect fit vertex points, they are not used for creation
|
||||
.ToList();
|
||||
List<double> value = new(vertices.Count * 3);
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
// vertex value is in the Global Coordinate System (GCS).
|
||||
value.AddRange(vertices[i].Position.ToArray());
|
||||
value.Add(vertices[i].Position.X);
|
||||
value.Add(vertices[i].Position.Y);
|
||||
value.Add(vertices[i].Position.Z);
|
||||
}
|
||||
|
||||
List<Objects.ICurve> segments = new();
|
||||
@@ -94,18 +96,15 @@ public class Polyline3dToSpeckleConverter
|
||||
}
|
||||
}
|
||||
|
||||
SOG.Polyline displayValue = segmentValues.ConvertToSpecklePolyline(_settingsStore.Current.SpeckleUnits);
|
||||
if (displayValue != null)
|
||||
{
|
||||
spline.displayValue = displayValue;
|
||||
}
|
||||
// set displayValue of spline
|
||||
spline.displayValue = _doublesConverter.Convert(segmentValues);
|
||||
|
||||
segments.Add(spline);
|
||||
}
|
||||
// for simple polyline3ds just get the polyline segment from the value
|
||||
else
|
||||
{
|
||||
SOG.Polyline polyline = value.ConvertToSpecklePolyline(_settingsStore.Current.SpeckleUnits);
|
||||
SOG.Polyline polyline = _doublesConverter.Convert(value);
|
||||
if (target.Closed)
|
||||
{
|
||||
polyline.closed = true;
|
||||
@@ -114,8 +113,6 @@ public class Polyline3dToSpeckleConverter
|
||||
segments.Add(polyline);
|
||||
}
|
||||
|
||||
SOG.Box bbox = _boxConverter.Convert(target.GeometricExtents);
|
||||
|
||||
SOG.Autocad.AutocadPolycurve polycurve =
|
||||
new()
|
||||
{
|
||||
@@ -123,11 +120,10 @@ public class Polyline3dToSpeckleConverter
|
||||
bulges = null,
|
||||
tangents = null,
|
||||
normal = null,
|
||||
value = value,
|
||||
value = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(value), // convert with reference point
|
||||
polyType = polyType,
|
||||
closed = target.Closed,
|
||||
length = target.Length,
|
||||
bbox = bbox,
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
|
||||
+17
-10
@@ -17,22 +17,23 @@ public class PolylineToSpeckleConverter
|
||||
{
|
||||
private readonly ITypedConverter<AG.LineSegment3d, SOG.Line> _lineConverter;
|
||||
private readonly ITypedConverter<AG.CircularArc3d, SOG.Arc> _arcConverter;
|
||||
|
||||
private readonly ITypedConverter<AG.Vector3d, SOG.Vector> _vectorConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public PolylineToSpeckleConverter(
|
||||
ITypedConverter<AG.LineSegment3d, SOG.Line> lineConverter,
|
||||
ITypedConverter<AG.CircularArc3d, SOG.Arc> arcConverter,
|
||||
ITypedConverter<AG.Vector3d, SOG.Vector> vectorConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IReferencePointConverter referencePointConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_lineConverter = lineConverter;
|
||||
_arcConverter = arcConverter;
|
||||
_vectorConverter = vectorConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -45,9 +46,11 @@ public class PolylineToSpeckleConverter
|
||||
List<Objects.ICurve> segments = new();
|
||||
for (int i = 0; i < target.NumberOfVertices; i++)
|
||||
{
|
||||
// get vertex value in the Object Coordinate System (OCS)
|
||||
AG.Point2d vertex = target.GetPoint2dAt(i);
|
||||
value.AddRange(vertex.ToArray());
|
||||
// get vertex value in the World Coordinate System (WCS)
|
||||
AG.Point3d vertex = target.GetPoint3dAt(i);
|
||||
value.Add(vertex.X);
|
||||
value.Add(vertex.Y);
|
||||
value.Add(vertex.Z);
|
||||
|
||||
// get the bulge
|
||||
bulges.Add(target.GetBulgeAt(i));
|
||||
@@ -71,22 +74,26 @@ public class PolylineToSpeckleConverter
|
||||
}
|
||||
|
||||
SOG.Vector normal = _vectorConverter.Convert(target.Normal);
|
||||
SOG.Box bbox = _boxConverter.Convert(target.GeometricExtents);
|
||||
|
||||
// get the elevation transformed by ucs
|
||||
double elevation = _referencePointConverter.ConvertOCSElevationDoubleToExternalCoordinates(
|
||||
target.Elevation,
|
||||
target.Normal
|
||||
);
|
||||
|
||||
SOG.Autocad.AutocadPolycurve polycurve =
|
||||
new()
|
||||
{
|
||||
segments = segments,
|
||||
value = value,
|
||||
value = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(value), // convert with reference point
|
||||
bulges = bulges,
|
||||
normal = normal,
|
||||
tangents = null,
|
||||
elevation = target.Elevation,
|
||||
elevation = elevation,
|
||||
polyType = SOG.Autocad.AutocadPolyType.Light,
|
||||
closed = target.Closed,
|
||||
length = target.Length,
|
||||
area = target.Area,
|
||||
bbox = bbox,
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
|
||||
+7
-72
@@ -1,85 +1,20 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Converters.Autocad.Geometry;
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Geometry;
|
||||
|
||||
[NameAndRankValue(typeof(ADB.SubDMesh), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
|
||||
public class DBSubDMeshToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
public class SubDMeshToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
{
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
private readonly ITypedConverter<ADB.SubDMesh, SOG.Mesh> _subDMeshConverter;
|
||||
|
||||
public DBSubDMeshToSpeckleConverter(IConverterSettingsStore<AutocadConversionSettings> settingsStore)
|
||||
public SubDMeshToSpeckleConverter(ITypedConverter<ADB.SubDMesh, SOG.Mesh> subDMeshConverter)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_subDMeshConverter = subDMeshConverter;
|
||||
}
|
||||
|
||||
public Base Convert(object target) => RawConvert((ADB.SubDMesh)target);
|
||||
public Base Convert(object target) => Convert((ADB.SubDMesh)target);
|
||||
|
||||
public SOG.Mesh RawConvert(ADB.SubDMesh target)
|
||||
{
|
||||
//vertices
|
||||
var vertices = new List<double>(target.Vertices.Count * 3);
|
||||
foreach (AG.Point3d vert in target.Vertices)
|
||||
{
|
||||
vertices.Add(vert.X);
|
||||
vertices.Add(vert.Y);
|
||||
vertices.Add(vert.Z);
|
||||
}
|
||||
|
||||
// faces
|
||||
var faces = new List<int>();
|
||||
int[] faceArr = target.FaceArray.ToArray(); // contains vertex indices
|
||||
int edgeCount = 0;
|
||||
for (int i = 0; i < faceArr.Length; i = i + edgeCount + 1)
|
||||
{
|
||||
List<int> faceVertices = new();
|
||||
edgeCount = faceArr[i];
|
||||
for (int j = i + 1; j <= i + edgeCount; j++)
|
||||
{
|
||||
faceVertices.Add(faceArr[j]);
|
||||
}
|
||||
|
||||
if (edgeCount == 4) // quad face
|
||||
{
|
||||
faces.AddRange(new List<int> { 4, faceVertices[0], faceVertices[1], faceVertices[2], faceVertices[3] });
|
||||
}
|
||||
else // triangle face
|
||||
{
|
||||
faces.AddRange(new List<int> { 3, faceVertices[0], faceVertices[1], faceVertices[2] });
|
||||
}
|
||||
}
|
||||
|
||||
// colors
|
||||
var colors = target
|
||||
.VertexColorArray.Select(o =>
|
||||
System
|
||||
.Drawing.Color.FromArgb(
|
||||
System.Convert.ToInt32(o.Red),
|
||||
System.Convert.ToInt32(o.Green),
|
||||
System.Convert.ToInt32(o.Blue)
|
||||
)
|
||||
.ToArgb()
|
||||
)
|
||||
.ToList();
|
||||
|
||||
SOG.Mesh speckleMesh =
|
||||
new()
|
||||
{
|
||||
vertices = vertices,
|
||||
faces = faces,
|
||||
colors = colors,
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
area = target.ComputeSurfaceArea()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
speckleMesh.volume = target.ComputeVolume();
|
||||
}
|
||||
catch (Exception e) when (!e.IsFatal()) { } // for non-volumetric meshes
|
||||
|
||||
return speckleMesh;
|
||||
}
|
||||
public SOG.Mesh Convert(ADB.SubDMesh target) => _subDMeshConverter.Convert(target);
|
||||
}
|
||||
|
||||
+7
-2
@@ -7,10 +7,15 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class BrepToSpeckleRawConverter : ITypedConverter<ABR.Brep, SOG.Mesh>
|
||||
{
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public BrepToSpeckleRawConverter(IConverterSettingsStore<AutocadConversionSettings> settingsStore)
|
||||
public BrepToSpeckleRawConverter(
|
||||
IReferencePointConverter referencePointConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_referencePointConverter = referencePointConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -65,7 +70,7 @@ public class BrepToSpeckleRawConverter : ITypedConverter<ABR.Brep, SOG.Mesh>
|
||||
new()
|
||||
{
|
||||
faces = faces,
|
||||
vertices = vertices,
|
||||
vertices = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(vertices), // transform by reference point
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
area = target.GetSurfaceArea()
|
||||
};
|
||||
|
||||
+6
-21
@@ -5,14 +5,17 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class CircularArc2dToSpeckleRawConverter : ITypedConverter<AG.CircularArc2d, SOG.Arc>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Point2d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<AG.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public CircularArc2dToSpeckleRawConverter(
|
||||
ITypedConverter<AG.Point2d, SOG.Point> pointConverter,
|
||||
ITypedConverter<AG.Plane, SOG.Plane> planeConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_pointConverter = pointConverter;
|
||||
_planeConverter = planeConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
@@ -35,27 +38,9 @@ public class CircularArc2dToSpeckleRawConverter : ITypedConverter<AG.CircularArc
|
||||
var arc = new SOG.Arc()
|
||||
{
|
||||
plane = _planeConverter.Convert(plane),
|
||||
startPoint = new()
|
||||
{
|
||||
x = target.StartPoint.X,
|
||||
y = target.StartPoint.Y,
|
||||
z = 0,
|
||||
units = units
|
||||
},
|
||||
endPoint = new()
|
||||
{
|
||||
x = target.EndPoint.X,
|
||||
y = target.EndPoint.Y,
|
||||
z = 0,
|
||||
units = units
|
||||
},
|
||||
midPoint = new()
|
||||
{
|
||||
x = midPoint.X,
|
||||
y = midPoint.Y,
|
||||
z = 0,
|
||||
units = units
|
||||
},
|
||||
startPoint = _pointConverter.Convert(target.StartPoint),
|
||||
endPoint = _pointConverter.Convert(target.EndPoint),
|
||||
midPoint = _pointConverter.Convert(midPoint),
|
||||
domain = new SOP.Interval { start = startParam, end = endParam },
|
||||
units = units
|
||||
};
|
||||
|
||||
-5
@@ -8,19 +8,16 @@ public class DBArcToSpeckleRawConverter : ITypedConverter<ADB.Arc, SOG.Arc>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Point3d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<AG.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBArcToSpeckleRawConverter(
|
||||
ITypedConverter<AG.Point3d, SOG.Point> pointConverter,
|
||||
ITypedConverter<AG.Plane, SOG.Plane> planeConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_pointConverter = pointConverter;
|
||||
_planeConverter = planeConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -33,7 +30,6 @@ public class DBArcToSpeckleRawConverter : ITypedConverter<ADB.Arc, SOG.Arc>
|
||||
SOG.Point end = _pointConverter.Convert(target.EndPoint);
|
||||
SOG.Point mid = _pointConverter.Convert(target.GetPointAtDist(target.Length / 2.0));
|
||||
SOP.Interval domain = new() { start = target.StartParam, end = target.EndParam };
|
||||
SOG.Box bbox = _boxConverter.Convert(target.GeometricExtents);
|
||||
|
||||
SOG.Arc arc =
|
||||
new()
|
||||
@@ -43,7 +39,6 @@ public class DBArcToSpeckleRawConverter : ITypedConverter<ADB.Arc, SOG.Arc>
|
||||
endPoint = end,
|
||||
midPoint = mid,
|
||||
domain = domain,
|
||||
bbox = bbox,
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
|
||||
+1
-6
@@ -7,17 +7,14 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
public class DBCircleToSpeckleRawConverter : ITypedConverter<ADB.Circle, SOG.Circle>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBCircleToSpeckleRawConverter(
|
||||
ITypedConverter<AG.Plane, SOG.Plane> planeConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_planeConverter = planeConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -26,14 +23,12 @@ public class DBCircleToSpeckleRawConverter : ITypedConverter<ADB.Circle, SOG.Cir
|
||||
public SOG.Circle Convert(ADB.Circle target)
|
||||
{
|
||||
SOG.Plane plane = _planeConverter.Convert(target.GetPlane());
|
||||
SOG.Box bbox = _boxConverter.Convert(target.GeometricExtents);
|
||||
SOG.Circle circle =
|
||||
new()
|
||||
{
|
||||
plane = plane,
|
||||
radius = target.Radius,
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
bbox = bbox
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
return circle;
|
||||
|
||||
+1
-5
@@ -1,4 +1,3 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
@@ -15,7 +14,6 @@ public class DBCurveToSpeckleRawConverter : ITypedConverter<ADB.Curve, Objects.I
|
||||
private readonly ITypedConverter<ADB.Circle, SOG.Circle> _circleConverter;
|
||||
private readonly ITypedConverter<ADB.Ellipse, SOG.Ellipse> _ellipseConverter;
|
||||
private readonly ITypedConverter<ADB.Spline, SOG.Curve> _splineConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBCurveToSpeckleRawConverter(
|
||||
ITypedConverter<ADB.Line, SOG.Line> lineConverter,
|
||||
@@ -25,8 +23,7 @@ public class DBCurveToSpeckleRawConverter : ITypedConverter<ADB.Curve, Objects.I
|
||||
ITypedConverter<ADB.Arc, SOG.Arc> arcConverter,
|
||||
ITypedConverter<ADB.Circle, SOG.Circle> circleConverter,
|
||||
ITypedConverter<ADB.Ellipse, SOG.Ellipse> ellipseConverter,
|
||||
ITypedConverter<ADB.Spline, SOG.Curve> splineConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
ITypedConverter<ADB.Spline, SOG.Curve> splineConverter
|
||||
)
|
||||
{
|
||||
_lineConverter = lineConverter;
|
||||
@@ -37,7 +34,6 @@ public class DBCurveToSpeckleRawConverter : ITypedConverter<ADB.Curve, Objects.I
|
||||
_circleConverter = circleConverter;
|
||||
_ellipseConverter = ellipseConverter;
|
||||
_splineConverter = splineConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+16
-10
@@ -6,18 +6,18 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class DBEllipseToSpeckleRawConverter : ITypedConverter<ADB.Ellipse, SOG.Ellipse>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly ITypedConverter<AG.Point3d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<AG.Vector3d, SOG.Vector> _vectorConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBEllipseToSpeckleRawConverter(
|
||||
ITypedConverter<AG.Plane, SOG.Plane> planeConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
ITypedConverter<AG.Point3d, SOG.Point> pointConverter,
|
||||
ITypedConverter<AG.Vector3d, SOG.Vector> vectorConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_planeConverter = planeConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_pointConverter = pointConverter;
|
||||
_vectorConverter = vectorConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,15 @@ public class DBEllipseToSpeckleRawConverter : ITypedConverter<ADB.Ellipse, SOG.E
|
||||
|
||||
public SOG.Ellipse Convert(ADB.Ellipse target)
|
||||
{
|
||||
SOG.Plane plane = _planeConverter.Convert(new AG.Plane(target.Center, target.MajorAxis, target.MinorAxis));
|
||||
SOG.Box bbox = _boxConverter.Convert(target.GeometricExtents);
|
||||
SOG.Plane plane =
|
||||
new()
|
||||
{
|
||||
origin = _pointConverter.Convert(target.Center),
|
||||
normal = _vectorConverter.Convert(target.Normal),
|
||||
xdir = _vectorConverter.Convert(target.MajorAxis),
|
||||
ydir = _vectorConverter.Convert(target.MinorAxis),
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
// the start and end param corresponds to start and end angle in radians
|
||||
SOP.Interval trim = new() { start = target.StartAngle, end = target.EndAngle };
|
||||
@@ -37,11 +44,10 @@ public class DBEllipseToSpeckleRawConverter : ITypedConverter<ADB.Ellipse, SOG.E
|
||||
plane = plane,
|
||||
firstRadius = target.MajorRadius,
|
||||
secondRadius = target.MinorRadius,
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
domain = new SOP.Interval { start = 0, end = Math.PI * 2 },
|
||||
trimDomain = trim,
|
||||
length = target.GetDistanceAtParameter(target.EndParam),
|
||||
bbox = bbox
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
return ellipse;
|
||||
|
||||
+1
-5
@@ -7,17 +7,14 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
public class DBLineToSpeckleRawConverter : ITypedConverter<ADB.Line, SOG.Line>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Point3d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBLineToSpeckleRawConverter(
|
||||
ITypedConverter<AG.Point3d, SOG.Point> pointConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_pointConverter = pointConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -28,8 +25,7 @@ public class DBLineToSpeckleRawConverter : ITypedConverter<ADB.Line, SOG.Line>
|
||||
{
|
||||
start = _pointConverter.Convert(target.StartPoint),
|
||||
end = _pointConverter.Convert(target.EndPoint),
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
domain = new SOP.Interval { start = 0, end = target.Length },
|
||||
bbox = _boxConverter.Convert(target.GeometricExtents)
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
}
|
||||
|
||||
+9
-7
@@ -8,18 +8,21 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class DBSplineToSpeckleRawConverter : ITypedConverter<ADB.Spline, SOG.Curve>
|
||||
{
|
||||
private readonly ITypedConverter<List<double>, SOG.Polyline> _doublesConverter;
|
||||
private readonly ITypedConverter<AG.Interval, SOP.Interval> _intervalConverter;
|
||||
private readonly ITypedConverter<ADB.Extents3d, SOG.Box> _boxConverter;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBSplineToSpeckleRawConverter(
|
||||
ITypedConverter<List<double>, SOG.Polyline> doublesConverter,
|
||||
ITypedConverter<AG.Interval, SOP.Interval> intervalConverter,
|
||||
ITypedConverter<ADB.Extents3d, SOG.Box> boxConverter,
|
||||
IReferencePointConverter referencePointConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_doublesConverter = doublesConverter;
|
||||
_intervalConverter = intervalConverter;
|
||||
_boxConverter = boxConverter;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -44,11 +47,11 @@ public class DBSplineToSpeckleRawConverter : ITypedConverter<ADB.Spline, SOG.Cur
|
||||
}
|
||||
}
|
||||
|
||||
// get points
|
||||
// get points, transformed by reference point setting
|
||||
List<Point3d> points = new();
|
||||
foreach (Point3d point in data.GetControlPoints().OfType<Point3d>())
|
||||
{
|
||||
points.Add(point);
|
||||
points.Add(_referencePointConverter.ConvertWCSPointToExternalCoordinates(point));
|
||||
}
|
||||
|
||||
// NOTE: for closed periodic splines, autocad does not track last #degree points.
|
||||
@@ -108,7 +111,6 @@ public class DBSplineToSpeckleRawConverter : ITypedConverter<ADB.Spline, SOG.Cur
|
||||
closed = periodicClosed || target.Closed,
|
||||
length = length,
|
||||
domain = domain,
|
||||
bbox = _boxConverter.Convert(target.GeometricExtents),
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
displayValue = target.Database is not null ? GetDisplayValue(target) : null!, //TODO change?
|
||||
};
|
||||
@@ -147,6 +149,6 @@ public class DBSplineToSpeckleRawConverter : ITypedConverter<ADB.Spline, SOG.Cur
|
||||
break;
|
||||
}
|
||||
|
||||
return verticesList.ConvertToSpecklePolyline(_settingsStore.Current.SpeckleUnits);
|
||||
return _doublesConverter.Convert(verticesList);
|
||||
}
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class DBSubDMeshToSpeckleRawConverter : ITypedConverter<ADB.SubDMesh, SOG.Mesh>
|
||||
{
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBSubDMeshToSpeckleRawConverter(
|
||||
IReferencePointConverter referencePointConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_referencePointConverter = referencePointConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
public SOG.Mesh Convert(ADB.SubDMesh target)
|
||||
{
|
||||
// vertices
|
||||
List<double> vertices = new(target.Vertices.Count * 3);
|
||||
foreach (AG.Point3d vert in target.Vertices)
|
||||
{
|
||||
vertices.Add(vert.X);
|
||||
vertices.Add(vert.Y);
|
||||
vertices.Add(vert.Z);
|
||||
}
|
||||
|
||||
// faces
|
||||
List<int> faces = new();
|
||||
int[] faceArr = target.FaceArray.ToArray(); // contains vertex indices
|
||||
int edgeCount = 0;
|
||||
for (int i = 0; i < faceArr.Length; i = i + edgeCount + 1)
|
||||
{
|
||||
List<int> faceVertices = new();
|
||||
edgeCount = faceArr[i];
|
||||
for (int j = i + 1; j <= i + edgeCount; j++)
|
||||
{
|
||||
faceVertices.Add(faceArr[j]);
|
||||
}
|
||||
|
||||
if (edgeCount == 4) // quad face
|
||||
{
|
||||
faces.AddRange(new List<int> { 4, faceVertices[0], faceVertices[1], faceVertices[2], faceVertices[3] });
|
||||
}
|
||||
else // triangle face
|
||||
{
|
||||
faces.AddRange(new List<int> { 3, faceVertices[0], faceVertices[1], faceVertices[2] });
|
||||
}
|
||||
}
|
||||
|
||||
// colors
|
||||
var colors = target
|
||||
.VertexColorArray.Select(o =>
|
||||
System
|
||||
.Drawing.Color.FromArgb(
|
||||
System.Convert.ToInt32(o.Red),
|
||||
System.Convert.ToInt32(o.Green),
|
||||
System.Convert.ToInt32(o.Blue)
|
||||
)
|
||||
.ToArgb()
|
||||
)
|
||||
.ToList();
|
||||
|
||||
SOG.Mesh speckleMesh =
|
||||
new()
|
||||
{
|
||||
vertices = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(vertices), // transform with reference point
|
||||
faces = faces,
|
||||
colors = colors,
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
area = target.ComputeSurfaceArea()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
speckleMesh.volume = target.ComputeVolume();
|
||||
}
|
||||
catch (Exception e) when (!e.IsFatal()) { } // for non-volumetric meshes
|
||||
|
||||
return speckleMesh;
|
||||
}
|
||||
}
|
||||
+19
-9
@@ -1,3 +1,4 @@
|
||||
using Speckle.Converters.Autocad.Helpers;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Objects.Annotation;
|
||||
@@ -7,17 +8,17 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
public class DBTextToSpeckleRawConverter : ITypedConverter<ADB.DBText, Text>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Point3d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<AG.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<AG.Vector3d, SOG.Vector> _vectorConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DBTextToSpeckleRawConverter(
|
||||
ITypedConverter<AG.Point3d, SOG.Point> pointConverter,
|
||||
ITypedConverter<AG.Plane, SOG.Plane> planeConverter,
|
||||
ITypedConverter<AG.Vector3d, SOG.Vector> vectorConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_pointConverter = pointConverter;
|
||||
_planeConverter = planeConverter;
|
||||
_vectorConverter = vectorConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -41,15 +42,24 @@ public class DBTextToSpeckleRawConverter : ITypedConverter<ADB.DBText, Text>
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
// For DBText, the following properties are stored in:
|
||||
// - Position: WCS
|
||||
// - Normal: WCS
|
||||
// - Rotation: OCS -> WCS https://help.autodesk.com/view/OARX/2020/ENU/?guid=OARX-ManagedRefGuide-Autodesk_AutoCAD_DatabaseServices_DBText_Rotation
|
||||
private SOG.Plane GetTextPlane(ADB.DBText target)
|
||||
{
|
||||
AG.Plane plane = new(target.Position, target.Normal);
|
||||
// Rotation prop is in OCS: calculate the x and y axis based in WCS
|
||||
AG.Matrix3d transform = TransformHelper.GetTransformFromOCSToWCS(target.Normal).Inverse();
|
||||
AG.Vector3d xDir = AG.Vector3d.XAxis.RotateBy(target.Rotation, target.Normal).TransformBy(transform);
|
||||
AG.Vector3d yDir = AG.Vector3d.YAxis.RotateBy(target.Rotation, target.Normal).TransformBy(transform);
|
||||
|
||||
if (target.Rotation != 0)
|
||||
return new()
|
||||
{
|
||||
plane.RotateBy(target.Rotation, target.Normal, target.Position);
|
||||
}
|
||||
|
||||
return _planeConverter.Convert(plane);
|
||||
origin = _pointConverter.Convert(target.Position),
|
||||
normal = _vectorConverter.Convert(target.Normal),
|
||||
xdir = _vectorConverter.Convert(xDir),
|
||||
ydir = _vectorConverter.Convert(yDir),
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class DoublesToSpeckleRawConverter : ITypedConverter<List<double>, SOG.Polyline>
|
||||
{
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public DoublesToSpeckleRawConverter(
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore,
|
||||
IReferencePointConverter referencePointConverter
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
}
|
||||
|
||||
public SOG.Polyline Convert(List<double> target)
|
||||
{
|
||||
// throw if list is malformed
|
||||
if (target.Count % 3 != 0)
|
||||
{
|
||||
throw new ArgumentException("Point list of xyz values is malformed", nameof(target));
|
||||
}
|
||||
|
||||
List<double> value = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(target);
|
||||
|
||||
return new() { value = value, units = _settingsStore.Current.SpeckleUnits };
|
||||
}
|
||||
}
|
||||
+21
-9
@@ -6,17 +6,17 @@ namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
public class MTextToSpeckleRawConverter : ITypedConverter<ADB.MText, SA.Text>
|
||||
{
|
||||
private readonly ITypedConverter<AG.Point3d, SOG.Point> _pointConverter;
|
||||
private readonly ITypedConverter<AG.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<AG.Vector3d, SOG.Vector> _vectorConverter;
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public MTextToSpeckleRawConverter(
|
||||
ITypedConverter<AG.Point3d, SOG.Point> pointConverter,
|
||||
ITypedConverter<AG.Plane, SOG.Plane> planeConverter,
|
||||
ITypedConverter<AG.Vector3d, SOG.Vector> vectorConverter,
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore
|
||||
)
|
||||
{
|
||||
_pointConverter = pointConverter;
|
||||
_planeConverter = planeConverter;
|
||||
_vectorConverter = vectorConverter;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
@@ -38,16 +38,28 @@ public class MTextToSpeckleRawConverter : ITypedConverter<ADB.MText, SA.Text>
|
||||
units = _settingsStore.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
// For MText, the following properties are stored in:
|
||||
// - Position: WCS
|
||||
// - Normal: WCS??
|
||||
// - Rotation: OCS -> UCS?? https://help.autodesk.com/view/OARX/2020/ENU/?guid=OARX-ManagedRefGuide-Autodesk_AutoCAD_DatabaseServices_MText_Rotation
|
||||
// "Accesses the angle between the X axis of the OCS for the normal vector of the current AutoCAD editor's UCS
|
||||
// and the projection of the MText object's direction vector onto the plane of the AutoCAD editor's current UCS."
|
||||
// - Direction: WCS
|
||||
// "Note that the direction vector need not be orthogonal to the normal vector." <- do not use FML
|
||||
private SOG.Plane GetTextPlane(ADB.MText target)
|
||||
{
|
||||
AG.Plane plane = new(target.Location, target.Normal);
|
||||
// Rotation prop is in UCS already: do NOT use vector converter or it will transform again!
|
||||
AG.Vector3d xDir = AG.Vector3d.XAxis.RotateBy(target.Rotation, target.Normal);
|
||||
AG.Vector3d yDir = AG.Vector3d.YAxis.RotateBy(target.Rotation, target.Normal);
|
||||
|
||||
if (target.Rotation != 0)
|
||||
return new()
|
||||
{
|
||||
plane.RotateBy(target.Rotation, target.Normal, target.Location);
|
||||
}
|
||||
|
||||
return _planeConverter.Convert(plane);
|
||||
origin = _pointConverter.Convert(target.Location),
|
||||
normal = _vectorConverter.Convert(target.Normal),
|
||||
xdir = new(xDir.X, xDir.Y, xDir.Z, _settingsStore.Current.SpeckleUnits),
|
||||
ydir = new(yDir.X, yDir.Y, yDir.Z, _settingsStore.Current.SpeckleUnits),
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+7
-4
@@ -23,13 +23,16 @@ public class PlaneToSpeckleRawConverter : ITypedConverter<AG.Plane, SOG.Plane>
|
||||
|
||||
public Base Convert(object target) => Convert((AG.Plane)target);
|
||||
|
||||
public SOG.Plane Convert(AG.Plane target) =>
|
||||
new()
|
||||
public SOG.Plane Convert(AG.Plane target)
|
||||
{
|
||||
AG.CoordinateSystem3d cs = target.GetCoordinateSystem(); // TODO: validate if this returns the coordinate system in GCS or already transformed
|
||||
return new()
|
||||
{
|
||||
origin = _pointConverter.Convert(target.PointOnPlane),
|
||||
normal = _vectorConverter.Convert(target.Normal),
|
||||
xdir = _vectorConverter.Convert(target.GetCoordinateSystem().Xaxis),
|
||||
ydir = _vectorConverter.Convert(target.GetCoordinateSystem().Yaxis),
|
||||
xdir = _vectorConverter.Convert(cs.Xaxis),
|
||||
ydir = _vectorConverter.Convert(cs.Yaxis),
|
||||
units = _settingsStore.Current.SpeckleUnits,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class Point2dToSpeckleRawConverter : ITypedConverter<AG.Point2d, SOG.Point>
|
||||
{
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
|
||||
public Point2dToSpeckleRawConverter(
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore,
|
||||
IReferencePointConverter referencePointConverter
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
}
|
||||
|
||||
public SOG.Point Convert(AG.Point2d target)
|
||||
{
|
||||
var extPt = _referencePointConverter.ConvertWCSDoublesToExternalCoordinates(new(3) { target.X, target.Y, 0 });
|
||||
|
||||
return new(extPt[0], extPt[1], extPt[2], _settingsStore.Current.SpeckleUnits);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class Point3dToSpeckleRawConverter : ITypedConverter<AG.Point3d, SOG.Point>
|
||||
{
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
|
||||
public Point3dToSpeckleRawConverter(
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore,
|
||||
IReferencePointConverter referencePointConverter
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
}
|
||||
|
||||
public SOG.Point Convert(AG.Point3d target)
|
||||
{
|
||||
AG.Point3d extPt = _referencePointConverter.ConvertWCSPointToExternalCoordinates(target);
|
||||
return new(extPt.X, extPt.Y, extPt.Z, _settingsStore.Current.SpeckleUnits);
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class PointToSpeckleRawConverter : ITypedConverter<AG.Point3d, SOG.Point>
|
||||
{
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public PointToSpeckleRawConverter(IConverterSettingsStore<AutocadConversionSettings> settingsStore)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
public SOG.Point Convert(AG.Point3d target) => new(target.X, target.Y, target.Z, _settingsStore.Current.SpeckleUnits);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class Vector3dToSpeckleRawConverter : ITypedConverter<AG.Vector3d, SOG.Vector>
|
||||
{
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
private readonly IReferencePointConverter _referencePointConverter;
|
||||
|
||||
public Vector3dToSpeckleRawConverter(
|
||||
IConverterSettingsStore<AutocadConversionSettings> settingsStore,
|
||||
IReferencePointConverter referencePointConverter
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_referencePointConverter = referencePointConverter;
|
||||
}
|
||||
|
||||
public SOG.Vector Convert(AG.Vector3d target)
|
||||
{
|
||||
AG.Vector3d extVector = _referencePointConverter.ConvertWCSVectorToExternalCoordinates(target);
|
||||
return new(extVector.X, extVector.Y, extVector.Z, _settingsStore.Current.SpeckleUnits);
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
|
||||
namespace Speckle.Converters.Autocad.ToSpeckle.Raw;
|
||||
|
||||
public class VectorToSpeckleRawConverter : ITypedConverter<AG.Vector3d, SOG.Vector>
|
||||
{
|
||||
private readonly IConverterSettingsStore<AutocadConversionSettings> _settingsStore;
|
||||
|
||||
public VectorToSpeckleRawConverter(IConverterSettingsStore<AutocadConversionSettings> settingsStore)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
public SOG.Vector Convert(AG.Vector3d target) =>
|
||||
new(target.X, target.Y, target.Z, _settingsStore.Current.SpeckleUnits);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user