Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 637ffbfc54 | |||
| c41c57544a | |||
| 6687383ce4 | |||
| f08d52e080 | |||
| 4dcf9910a5 | |||
| 9a61ded43e | |||
| 5acb0b80ab | |||
| ba41ceca2f | |||
| 8474088e5b | |||
| fe042d7a1c | |||
| d681c63a05 | |||
| bdc0e2b5bd | |||
| 6dc1726536 | |||
| 46198934ec | |||
| b4191d1d65 | |||
| f007e28565 | |||
| d05667dac8 | |||
| c922976bcd | |||
| c3aa44dfc2 | |||
| e1a64189c8 | |||
| b0e0669cab | |||
| b2a14e055c | |||
| 74f4525ff2 | |||
| bb57b31ae4 | |||
| d33a6ca358 |
+15
-25
@@ -6,32 +6,22 @@ using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Bindings;
|
||||
|
||||
public class NavisworksBasicConnectorBinding : IBasicConnectorBinding
|
||||
public class NavisworksBasicConnectorBinding(
|
||||
IBrowserBridge parent,
|
||||
DocumentModelStore store,
|
||||
ISpeckleApplication speckleApplication
|
||||
) : IBasicConnectorBinding
|
||||
{
|
||||
public string Name => "baseBinding";
|
||||
public IBrowserBridge Parent { get; }
|
||||
public BasicConnectorBindingCommands Commands { get; }
|
||||
public IBrowserBridge Parent { get; } = parent;
|
||||
|
||||
private readonly DocumentModelStore _store;
|
||||
private readonly ISpeckleApplication _speckleApplication;
|
||||
public BasicConnectorBindingCommands Commands { get; } = new(parent);
|
||||
|
||||
public NavisworksBasicConnectorBinding(
|
||||
IBrowserBridge parent,
|
||||
DocumentModelStore store,
|
||||
ISpeckleApplication speckleApplication
|
||||
)
|
||||
{
|
||||
Parent = parent;
|
||||
_store = store;
|
||||
_speckleApplication = speckleApplication;
|
||||
Commands = new BasicConnectorBindingCommands(parent);
|
||||
}
|
||||
public string GetSourceApplicationName() => speckleApplication.Slug;
|
||||
|
||||
public string GetSourceApplicationName() => _speckleApplication.Slug;
|
||||
public string GetSourceApplicationVersion() => speckleApplication.HostApplicationVersion;
|
||||
|
||||
public string GetSourceApplicationVersion() => _speckleApplication.HostApplicationVersion;
|
||||
|
||||
public string GetConnectorVersion() => _speckleApplication.SpeckleVersion;
|
||||
public string GetConnectorVersion() => speckleApplication.SpeckleVersion;
|
||||
|
||||
public DocumentInfo? GetDocumentInfo() =>
|
||||
NavisworksApp.ActiveDocument is null || NavisworksApp.ActiveDocument.Models.Count == 0
|
||||
@@ -42,15 +32,15 @@ public class NavisworksBasicConnectorBinding : IBasicConnectorBinding
|
||||
NavisworksApp.ActiveDocument.GetHashCode().ToString()
|
||||
);
|
||||
|
||||
public DocumentModelStore GetDocumentState() => _store;
|
||||
public DocumentModelStore GetDocumentState() => store;
|
||||
|
||||
public void AddModel(ModelCard model) => _store.AddModel(model);
|
||||
public void AddModel(ModelCard model) => store.AddModel(model);
|
||||
|
||||
public void UpdateModel(ModelCard model) => _store.UpdateModel(model);
|
||||
public void UpdateModel(ModelCard model) => store.UpdateModel(model);
|
||||
|
||||
public void RemoveModel(ModelCard model) => _store.RemoveModel(model);
|
||||
public void RemoveModel(ModelCard model) => store.RemoveModel(model);
|
||||
|
||||
public void RemoveModels(List<ModelCard> models) => _store.RemoveModels(models);
|
||||
public void RemoveModels(List<ModelCard> models) => store.RemoveModels(models);
|
||||
|
||||
public Task HighlightModel(string modelCardId) => Task.CompletedTask;
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Connectors.DUI.Bindings;
|
||||
using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Bindings;
|
||||
|
||||
|
||||
+61
-11
@@ -12,6 +12,7 @@ using Speckle.Connectors.DUI.Models;
|
||||
using Speckle.Connectors.DUI.Models.Card;
|
||||
using Speckle.Connectors.DUI.Models.Card.SendFilter;
|
||||
using Speckle.Connectors.DUI.Settings;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Sdk.Common;
|
||||
@@ -58,12 +59,12 @@ public class NavisworksSendBinding : ISendBinding
|
||||
|
||||
private static void SubscribeToNavisworksEvents() { }
|
||||
|
||||
// Do not change the behavior/scope of this class on send binding unless make sure the behavior is same. Otherwise, we might not be able to update list of saved sets.
|
||||
// WARNING: Changes to filter behavior here must match everywhere filters are used, or saved sets won't update correctly
|
||||
public List<ISendFilter> GetSendFilters() =>
|
||||
[
|
||||
new NavisworksSelectionFilter() { IsDefault = true },
|
||||
new NavisworksSavedSetsFilter(new ElementSelectionService()),
|
||||
new NavisworksSavedViewsFilter(new ElementSelectionService())
|
||||
new NavisworksSavedSetsFilter(new ConnectorElementSelectionService()),
|
||||
new NavisworksSavedViewsFilter(new ConnectorElementSelectionService())
|
||||
];
|
||||
|
||||
public List<ICardSetting> GetSendSettings() =>
|
||||
@@ -105,6 +106,7 @@ public class NavisworksSendBinding : ISendBinding
|
||||
)
|
||||
{
|
||||
var selectedPaths = modelCard.SendFilter.NotNull().RefreshObjectIds();
|
||||
|
||||
var convertHiddenElementsSetting =
|
||||
modelCard.Settings!.FirstOrDefault(s => s.Id == "convertHiddenElements")?.Value as bool? ?? false;
|
||||
var message = convertHiddenElementsSetting
|
||||
@@ -115,30 +117,78 @@ public class NavisworksSendBinding : ISendBinding
|
||||
{
|
||||
throw new SpeckleSendFilterException(message);
|
||||
}
|
||||
|
||||
onOperationProgressed.Report(new CardProgress("Getting selection...", null));
|
||||
await Task.CompletedTask;
|
||||
|
||||
var modelItems = new List<NAV.ModelItem>();
|
||||
int estimatedCapacity = selectedPaths.Count * 10;
|
||||
var modelItems = new List<NAV.ModelItem>(estimatedCapacity);
|
||||
double count = 0;
|
||||
|
||||
foreach (var path in selectedPaths)
|
||||
{
|
||||
onOperationProgressed.Report(new CardProgress("Getting selection...", count / selectedPaths.Count));
|
||||
await Task.CompletedTask;
|
||||
|
||||
var modelItem = _selectionService.GetModelItemFromPath(path);
|
||||
modelItems.AddRange(_selectionService.GetGeometryNodes(modelItem).Where(_selectionService.IsVisible));
|
||||
var hasChildren = modelItem.Children.Any();
|
||||
|
||||
if (hasChildren)
|
||||
{
|
||||
int nodesVisited = 0;
|
||||
int hiddenBranchesPruned = 0;
|
||||
const int REPORT_INTERVAL = 1000;
|
||||
|
||||
void TraverseWithProgress(NAV.ModelItem node)
|
||||
{
|
||||
nodesVisited++;
|
||||
|
||||
if (nodesVisited % REPORT_INTERVAL == 0)
|
||||
{
|
||||
onOperationProgressed.Report(
|
||||
new CardProgress(
|
||||
$"Expanding tree: {nodesVisited} visited, {modelItems.Count} with geometry, {hiddenBranchesPruned} hidden",
|
||||
null
|
||||
)
|
||||
);
|
||||
Task.Delay(1).Wait();
|
||||
}
|
||||
|
||||
if (!_selectionService.IsVisible(node))
|
||||
{
|
||||
hiddenBranchesPruned++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.HasGeometry)
|
||||
{
|
||||
modelItems.Add(node);
|
||||
}
|
||||
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
TraverseWithProgress(child);
|
||||
}
|
||||
}
|
||||
|
||||
TraverseWithProgress(modelItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (modelItem.HasGeometry && _selectionService.IsVisible(modelItem))
|
||||
{
|
||||
modelItems.Add(modelItem);
|
||||
}
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
return modelItems.Count == 0 ? throw new SpeckleSendFilterException(message) : modelItems;
|
||||
}
|
||||
|
||||
public void CancelSend(string modelCardId) => _cancellationManager.CancelOperation(modelCardId);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all outstanding send operations for the current document.
|
||||
/// This method is called when the active document changes, to ensure
|
||||
/// that any in-progress send operations are properly canceled before
|
||||
/// the new document is loaded.
|
||||
/// </summary>
|
||||
public void CancelAllSendOperations()
|
||||
{
|
||||
foreach (var modelCardId in _store.GetSenders().Select(m => m.ModelCardId))
|
||||
|
||||
+8
-5
@@ -15,7 +15,7 @@ using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Connectors.DUI.Models;
|
||||
using Speckle.Connectors.DUI.Models.Card.SendFilter;
|
||||
using Speckle.Connectors.DUI.WebView;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Constants.Registers;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Sdk.Models.GraphTraversal;
|
||||
@@ -53,9 +53,6 @@ public static class NavisworksConnectorServiceRegistration
|
||||
serviceCollection.AddScoped<NavisworksMaterialUnpacker>();
|
||||
serviceCollection.AddScoped<NavisworksColorUnpacker>();
|
||||
|
||||
// Register dual shared geometry stores for instancing pattern
|
||||
serviceCollection.AddScoped<InstanceStoreManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IAppIdleManager, NavisworksIdleManager>();
|
||||
|
||||
// Sending operations
|
||||
@@ -64,6 +61,9 @@ public static class NavisworksConnectorServiceRegistration
|
||||
serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc());
|
||||
serviceCollection.AddSingleton<IOperationProgressManager, OperationProgressManager>();
|
||||
|
||||
// Registers and caches
|
||||
serviceCollection.AddScoped<IInstanceFragmentRegistry, InstanceFragmentRegistry>();
|
||||
|
||||
// Register Intercom/interop
|
||||
serviceCollection.AddSingleton<NavisworksDocumentModelStore>();
|
||||
serviceCollection.AddSingleton<DocumentModelStore>(sp => sp.GetRequiredService<NavisworksDocumentModelStore>());
|
||||
@@ -73,6 +73,9 @@ public static class NavisworksConnectorServiceRegistration
|
||||
serviceCollection.AddScoped<ISendFilter, NavisworksSelectionFilter>();
|
||||
serviceCollection.AddScoped<ISendFilter, NavisworksSavedSetsFilter>();
|
||||
serviceCollection.AddScoped<ISendFilter, NavisworksSavedViewsFilter>();
|
||||
serviceCollection.AddScoped<IElementSelectionService, ElementSelectionService>();
|
||||
serviceCollection.AddScoped<
|
||||
Converter.Navisworks.Services.IElementSelectionService,
|
||||
ConnectorElementSelectionService
|
||||
>();
|
||||
}
|
||||
}
|
||||
|
||||
+9
-28
@@ -1,31 +1,12 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
|
||||
namespace Speckle.Connector.Navisworks.Services;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Services;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public class ElementSelectionService : IElementSelectionService
|
||||
/// <summary>
|
||||
/// Connector-specific element selection service that extends the converter's base implementation.
|
||||
/// Inherits the cached visibility checking and path resolution from the converter layer.
|
||||
/// </summary>
|
||||
public class ConnectorElementSelectionService : Converter.Navisworks.Services.ElementSelectionService
|
||||
{
|
||||
private readonly Dictionary<Guid, bool> _visibleCache = new();
|
||||
|
||||
public string GetModelItemPath(NAV.ModelItem modelItem) => ResolveModelItemToIndexPath(modelItem);
|
||||
|
||||
public NAV.ModelItem GetModelItemFromPath(string path) => ResolveIndexPathToModelItem(path);
|
||||
|
||||
public bool IsVisible(NAV.ModelItem modelItem)
|
||||
{
|
||||
var key = modelItem.InstanceGuid;
|
||||
if (_visibleCache.TryGetValue(key, out var isVisible))
|
||||
{
|
||||
return isVisible;
|
||||
}
|
||||
//same as ElementSelectionHelper.IsElementVisible
|
||||
foreach (var item in modelItem.AncestorsAndSelf)
|
||||
{
|
||||
_visibleCache[item.InstanceGuid] = !item.IsHidden;
|
||||
}
|
||||
return _visibleCache[key];
|
||||
}
|
||||
|
||||
public IEnumerable<NAV.ModelItem> GetGeometryNodes(NAV.ModelItem modelItem) => ResolveGeometryLeafNodes(modelItem);
|
||||
// This inherits all functionality from the converter's ElementSelectionService
|
||||
// including cached IsVisible, GetModelItemPath, GetModelItemFromPath, and GetGeometryNodes
|
||||
// Connector-specific extensions can be added here if needed in the future
|
||||
}
|
||||
|
||||
+13
-9
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Helpers;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Sdk;
|
||||
@@ -130,16 +130,20 @@ public class NavisworksColorUnpacker(
|
||||
var comSelection = ComBridge.ToInwOpSelection([modelItem]);
|
||||
try
|
||||
{
|
||||
var pathsCollection = comSelection.Paths();
|
||||
var paths = comSelection.Paths();
|
||||
try
|
||||
{
|
||||
foreach (ComApi.InwOaPath path in pathsCollection)
|
||||
foreach (ComApi.InwOaPath path in paths)
|
||||
{
|
||||
var fragmentsCollection = path.Fragments();
|
||||
GC.KeepAlive(path);
|
||||
|
||||
var fragments = path.Fragments();
|
||||
try
|
||||
{
|
||||
foreach (ComApi.InwOaFragment3 fragment in fragmentsCollection.OfType<ComApi.InwOaFragment3>())
|
||||
foreach (ComApi.InwOaFragment3 fragment in fragments)
|
||||
{
|
||||
GC.KeepAlive(fragment);
|
||||
|
||||
fragment.GenerateSimplePrimitives(ComApi.nwEVertexProperty.eNORMAL, primitiveChecker);
|
||||
|
||||
if (primitiveChecker.HasTriangles)
|
||||
@@ -150,9 +154,9 @@ public class NavisworksColorUnpacker(
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fragmentsCollection != null)
|
||||
if (fragments != null)
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragmentsCollection);
|
||||
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,9 +165,9 @@ public class NavisworksColorUnpacker(
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pathsCollection != null)
|
||||
if (paths != null)
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.ReleaseComObject(pathsCollection);
|
||||
System.Runtime.InteropServices.Marshal.ReleaseComObject(paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-11
@@ -120,17 +120,6 @@ public sealed class NavisworksDocumentEvents
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromDocumentModelEvents(object _)
|
||||
{
|
||||
var activeDocument = NavisworksApp.ActiveDocument;
|
||||
if (activeDocument != null)
|
||||
{
|
||||
UnsubscribeFromModelEvents(activeDocument);
|
||||
}
|
||||
|
||||
_isSubscribed = false;
|
||||
}
|
||||
|
||||
private void UnsubscribeFromModelEvents(NAV.Document document)
|
||||
{
|
||||
document.Models.CollectionChanged -= HandleDocumentModelCountChanged;
|
||||
|
||||
+7
-73
@@ -1,22 +1,18 @@
|
||||
using Autodesk.Navisworks.Api.ComApi;
|
||||
using Autodesk.Navisworks.Api.Interop.ComApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Constants;
|
||||
using Speckle.Converter.Navisworks.Helpers;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
using Speckle.Converter.Navisworks.ToSpeckle;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Objects.Other;
|
||||
using Speckle.Sdk;
|
||||
using static Speckle.Converter.Navisworks.Constants.MaterialConstants;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.HostApp;
|
||||
|
||||
public class NavisworksMaterialUnpacker(
|
||||
ILogger<NavisworksMaterialUnpacker> logger,
|
||||
IConverterSettingsStore<NavisworksConversionSettings> converterSettings,
|
||||
IElementSelectionService selectionService,
|
||||
GeometryToSpeckleConverter converter
|
||||
IElementSelectionService selectionService
|
||||
)
|
||||
{
|
||||
private static T SelectByRepresentationMode<T>(
|
||||
@@ -74,66 +70,6 @@ public class NavisworksMaterialUnpacker(
|
||||
var navisworksObjectId = selectionService.GetModelItemPath(navisworksObject);
|
||||
var finalId = mergedIds.TryGetValue(navisworksObjectId, out var mergedId) ? mergedId : navisworksObjectId;
|
||||
|
||||
string hashId = "";
|
||||
try
|
||||
{
|
||||
var item = selectionService.GetModelItemFromPath(finalId);
|
||||
var comSelection = ComApiBridge.ToInwOpSelection([item]);
|
||||
try
|
||||
{
|
||||
var paths = comSelection.Paths();
|
||||
try
|
||||
{
|
||||
if (paths.Count > 0)
|
||||
{
|
||||
var firstPath = paths.OfType<InwOaPath>().FirstOrDefault();
|
||||
if (firstPath != null)
|
||||
{
|
||||
var fragments = firstPath.Fragments();
|
||||
try
|
||||
{
|
||||
if (fragments.Count > 1)
|
||||
{
|
||||
var fragmentId = converter.GenerateFragmentId(paths);
|
||||
hashId = $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}";
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fragments != null)
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.ReleaseComObject(fragments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (paths != null)
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.ReleaseComObject(paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (comSelection != null)
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.ReleaseComObject(comSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{ // If COM interop fails during hash generation, fall back to using finalId
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"Failed to generate fragment hash ID for item {ItemId}, using finalId as fallback",
|
||||
finalId
|
||||
);
|
||||
hashId = "";
|
||||
}
|
||||
|
||||
var geometry = navisworksObject.Geometry;
|
||||
var mode = converterSettings.Current.User.VisualRepresentationMode;
|
||||
|
||||
@@ -162,7 +98,7 @@ public class NavisworksMaterialUnpacker(
|
||||
);
|
||||
|
||||
var materialName =
|
||||
$"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
|
||||
$"{DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(ColorConverter.NavisworksColorToColor(renderColor).ToArgb())}";
|
||||
|
||||
var itemCategory = navisworksObject.PropertyCategories.FindCategoryByDisplayName("Item");
|
||||
if (itemCategory != null)
|
||||
@@ -188,14 +124,14 @@ public class NavisworksMaterialUnpacker(
|
||||
|
||||
if (renderMaterialProxies.TryGetValue(renderMaterialId.ToString(), out RenderMaterialProxy? value))
|
||||
{
|
||||
value.objects.Add(!string.IsNullOrEmpty(hashId) ? hashId : finalId);
|
||||
value.objects.Add(finalId);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderMaterialProxies[renderMaterialId.ToString()] = new RenderMaterialProxy()
|
||||
{
|
||||
value = CreateRenderMaterial(materialName, renderTransparency, renderColor, renderMaterialId),
|
||||
objects = [!string.IsNullOrEmpty(hashId) ? hashId : finalId]
|
||||
objects = [finalId]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -219,9 +155,7 @@ public class NavisworksMaterialUnpacker(
|
||||
|
||||
var speckleRenderMaterial = new RenderMaterial()
|
||||
{
|
||||
name = !string.IsNullOrEmpty(name)
|
||||
? name
|
||||
: $"{MaterialConstants.DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}",
|
||||
name = !string.IsNullOrEmpty(name) ? name : $"{DEFAULT_MATERIAL_NAME_PREFIX}{Math.Abs(color.ToArgb())}",
|
||||
opacity = 1 - transparency,
|
||||
metalness = 0,
|
||||
roughness = 1,
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Connectors.DUI.Exceptions;
|
||||
using Speckle.Connectors.DUI.Exceptions;
|
||||
using Speckle.Connectors.DUI.Models.Card.SendFilter;
|
||||
using Speckle.Connectors.DUI.Utils;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Operations.Send.Filters;
|
||||
|
||||
|
||||
+5
-7
@@ -1,7 +1,7 @@
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Connectors.DUI.Exceptions;
|
||||
using Speckle.Connectors.DUI.Exceptions;
|
||||
using Speckle.Connectors.DUI.Models.Card.SendFilter;
|
||||
using Speckle.Connectors.DUI.Utils;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Operations.Send.Filters;
|
||||
|
||||
@@ -48,8 +48,6 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect
|
||||
return objectIds;
|
||||
}
|
||||
|
||||
var savedViews = NavisworksApp.ActiveDocument.SavedViewpoints;
|
||||
|
||||
foreach (var savedViewItem in SelectedItems.Select(item => ResolveSavedView(item.Id)))
|
||||
{
|
||||
// Get the visible elements in the saved view.
|
||||
@@ -82,12 +80,12 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect
|
||||
{
|
||||
var objectIds = new List<string>();
|
||||
|
||||
// THIS IS COMMENTED OUT AS IT IS LEGACY DEFENSIVE BEHAVIOUR - DISCUSSION REQUIRED
|
||||
// THIS IS COMMENTED OUT AS IT IS LEGACY DEFENSIVE BEHAVIOR - DISCUSSION REQUIRED
|
||||
// if (!savedView.ContainsVisibilityOverrides)
|
||||
// {
|
||||
// // We check this again as the view settings may have changed in the saved card.
|
||||
// // If the saved view does not contain visibility overrides, this is effectively everything in the model.
|
||||
// // This will need to be the documented behaviour.
|
||||
// // This will need to be the documented behavior.
|
||||
// throw new SpeckleSendFilterException(
|
||||
// "Saved view does not contain visibility overrides. This would effectively publish everything in the model."
|
||||
// );
|
||||
@@ -154,7 +152,7 @@ public class NavisworksSavedViewsFilter : DiscriminatedObject, ISendFilterSelect
|
||||
switch (item)
|
||||
{
|
||||
// case NAV.SavedViewpoint { ContainsVisibilityOverrides: false }:
|
||||
// Legacy defensive behaviour: skip viewpoints without visibility overrides.
|
||||
// Legacy defensive behavior: skip viewpoints without visibility overrides.
|
||||
// Essentially, send everything, or whatever the current view state for hidden elements is.
|
||||
// break;
|
||||
case NAV.SavedViewpointAnimationCut:
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using Speckle.Converter.Navisworks.Constants;
|
||||
using static Speckle.Converter.Navisworks.Constants.PathConstants;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Operations.Send.Filters;
|
||||
|
||||
@@ -15,6 +15,6 @@ public static class SavedItemHelpers
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return string.Join(PathConstants.SET_SEPARATOR, pathParts);
|
||||
return string.Join(SET_SEPARATOR, pathParts);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Constants;
|
||||
using static Speckle.Converter.Navisworks.Constants.PathConstants;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Operations.Send;
|
||||
|
||||
@@ -10,29 +10,29 @@ public static class GeometryNodeMerger
|
||||
{
|
||||
/// <summary>
|
||||
/// Groups sibling geometry nodes based on material properties for merging.
|
||||
/// Only merges nodes that share the same parent and have identical material properties.
|
||||
/// This only merges nodes that share the same parent and have identical material properties.
|
||||
/// </summary>
|
||||
/// <param name="nodes">The collection of ModelItems to process</param>
|
||||
/// <returns>Dictionary mapping parent paths (with material signature suffix) to their mergeable child nodes</returns>
|
||||
public static Dictionary<string, List<NAV.ModelItem>> GroupSiblingGeometryNodes(IReadOnlyList<NAV.ModelItem> nodes)
|
||||
{
|
||||
var selectionService = new ElementSelectionService();
|
||||
var selectionService = new ConnectorElementSelectionService();
|
||||
|
||||
// Group nameless geometry nodes by parent path and material signature
|
||||
var mergeableGroups = nodes
|
||||
.Where(node => node.HasGeometry && string.IsNullOrEmpty(node.DisplayName)) // Only anonymous geometry nodes
|
||||
.GroupBy(node =>
|
||||
{
|
||||
// Get parent path
|
||||
// Get the parent path
|
||||
var path = selectionService.GetModelItemPath(node);
|
||||
var lastSeparatorIndex = path.LastIndexOf(PathConstants.SEPARATOR);
|
||||
var lastSeparatorIndex = path.LastIndexOf(SEPARATOR);
|
||||
var parentPath = lastSeparatorIndex == -1 ? path : path[..lastSeparatorIndex];
|
||||
|
||||
// Generate material signature
|
||||
string signature = GenerateSignature(node);
|
||||
|
||||
// Combine parent path with signature
|
||||
return $"{parentPath}{PathConstants.MATERIAL_SEPARATOR}{signature}";
|
||||
return $"{parentPath}{MATERIAL_SEPARATOR}{signature}";
|
||||
})
|
||||
.Where(group => group.Count() > 1) // Only include groups with multiple children
|
||||
.ToDictionary(group => group.Key, group => group.ToList());
|
||||
@@ -95,7 +95,7 @@ public static class GeometryNodeMerger
|
||||
// Build a consistent string representation of all properties
|
||||
var hashInput = new System.Text.StringBuilder();
|
||||
|
||||
// Sort keys to ensure consistent order
|
||||
// Sort keys to ensure a consistent order
|
||||
var sortedKeys = properties.Keys.OrderBy(k => k).ToList();
|
||||
|
||||
foreach (var key in sortedKeys)
|
||||
@@ -139,7 +139,7 @@ public static class GeometryNodeMerger
|
||||
/// </summary>
|
||||
private static string GetMaterialName(NAV.ModelItem node)
|
||||
{
|
||||
// Check Item category for material name
|
||||
// Check the Item category for material name
|
||||
var itemCategory = node.PropertyCategories.FindCategoryByDisplayName("Item");
|
||||
if (itemCategory != null)
|
||||
{
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Constants;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using static Speckle.Converter.Navisworks.Constants.PathConstants;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Operations.Send;
|
||||
|
||||
@@ -59,8 +59,8 @@ public class NavisworksHierarchyBuilder
|
||||
allPaths.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
var depthA = a.Count(c => c == PathConstants.SEPARATOR);
|
||||
var depthB = b.Count(c => c == PathConstants.SEPARATOR);
|
||||
var depthA = a.Count(c => c == SEPARATOR);
|
||||
var depthB = b.Count(c => c == SEPARATOR);
|
||||
return depthB.CompareTo(depthA); // <- Sort in ascending order of path length
|
||||
}
|
||||
);
|
||||
@@ -126,7 +126,7 @@ public class NavisworksHierarchyBuilder
|
||||
|
||||
private static string GetParentPath(string path)
|
||||
{
|
||||
var idx = path.LastIndexOf(PathConstants.SEPARATOR);
|
||||
var idx = path.LastIndexOf(SEPARATOR);
|
||||
return idx == -1 ? string.Empty : path[..idx];
|
||||
}
|
||||
|
||||
|
||||
+156
-39
@@ -1,6 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Connector.Navisworks.HostApp;
|
||||
using Speckle.Connector.Navisworks.Services;
|
||||
using Speckle.Connectors.Common.Builders;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Connectors.Common.Conversion;
|
||||
@@ -14,7 +13,10 @@ using Speckle.Sdk;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
using static Speckle.Connector.Navisworks.Operations.Send.GeometryNodeMerger;
|
||||
using static Speckle.Connectors.Common.Operations.ProxyKeys;
|
||||
using static Speckle.Converter.Navisworks.Constants.InstanceConstants;
|
||||
|
||||
namespace Speckle.Connector.Navisworks.Operations.Send;
|
||||
|
||||
@@ -26,14 +28,15 @@ public class NavisworksRootObjectBuilder(
|
||||
ISdkActivityFactory activityFactory,
|
||||
NavisworksMaterialUnpacker materialUnpacker,
|
||||
NavisworksColorUnpacker colorUnpacker,
|
||||
Speckle.Converter.Navisworks.Constants.Registers.IInstanceFragmentRegistry instanceRegistry,
|
||||
IElementSelectionService elementSelectionService,
|
||||
IUiUnitsCache uiUnitsCache,
|
||||
InstanceStoreManager instanceStoreManager
|
||||
IUiUnitsCache uiUnitsCache
|
||||
) : IRootObjectBuilder<NAV.ModelItem>
|
||||
{
|
||||
#pragma warning disable CA1823
|
||||
#pragma warning restore CA1823
|
||||
private bool SkipNodeMerging { get; set; }
|
||||
|
||||
internal NavisworksConversionSettings GetCurrentSettings() => converterSettings.Current;
|
||||
private bool DisableGroupingForInstanceTesting { get; set; }
|
||||
|
||||
public async Task<RootObjectBuilderResult> Build(
|
||||
IReadOnlyList<NAV.ModelItem> navisworksModelItems,
|
||||
@@ -43,14 +46,14 @@ public class NavisworksRootObjectBuilder(
|
||||
)
|
||||
{
|
||||
#if DEBUG
|
||||
SkipNodeMerging = true;
|
||||
SkipNodeMerging = false;
|
||||
DisableGroupingForInstanceTesting = false;
|
||||
#endif
|
||||
using var activity = activityFactory.Start("Build");
|
||||
|
||||
ValidateInputs(navisworksModelItems, projectId, onOperationProgressed);
|
||||
|
||||
var rootCollection = InitializeRootCollection();
|
||||
|
||||
(Dictionary<string, Base?> convertedElements, List<SendConversionResult> conversionResults) =
|
||||
await ConvertModelItemsAsync(navisworksModelItems, projectId, onOperationProgressed, cancellationToken);
|
||||
|
||||
@@ -58,30 +61,17 @@ public class NavisworksRootObjectBuilder(
|
||||
|
||||
var groupedNodes = SkipNodeMerging ? [] : GroupSiblingGeometryNodes(navisworksModelItems);
|
||||
var finalElements = BuildFinalElements(convertedElements, groupedNodes);
|
||||
List<Base> geometryDefinitions = instanceStoreManager.GetGeometryDefinitions();
|
||||
|
||||
await AddProxiesToCollection(rootCollection, navisworksModelItems, groupedNodes);
|
||||
|
||||
var geometryDefinitionsCollection = new Collection
|
||||
{
|
||||
name = "Geometry Definitions",
|
||||
["units"] = converterSettings.Current.Derived.SpeckleUnits,
|
||||
elements = geometryDefinitions
|
||||
};
|
||||
|
||||
var mainElementsCollection = new Collection
|
||||
{
|
||||
name = rootCollection.name,
|
||||
["units"] = converterSettings.Current.Derived.SpeckleUnits,
|
||||
elements = finalElements
|
||||
};
|
||||
|
||||
rootCollection.elements = [mainElementsCollection];
|
||||
if (geometryDefinitions.Count > 0)
|
||||
{
|
||||
rootCollection.elements.Add(geometryDefinitionsCollection);
|
||||
}
|
||||
AddInstanceDefinitionsToCollection(rootCollection, ref finalElements);
|
||||
int finalInstanceProxyCount = CountInstanceProxiesRecursive(finalElements);
|
||||
logger.LogInformation(
|
||||
"Final output contains {count} InstanceProxy objects in displayValues",
|
||||
finalInstanceProxyCount
|
||||
);
|
||||
|
||||
rootCollection.elements = finalElements;
|
||||
return new RootObjectBuilderResult(rootCollection, conversionResults);
|
||||
}
|
||||
|
||||
@@ -127,16 +117,32 @@ public class NavisworksRootObjectBuilder(
|
||||
var convertedBases = new Dictionary<string, Base?>();
|
||||
int processedCount = 0;
|
||||
int totalCount = navisworksModelItems.Count;
|
||||
int instanceProxyCount = 0;
|
||||
|
||||
foreach (var item in navisworksModelItems)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var converted = ConvertNavisworksItem(item, convertedBases, projectId);
|
||||
results.Add(converted);
|
||||
|
||||
if (
|
||||
converted.Status == Status.SUCCESS
|
||||
&& convertedBases.TryGetValue(elementSelectionService.GetModelItemPath(item), out var convertedBase)
|
||||
&& convertedBase?["displayValue"] is List<Base> displayValues
|
||||
)
|
||||
{
|
||||
instanceProxyCount += displayValues.Count(dv => dv.GetType().Name == "InstanceProxy");
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
onOperationProgressed.Report(new CardProgress("Converting", (double)processedCount / totalCount));
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Converted {total} items, found {instanceProxies} InstanceProxy objects",
|
||||
totalCount,
|
||||
instanceProxyCount
|
||||
);
|
||||
return Task.FromResult((convertedBases, results));
|
||||
}
|
||||
|
||||
@@ -155,10 +161,24 @@ public class NavisworksRootObjectBuilder(
|
||||
{
|
||||
var finalElements = new List<Base>();
|
||||
var processedPaths = new HashSet<string>();
|
||||
AddGroupedElements(finalElements, convertedBases, groupedNodes, processedPaths);
|
||||
|
||||
if (!DisableGroupingForInstanceTesting)
|
||||
{
|
||||
AddGroupedElements(finalElements, convertedBases, groupedNodes, processedPaths);
|
||||
logger.LogInformation(
|
||||
"After grouping: {grouped} paths processed, {elements} elements in collection",
|
||||
processedPaths.Count,
|
||||
finalElements.Count
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Grouping disabled for instance testing");
|
||||
}
|
||||
|
||||
if (converterSettings.Current.User.PreserveModelHierarchy)
|
||||
{
|
||||
logger.LogInformation("Building hierarchy (PreserveModelHierarchy=true)");
|
||||
var hierarchyBuilder = new NavisworksHierarchyBuilder(
|
||||
convertedBases,
|
||||
rootToSpeckleConverter,
|
||||
@@ -168,7 +188,10 @@ public class NavisworksRootObjectBuilder(
|
||||
return hierarchyBuilder.BuildHierarchy();
|
||||
}
|
||||
|
||||
logger.LogInformation("Adding remaining elements (flat mode)");
|
||||
AddRemainingElements(finalElements, convertedBases, processedPaths);
|
||||
|
||||
logger.LogInformation("Final elements count: {count}", finalElements.Count);
|
||||
return finalElements;
|
||||
}
|
||||
|
||||
@@ -181,7 +204,7 @@ public class NavisworksRootObjectBuilder(
|
||||
{
|
||||
foreach (var group in groupedNodes)
|
||||
{
|
||||
var siblingBases = new List<Base>();
|
||||
var siblingBases = new List<Base>(group.Value.Count);
|
||||
foreach (var itemPath in group.Value.Select(elementSelectionService.GetModelItemPath))
|
||||
{
|
||||
processedPaths.Add(itemPath);
|
||||
@@ -236,10 +259,29 @@ public class NavisworksRootObjectBuilder(
|
||||
string cleanParentPath = ElementSelectionHelper.GetCleanPath(groupKey);
|
||||
(string name, string path) = GetElementNameAndPath(cleanParentPath);
|
||||
|
||||
int estimatedCapacity = siblingBases.Sum(b => (b["displayValue"] as List<Base>)?.Count ?? 0);
|
||||
var displayValues = new List<Base>(estimatedCapacity);
|
||||
displayValues.AddRange(
|
||||
siblingBases
|
||||
.Where(sibling => sibling["displayValue"] is List<Base>)
|
||||
.SelectMany(sibling => (List<Base>)sibling["displayValue"]!)
|
||||
);
|
||||
|
||||
var instanceProxyCount = displayValues.Count(dv => dv.GetType().Name == "InstanceProxy");
|
||||
if (instanceProxyCount > 0)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Group {groupKey} merging {siblings} siblings with {proxies} InstanceProxy objects",
|
||||
groupKey,
|
||||
siblingBases.Count,
|
||||
instanceProxyCount
|
||||
);
|
||||
}
|
||||
|
||||
return new NavisworksObject
|
||||
{
|
||||
name = name,
|
||||
displayValue = siblingBases.SelectMany(b => b["displayValue"] as List<Base> ?? []).ToList(),
|
||||
displayValue = displayValues,
|
||||
properties = siblingBases.First()["properties"] as Dictionary<string, object?> ?? [],
|
||||
units = converterSettings.Current.Derived.SpeckleUnits,
|
||||
applicationId = groupKey,
|
||||
@@ -280,25 +322,100 @@ public class NavisworksRootObjectBuilder(
|
||||
var renderMaterials = materialUnpacker.UnpackRenderMaterial(navisworksModelItems, groupedNodes);
|
||||
if (renderMaterials.Count > 0)
|
||||
{
|
||||
rootCollection[ProxyKeys.RENDER_MATERIAL] = renderMaterials;
|
||||
rootCollection[RENDER_MATERIAL] = renderMaterials;
|
||||
}
|
||||
|
||||
var colors = colorUnpacker.UnpackColor(navisworksModelItems, groupedNodes);
|
||||
if (colors.Count > 0)
|
||||
{
|
||||
rootCollection[ProxyKeys.COLOR] = colors;
|
||||
}
|
||||
|
||||
var instanceDefinitionProxies = instanceStoreManager.GetInstanceDefinitionProxies();
|
||||
|
||||
if (instanceDefinitionProxies.Count > 0)
|
||||
{
|
||||
rootCollection[ProxyKeys.INSTANCE_DEFINITION] = instanceDefinitionProxies.ToList();
|
||||
rootCollection[COLOR] = colors;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void AddInstanceDefinitionsToCollection(Collection rootCollection, ref List<Base> finalElements)
|
||||
{
|
||||
using var _ = activityFactory.Start("BuildInstanceDefinitions");
|
||||
|
||||
// Get all definition geometries from the registry
|
||||
var allDefinitions = instanceRegistry.GetAllDefinitionGeometries();
|
||||
|
||||
if (allDefinitions.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No instance definitions found - instancing may be disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Building instance structure for {count} definition groups", allDefinitions.Count);
|
||||
|
||||
if (allDefinitions.Count > 100)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Large number of definition groups ({count}) detected - this may indicate instance grouping is not working effectively",
|
||||
allDefinitions.Count
|
||||
);
|
||||
}
|
||||
|
||||
var instanceDefinitionProxies = new List<InstanceDefinitionProxy>(allDefinitions.Count);
|
||||
|
||||
int estimatedGeometryCount = allDefinitions.Sum(kvp => kvp.Value.Count);
|
||||
var allDefinitionGeometries = new List<Base>(estimatedGeometryCount);
|
||||
|
||||
foreach (var kvp in allDefinitions)
|
||||
{
|
||||
var groupKey = kvp.Key;
|
||||
var geometries = kvp.Value;
|
||||
var groupKeyHash = groupKey.ToHashString();
|
||||
|
||||
var defProxy = new InstanceDefinitionProxy
|
||||
{
|
||||
name = $"Shared Geometry {groupKeyHash}",
|
||||
objects = geometries.Select(g => g.applicationId ?? "").Where(id => !string.IsNullOrEmpty(id)).ToList(),
|
||||
applicationId = $"{DEFINITION_ID_PREFIX}{groupKeyHash}",
|
||||
maxDepth = 0
|
||||
};
|
||||
|
||||
instanceDefinitionProxies.Add(defProxy);
|
||||
allDefinitionGeometries.AddRange(geometries);
|
||||
}
|
||||
|
||||
rootCollection[INSTANCE_DEFINITION] = instanceDefinitionProxies;
|
||||
var geometryDefinitionsCollection = new Collection
|
||||
{
|
||||
name = "Geometry Definitions",
|
||||
elements = allDefinitionGeometries
|
||||
};
|
||||
|
||||
var objectCollection = new Collection { name = "", elements = finalElements };
|
||||
|
||||
finalElements = [geometryDefinitionsCollection, objectCollection];
|
||||
|
||||
logger.LogInformation(
|
||||
"Added {proxyCount} instance definition proxies and {geomCount} definition geometries",
|
||||
instanceDefinitionProxies.Count,
|
||||
allDefinitionGeometries.Count
|
||||
);
|
||||
}
|
||||
|
||||
private int CountInstanceProxiesRecursive(List<Base> elements)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var element in elements)
|
||||
{
|
||||
if (element["displayValue"] is List<Base> displayValues)
|
||||
{
|
||||
count += displayValues.Count(dv => dv.GetType().Name == "InstanceProxy");
|
||||
}
|
||||
|
||||
if (element is Collection { elements: not null } collection)
|
||||
{
|
||||
count += CountInstanceProxiesRecursive(collection.elements);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private SendConversionResult ConvertNavisworksItem(
|
||||
NAV.ModelItem navisworksItem,
|
||||
Dictionary<string, Base?> convertedBases,
|
||||
|
||||
+71
-124
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Connectors.Common.Caching;
|
||||
using Speckle.Connectors.DUI.Models.Card;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
using Speckle.InterfaceGenerator;
|
||||
@@ -8,158 +7,106 @@ using Speckle.Sdk.Common;
|
||||
namespace Speckle.Connector.Navisworks.Operations.Send.Settings;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public class ToSpeckleSettingsManagerNavisworks : IToSpeckleSettingsManagerNavisworks
|
||||
public class ToSpeckleSettingsManagerNavisworks(ISendConversionCache sendConversionCache)
|
||||
: IToSpeckleSettingsManagerNavisworks
|
||||
{
|
||||
private readonly ISendConversionCache _sendConversionCache;
|
||||
|
||||
// cache invalidation process run with ModelCardId since the settings are model specific
|
||||
// cache invalidation process run with ModelCardId since the settings are model-specific
|
||||
private readonly Dictionary<string, RepresentationMode> _visualRepresentationCache = [];
|
||||
private readonly Dictionary<string, OriginMode> _originModeCache = [];
|
||||
private readonly Dictionary<string, bool?> _convertHiddenElementsCache = [];
|
||||
private readonly Dictionary<string, bool?> _includeInternalPropertiesCache = [];
|
||||
private readonly Dictionary<string, bool?> _preserveModelHierarchyCache = [];
|
||||
private readonly Dictionary<string, bool?> _revitCategoryMappingCache = [];
|
||||
private readonly Dictionary<string, bool> _convertHiddenElementsCache = [];
|
||||
private readonly Dictionary<string, bool> _includeInternalPropertiesCache = [];
|
||||
private readonly Dictionary<string, bool> _preserveModelHierarchyCache = [];
|
||||
private readonly Dictionary<string, bool> _revitCategoryMappingCache = [];
|
||||
|
||||
public ToSpeckleSettingsManagerNavisworks(ISendConversionCache sendConversionCache)
|
||||
{
|
||||
_sendConversionCache = sendConversionCache;
|
||||
}
|
||||
|
||||
public RepresentationMode GetVisualRepresentationMode(SenderModelCard modelCard)
|
||||
/// <summary>
|
||||
/// Generic helper to get a setting value with caching and cache invalidation.
|
||||
/// </summary>
|
||||
private T GetCachedSetting<T>(
|
||||
SenderModelCard modelCard,
|
||||
string settingId,
|
||||
Dictionary<string, T> cache,
|
||||
Func<object?, T> valueExtractor,
|
||||
T defaultValue
|
||||
)
|
||||
{
|
||||
if (modelCard == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelCard));
|
||||
}
|
||||
|
||||
var representationString = modelCard.Settings?.First(s => s.Id == "visualRepresentation").Value as string;
|
||||
var settingValue = modelCard.Settings?.FirstOrDefault(s => s.Id == settingId)?.Value;
|
||||
var returnValue = settingValue != null ? valueExtractor(settingValue) : defaultValue;
|
||||
|
||||
if (
|
||||
representationString is not null
|
||||
&& VisualRepresentationSetting.VisualRepresentationMap.TryGetValue(
|
||||
representationString,
|
||||
out RepresentationMode representation
|
||||
)
|
||||
cache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue)
|
||||
&& !EqualityComparer<T>.Default.Equals(previousValue, returnValue)
|
||||
)
|
||||
{
|
||||
if (_visualRepresentationCache.TryGetValue(modelCard.ModelCardId.NotNull(), out RepresentationMode previousType))
|
||||
{
|
||||
if (previousType != representation)
|
||||
{
|
||||
EvictCacheForModelCard(modelCard);
|
||||
}
|
||||
}
|
||||
|
||||
_visualRepresentationCache[modelCard.ModelCardId.NotNull()] = representation;
|
||||
return representation;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Invalid visual representation value: {representationString}");
|
||||
}
|
||||
|
||||
public OriginMode GetOriginMode(SenderModelCard modelCard)
|
||||
{
|
||||
if (modelCard == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelCard));
|
||||
}
|
||||
|
||||
var originString = modelCard.Settings?.FirstOrDefault(s => s.Id == "originMode")?.Value as string;
|
||||
if (!OriginModeSetting.OriginModeMap.TryGetValue(originString ?? string.Empty, out var origin))
|
||||
{
|
||||
return OriginMode.ModelOrigin; // Default to ModelOrigin if not specified or invalid
|
||||
}
|
||||
|
||||
if (_originModeCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousType) && previousType != origin)
|
||||
{
|
||||
EvictCacheForModelCard(modelCard);
|
||||
}
|
||||
|
||||
_originModeCache[modelCard.ModelCardId.NotNull()] = origin;
|
||||
return origin;
|
||||
}
|
||||
|
||||
public bool GetMappingToRevitCategories(SenderModelCard modelCard)
|
||||
{
|
||||
if (modelCard == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelCard));
|
||||
}
|
||||
|
||||
var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "mappingToRevitCategories")?.Value as bool?;
|
||||
|
||||
var returnValue = value != null && value.NotNull();
|
||||
if (_revitCategoryMappingCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue))
|
||||
{
|
||||
if (previousValue != returnValue)
|
||||
{
|
||||
EvictCacheForModelCard(modelCard);
|
||||
}
|
||||
}
|
||||
|
||||
_revitCategoryMappingCache[modelCard.ModelCardId] = returnValue;
|
||||
cache[modelCard.ModelCardId.NotNull()] = returnValue;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public bool GetConvertHiddenElements(SenderModelCard modelCard)
|
||||
{
|
||||
if (modelCard == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelCard));
|
||||
}
|
||||
|
||||
var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "convertHiddenElements")?.Value as bool?;
|
||||
|
||||
var returnValue = value != null && value.NotNull();
|
||||
if (_convertHiddenElementsCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue))
|
||||
{
|
||||
if (previousValue != returnValue)
|
||||
public RepresentationMode GetVisualRepresentationMode(SenderModelCard modelCard) =>
|
||||
GetCachedSetting(
|
||||
modelCard,
|
||||
"visualRepresentation",
|
||||
_visualRepresentationCache,
|
||||
value =>
|
||||
{
|
||||
EvictCacheForModelCard(modelCard);
|
||||
}
|
||||
}
|
||||
var representationString = value as string;
|
||||
return
|
||||
representationString is not null
|
||||
&& VisualRepresentationSetting.VisualRepresentationMap.TryGetValue(
|
||||
representationString,
|
||||
out RepresentationMode representation
|
||||
)
|
||||
? representation
|
||||
: throw new ArgumentException($"Invalid visual representation value: {representationString}");
|
||||
},
|
||||
RepresentationMode.Active // default value if setting not found
|
||||
);
|
||||
|
||||
_convertHiddenElementsCache[modelCard.ModelCardId] = returnValue;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public bool GetIncludeInternalProperties([NotNull] SenderModelCard modelCard)
|
||||
{
|
||||
var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "includeInternalProperties")?.Value as bool?;
|
||||
|
||||
var returnValue = value != null && value.NotNull();
|
||||
if (_includeInternalPropertiesCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue))
|
||||
{
|
||||
if (previousValue != returnValue)
|
||||
public OriginMode GetOriginMode(SenderModelCard modelCard) =>
|
||||
GetCachedSetting(
|
||||
modelCard,
|
||||
"originMode",
|
||||
_originModeCache,
|
||||
value =>
|
||||
{
|
||||
EvictCacheForModelCard(modelCard);
|
||||
}
|
||||
}
|
||||
var originString = value as string;
|
||||
if (OriginModeSetting.OriginModeMap.TryGetValue(originString ?? string.Empty, out var origin))
|
||||
{
|
||||
return origin;
|
||||
}
|
||||
return OriginMode.ModelOrigin;
|
||||
},
|
||||
OriginMode.ModelOrigin
|
||||
);
|
||||
|
||||
_includeInternalPropertiesCache[modelCard.ModelCardId] = returnValue;
|
||||
return returnValue;
|
||||
}
|
||||
public bool GetMappingToRevitCategories(SenderModelCard modelCard) =>
|
||||
GetCachedSetting(modelCard, "mappingToRevitCategories", _revitCategoryMappingCache, value => value is true, false);
|
||||
|
||||
public bool GetPreserveModelHierarchy([NotNull] SenderModelCard modelCard)
|
||||
{
|
||||
var value = modelCard.Settings?.FirstOrDefault(s => s.Id == "preserveModelHierarchy")?.Value as bool?;
|
||||
public bool GetConvertHiddenElements(SenderModelCard modelCard) =>
|
||||
GetCachedSetting(modelCard, "convertHiddenElements", _convertHiddenElementsCache, value => value is true, false);
|
||||
|
||||
var returnValue = value != null && value.NotNull();
|
||||
if (_preserveModelHierarchyCache.TryGetValue(modelCard.ModelCardId.NotNull(), out var previousValue))
|
||||
{
|
||||
if (previousValue != returnValue)
|
||||
{
|
||||
EvictCacheForModelCard(modelCard);
|
||||
}
|
||||
}
|
||||
public bool GetIncludeInternalProperties(SenderModelCard modelCard) =>
|
||||
GetCachedSetting(
|
||||
modelCard,
|
||||
"includeInternalProperties",
|
||||
_includeInternalPropertiesCache,
|
||||
value => value is true,
|
||||
false
|
||||
);
|
||||
|
||||
_preserveModelHierarchyCache[modelCard.ModelCardId] = returnValue;
|
||||
return returnValue;
|
||||
}
|
||||
public bool GetPreserveModelHierarchy(SenderModelCard modelCard) =>
|
||||
GetCachedSetting(modelCard, "preserveModelHierarchy", _preserveModelHierarchyCache, value => value is true, false);
|
||||
|
||||
private void EvictCacheForModelCard(SenderModelCard modelCard)
|
||||
{
|
||||
var objectIds = modelCard.SendFilter != null ? modelCard.SendFilter.NotNull().SelectedObjectIds : [];
|
||||
_sendConversionCache.EvictObjects(objectIds);
|
||||
sendConversionCache.EvictObjects(objectIds);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public static class SpeckleV3Tool
|
||||
public const string RIBBON_STRINGS = "NavisworksRibbon.name";
|
||||
public const string PLUGIN_SUFFIX = ".Speckle";
|
||||
|
||||
public static Speckle.Sdk.Application App =>
|
||||
public static Sdk.Application App =>
|
||||
#if NAVIS
|
||||
HostApplications.Navisworks;
|
||||
#else
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
xmlns:dui="clr-namespace:Speckle.Connectors.DUI;assembly=Speckle.Connectors.DUI"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<wv2:CoreWebView2CreationProperties x:Key="EvergreenWebView2CreationProperties" UserDataFolder="C:\temp" />
|
||||
</UserControl.Resources>
|
||||
<DockPanel>
|
||||
<wv2:WebView2
|
||||
CreationProperties="{StaticResource EvergreenWebView2CreationProperties}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
Name="Browser" Grid.Row="0" Source="{x:Static dui:Url.Netlify}" />
|
||||
</DockPanel>
|
||||
<!-- WebView2 is created lazily -->
|
||||
<Border Name="BrowserContainer" Background="White">
|
||||
<TextBlock Name="LoadingText"
|
||||
Text="Loading Speckle..."
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="Gray" />
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
@@ -2,6 +2,8 @@ using System.Windows.Controls;
|
||||
using System.Windows.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.Wpf;
|
||||
using Speckle.Connectors.DUI;
|
||||
using Speckle.Connectors.DUI.Bindings;
|
||||
using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Connectors.Revit.Plugin;
|
||||
@@ -12,6 +14,10 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IRevitTask _revitTask;
|
||||
#pragma warning disable CA2213
|
||||
private WebView2? _browser;
|
||||
#pragma warning restore CA2213
|
||||
private bool _isInitializing;
|
||||
|
||||
public RevitControlWebView(IServiceProvider serviceProvider, IRevitTask revitTask)
|
||||
{
|
||||
@@ -19,35 +25,61 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
|
||||
_revitTask = revitTask;
|
||||
InitializeComponent();
|
||||
|
||||
Browser.CoreWebView2InitializationCompleted += (sender, args) =>
|
||||
// Delay WebView2 creation until the panel is actually visible
|
||||
// This avoids conflicts with other plugins (like pyRevit) during startup
|
||||
IsVisibleChanged += OnIsVisibleChanged;
|
||||
}
|
||||
|
||||
private void OnIsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.NewValue is true && _browser == null && !_isInitializing)
|
||||
{
|
||||
_isInitializing = true;
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, CreateWebView2);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateWebView2()
|
||||
{
|
||||
_browser = new WebView2
|
||||
{
|
||||
CreationProperties = new CoreWebView2CreationProperties { UserDataFolder = "C:\\temp" },
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = System.Windows.VerticalAlignment.Stretch,
|
||||
Source = Url.Netlify
|
||||
};
|
||||
|
||||
_browser.CoreWebView2InitializationCompleted += (sender, args) =>
|
||||
_serviceProvider
|
||||
.GetRequiredService<ITopLevelExceptionHandler>()
|
||||
.CatchUnhandled(() => OnInitialized(sender, args));
|
||||
|
||||
BrowserContainer.Child = _browser;
|
||||
}
|
||||
|
||||
public bool IsBrowserInitialized => Browser.IsInitialized;
|
||||
public bool IsBrowserInitialized => _browser?.IsInitialized ?? false;
|
||||
|
||||
public object BrowserElement => Browser;
|
||||
public object BrowserElement => _browser!;
|
||||
|
||||
public void ExecuteScript(string script)
|
||||
{
|
||||
if (!Browser.IsInitialized)
|
||||
if (_browser == null || !_browser.IsInitialized)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to execute script, Webview2 is not initialized yet.");
|
||||
}
|
||||
_revitTask.Run(() => Browser.ExecuteScriptAsync(script));
|
||||
_revitTask.Run(() => _browser.ExecuteScriptAsync(script));
|
||||
}
|
||||
|
||||
public void SendProgress(string script)
|
||||
{
|
||||
if (!Browser.IsInitialized)
|
||||
if (_browser == null || !_browser.IsInitialized)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to execute script, Webview2 is not initialized yet.");
|
||||
}
|
||||
//always invoke even on the main thread because it's better somehow
|
||||
Browser.Dispatcher.Invoke(
|
||||
_browser.Dispatcher.Invoke(
|
||||
//fire and forget
|
||||
() => Browser.ExecuteScriptAsync(script),
|
||||
() => _browser.ExecuteScriptAsync(script),
|
||||
DispatcherPriority.Background
|
||||
);
|
||||
}
|
||||
@@ -74,11 +106,18 @@ public sealed partial class RevitControlWebView : UserControl, IBrowserScriptExe
|
||||
private void SetupBinding(IBinding binding)
|
||||
{
|
||||
binding.Parent.AssociateWithBinding(binding);
|
||||
Browser.CoreWebView2.AddHostObjectToScript(binding.Name, binding.Parent);
|
||||
_browser!.CoreWebView2.AddHostObjectToScript(binding.Name, binding.Parent);
|
||||
}
|
||||
|
||||
public void ShowDevTools() => Browser.CoreWebView2.OpenDevToolsWindow();
|
||||
public void ShowDevTools() => _browser?.CoreWebView2?.OpenDevToolsWindow();
|
||||
|
||||
//https://github.com/MicrosoftEdge/WebView2Feedback/issues/2161
|
||||
public void Dispose() => Browser.Dispatcher.Invoke(() => Browser.Dispose(), DispatcherPriority.Send);
|
||||
public void Dispose()
|
||||
{
|
||||
if (_browser != null)
|
||||
{
|
||||
_browser.Dispatcher.Invoke(() => _browser.Dispose(), DispatcherPriority.Send);
|
||||
_browser = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Speckle.Connectors.Revit.Bindings;
|
||||
|
||||
internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
{
|
||||
private readonly IAppIdleManager _idleManager;
|
||||
private readonly RevitIdleManager _revitIdleManager;
|
||||
private readonly RevitContext _revitContext;
|
||||
private readonly DocumentModelStore _store;
|
||||
private readonly ICancellationManager _cancellationManager;
|
||||
@@ -38,6 +38,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
private readonly LinkedModelHandler _linkedModelHandler;
|
||||
private readonly IThreadContext _threadContext;
|
||||
private readonly ISendOperationManagerFactory _sendOperationManagerFactory;
|
||||
private bool _isDocChangedSubscribed;
|
||||
private EventHandler<Autodesk.Revit.DB.Events.DocumentChangedEventArgs>? _documentChangedHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Used internally to aggregate the changed objects' id. Note we're using a concurrent dictionary here as the expiry check method is not thread safe, and this was causing problems. See:
|
||||
@@ -48,7 +50,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
private ConcurrentHashSet<ElementId> ChangedObjectIds { get; set; } = new();
|
||||
|
||||
public RevitSendBinding(
|
||||
IAppIdleManager idleManager,
|
||||
RevitIdleManager revitIdleManager,
|
||||
RevitContext revitContext,
|
||||
DocumentModelStore store,
|
||||
ICancellationManager cancellationManager,
|
||||
@@ -66,7 +68,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
)
|
||||
: base("sendBinding", bridge)
|
||||
{
|
||||
_idleManager = idleManager;
|
||||
_revitIdleManager = revitIdleManager;
|
||||
_revitContext = revitContext;
|
||||
_store = store;
|
||||
_cancellationManager = cancellationManager;
|
||||
@@ -86,12 +88,54 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
|
||||
revitTask.Run(() =>
|
||||
{
|
||||
revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
|
||||
_topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
|
||||
// revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
|
||||
// _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
|
||||
_documentChangedHandler = (_, e) => _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
|
||||
_store.ModelCardsChanged += (_, e) => OnModelCardsChanged(e);
|
||||
_store.DocumentChanged += (_, _) => topLevelExceptionHandler.FireAndForget(async () => await OnDocumentChanged());
|
||||
});
|
||||
}
|
||||
|
||||
private void OnModelCardsChanged(ModelCardsChangedEventArgs e)
|
||||
{
|
||||
if (e.ModelCards.Count > 0 && e.ModelCards.Any(m => m.TypeDiscriminator == nameof(SenderModelCard)))
|
||||
{
|
||||
SubscribeDocChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
UnsubscribeDocChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeDocChanged()
|
||||
{
|
||||
if (_documentChangedHandler == null || _isDocChangedSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_threadContext.RunOnMain(() =>
|
||||
{
|
||||
_revitContext.UIApplication.NotNull().Application.DocumentChanged += _documentChangedHandler;
|
||||
});
|
||||
_isDocChangedSubscribed = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeDocChanged()
|
||||
{
|
||||
if (_documentChangedHandler == null || !_isDocChangedSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_threadContext.RunOnMain(() =>
|
||||
{
|
||||
_revitContext.UIApplication.NotNull().Application.DocumentChanged -= _documentChangedHandler;
|
||||
});
|
||||
_isDocChangedSubscribed = false;
|
||||
}
|
||||
|
||||
public List<ISendFilter> GetSendFilters() =>
|
||||
[
|
||||
new RevitSelectionFilter { IsDefault = true },
|
||||
@@ -276,7 +320,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
|
||||
if (addedElementIds.Count > 0)
|
||||
{
|
||||
_idleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
|
||||
_revitIdleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
|
||||
}
|
||||
|
||||
if (HaveUnitsChanged(doc))
|
||||
@@ -296,8 +340,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
_sendConversionCache.EvictObjects(unpackedObjectIds);
|
||||
}
|
||||
|
||||
_idleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
|
||||
_idleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
|
||||
_revitIdleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
|
||||
_revitIdleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
|
||||
}
|
||||
|
||||
// Keeps track of doc and current units
|
||||
|
||||
@@ -17,7 +17,7 @@ internal sealed class SelectionBinding : RevitBaseBinding, ISelectionBinding, ID
|
||||
public SelectionBinding(
|
||||
RevitContext revitContext,
|
||||
IBrowserBridge parent,
|
||||
IAppIdleManager idleManager,
|
||||
RevitIdleManager idleManager,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
IRevitTask revitTask
|
||||
)
|
||||
|
||||
+2
-1
@@ -48,11 +48,12 @@ public static class ServiceRegistration
|
||||
serviceCollection.AddSingleton<IBinding, SelectionBinding>();
|
||||
serviceCollection.AddSingleton<IBinding, RevitSendBinding>();
|
||||
serviceCollection.AddSingleton<IBinding, RevitReceiveBinding>();
|
||||
serviceCollection.AddSingleton<RevitIdleManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IBasicConnectorBinding>());
|
||||
serviceCollection.AddSingleton<IBasicConnectorBinding, BasicConnectorBindingRevit>();
|
||||
|
||||
serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
|
||||
// serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
|
||||
|
||||
// send operation and dependencies
|
||||
serviceCollection.AddScoped<SendOperation<DocumentToConvert>>();
|
||||
|
||||
@@ -17,13 +17,16 @@ namespace Speckle.Connectors.Revit.HostApp;
|
||||
internal sealed class RevitDocumentStore : DocumentModelStore
|
||||
{
|
||||
private readonly ILogger<RevitDocumentStore> _logger;
|
||||
private readonly IAppIdleManager _idleManager;
|
||||
|
||||
//private readonly IAppIdleManager _idleManager;
|
||||
private readonly RevitIdleManager _idleManager;
|
||||
private readonly RevitContext _revitContext;
|
||||
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
|
||||
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
|
||||
|
||||
public RevitDocumentStore(
|
||||
IAppIdleManager idleManager,
|
||||
//IAppIdleManager idleManager,
|
||||
RevitIdleManager idleManager,
|
||||
RevitContext revitContext,
|
||||
IJsonSerializer jsonSerializer,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
@@ -34,6 +37,7 @@ internal sealed class RevitDocumentStore : DocumentModelStore
|
||||
: base(logger, jsonSerializer)
|
||||
{
|
||||
_jsonCacheManager = jsonCacheManagerFactory.CreateForUser("ConnectorsFileData");
|
||||
//_idleManager = idleManager;
|
||||
_idleManager = idleManager;
|
||||
_revitContext = revitContext;
|
||||
_topLevelExceptionHandler = topLevelExceptionHandler;
|
||||
|
||||
@@ -50,7 +50,8 @@ public static class SupportedCategoriesUtils
|
||||
return
|
||||
#if REVIT2023_OR_GREATER
|
||||
category.BuiltInCategory != BuiltInCategory.OST_AreaSchemes
|
||||
&& category.BuiltInCategory != BuiltInCategory.OST_AreaSchemeLines;
|
||||
&& category.BuiltInCategory != BuiltInCategory.OST_AreaSchemeLines
|
||||
&& category.BuiltInCategory != BuiltInCategory.INVALID;
|
||||
#else
|
||||
category.Name != "OST_AreaSchemeLines" && category.Name != "OST_AreaSchemes";
|
||||
#endif
|
||||
|
||||
+102
-2
@@ -21,6 +21,7 @@ using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Common.Exceptions;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
using Transform = Speckle.Objects.Other.Transform;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Operations.Receive;
|
||||
@@ -38,12 +39,15 @@ public sealed class RevitHostObjectBuilder(
|
||||
IThreadContext threadContext,
|
||||
RevitToHostCacheSingleton revitToHostCacheSingleton,
|
||||
ITypedConverter<
|
||||
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix),
|
||||
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject),
|
||||
DirectShape
|
||||
> localToGlobalDirectShapeConverter,
|
||||
IReceiveConversionHandler conversionHandler
|
||||
) : IHostObjectBuilder, IDisposable
|
||||
{
|
||||
// Maps atomic object applicationId -> parent DataObject
|
||||
private readonly Dictionary<string, DataObject> _atomicObjectToParentDataObject = new();
|
||||
|
||||
public Task<HostObjectBuilderResult> Build(
|
||||
Base rootObject,
|
||||
string projectName,
|
||||
@@ -102,6 +106,9 @@ public sealed class RevitHostObjectBuilder(
|
||||
unpackedRoot.ObjectsToConvert.ToList()
|
||||
);
|
||||
|
||||
// Register DataObjects with InstanceProxy displayValues
|
||||
RegisterDataObjectsWithInstanceProxies(unpackedRoot);
|
||||
|
||||
// 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
|
||||
@@ -176,6 +183,9 @@ public sealed class RevitHostObjectBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
// Update DataObject lookup IDs
|
||||
UpdateAtomicObjectLookupWithModifiedIds(originalToModifiedIds);
|
||||
|
||||
// 2 - Bake materials (now with the updated IDs)
|
||||
if (unpackedRoot.RenderMaterialProxies != null)
|
||||
{
|
||||
@@ -234,6 +244,87 @@ public sealed class RevitHostObjectBuilder(
|
||||
return conversionResults.builderResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers DataObjects that have InstanceProxy displayValues and builds the lookup.
|
||||
/// </summary>
|
||||
private void RegisterDataObjectsWithInstanceProxies(RootObjectUnpackerResult unpackedRoot)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
|
||||
private Autodesk.Revit.DB.Transform? CalculateNewTransform(
|
||||
Autodesk.Revit.DB.Transform? receiveTransform,
|
||||
Autodesk.Revit.DB.Transform? rootTransform
|
||||
@@ -278,9 +369,17 @@ public sealed class RevitHostObjectBuilder(
|
||||
onOperationProgressed.Report(new("Converting", (double)++count / localToGlobalMaps.Count));
|
||||
if (result is DirectShapeDefinitionWrapper)
|
||||
{
|
||||
// Look up parent DataObject for this atomic object (handles InstanceProxy displayValue)
|
||||
var atomicId = localToGlobalMap.AtomicObject.applicationId;
|
||||
DataObject? parentDataObject = null;
|
||||
if (atomicId is not null)
|
||||
{
|
||||
_atomicObjectToParentDataObject.TryGetValue(atomicId, out parentDataObject);
|
||||
}
|
||||
|
||||
// direct shape creation happens here
|
||||
DirectShape directShapes = localToGlobalDirectShapeConverter.Convert(
|
||||
(localToGlobalMap.AtomicObject, localToGlobalMap.Matrix)
|
||||
(localToGlobalMap.AtomicObject, localToGlobalMap.Matrix, parentDataObject)
|
||||
);
|
||||
|
||||
bakedObjectIds.Add(directShapes.UniqueId);
|
||||
@@ -351,6 +450,7 @@ 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();
|
||||
groupManager.PurgeGroups(baseGroupName);
|
||||
materialBaker.PurgeMaterials(baseGroupName);
|
||||
}
|
||||
|
||||
+10
-36
@@ -42,11 +42,7 @@ public class RevitRootObjectBuilder(
|
||||
() => Task.FromResult(BuildSync(documentElementContexts, projectId, onOperationProgressed, ct))
|
||||
);
|
||||
|
||||
#pragma warning disable CA1506
|
||||
#pragma warning disable CA1502
|
||||
private RootObjectBuilderResult BuildSync(
|
||||
#pragma warning restore CA1506
|
||||
#pragma warning restore CA1502
|
||||
IReadOnlyList<DocumentToConvert> documentElementContexts,
|
||||
string projectId,
|
||||
IProgress<CardProgress> onOperationProgressed,
|
||||
@@ -60,9 +56,6 @@ public class RevitRootObjectBuilder(
|
||||
throw new SpeckleException("Family Environment documents are not supported.");
|
||||
}
|
||||
|
||||
// create a new send pipeline
|
||||
using var sendPipeline = new Speckle.Sdk.Pipeline.Send();
|
||||
|
||||
// init the root
|
||||
Collection rootObject =
|
||||
new() { name = converterSettings.Current.Document.PathName.Split('\\').Last().Split('.').First() };
|
||||
@@ -191,12 +184,10 @@ public class RevitRootObjectBuilder(
|
||||
// non-transformed elements can safely rely on cache
|
||||
// TODO: Potential here to transform cached objects and NOT reconvert,
|
||||
// TODO: we wont do !hasTransform here, and re-set application id before this
|
||||
bool wasCached = false;
|
||||
|
||||
if (!hasTransform && sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
|
||||
{
|
||||
// TODO: cahce hit
|
||||
converted = value;
|
||||
wasCached = true;
|
||||
cacheHitCount++;
|
||||
}
|
||||
// not in cache means we convert
|
||||
@@ -215,12 +206,6 @@ public class RevitRootObjectBuilder(
|
||||
converted.applicationId = applicationId;
|
||||
}
|
||||
|
||||
var reference = sendPipeline.Process(converted).Result; // .Wait(cancellationToken);//.ConfigureAwait(false);
|
||||
if (!wasCached)
|
||||
{
|
||||
sendConversionCache.AppendSendResult(projectId, applicationId, reference);
|
||||
}
|
||||
|
||||
var collection = sendCollectionManager.GetAndCreateObjectHostCollection(
|
||||
revitElement,
|
||||
rootObject,
|
||||
@@ -228,7 +213,7 @@ public class RevitRootObjectBuilder(
|
||||
modelDisplayName
|
||||
);
|
||||
|
||||
collection.elements.Add(reference);
|
||||
collection.elements.Add(converted);
|
||||
results.Add(new(Status.SUCCESS, applicationId, sourceType, converted));
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
@@ -269,20 +254,13 @@ public class RevitRootObjectBuilder(
|
||||
rootObject[ProxyKeys.INSTANCE_DEFINITION] = revitToSpeckleCacheSingleton.GetInstanceDefinitionProxiesForObjects(
|
||||
idsAndSubElementIds
|
||||
);
|
||||
// NOTE: i might be overdoing things in here, but tldr:
|
||||
// - all instance objects (meshes) are processed individually
|
||||
// - process their collection individually, and then attach it to the root collection
|
||||
// we could, theoretically, just process the collection as a whole (but it can be big?)
|
||||
// note/ask: do these need to go in the conversion cache? or not?
|
||||
var instanceObjects = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds);
|
||||
var instanceReferences = new Collection("revitInstancedObjects");
|
||||
foreach (var instanceObject in instanceObjects)
|
||||
{
|
||||
var referenceInstanceObject = sendPipeline.Process(instanceObject).Result;
|
||||
instanceReferences.elements.Add(referenceInstanceObject);
|
||||
}
|
||||
var instanceReferenceCollection = sendPipeline.Process(instanceReferences).Result;
|
||||
rootObject.elements.Add(instanceReferenceCollection);
|
||||
rootObject.elements.Add(
|
||||
new Collection()
|
||||
{
|
||||
elements = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds),
|
||||
name = "revitInstancedObjects"
|
||||
}
|
||||
);
|
||||
|
||||
// STEP 6: Unpack all other objects to attach to root collection
|
||||
List<Objects.Other.Camera> views = viewUnpacker.Unpack(converterSettings.Current.Document);
|
||||
@@ -301,10 +279,6 @@ public class RevitRootObjectBuilder(
|
||||
rootObject[RootKeys.REFERENCE_POINT_TRANSFORM] = transformMatrix;
|
||||
}
|
||||
|
||||
// NOTE: could be
|
||||
sendPipeline.Process(rootObject).Wait(cancellationToken);
|
||||
sendPipeline.WaitForUpload().Wait(cancellationToken);
|
||||
|
||||
return new RootObjectBuilderResult(new Collection() { name = "ignore" }, results);
|
||||
return new RootObjectBuilderResult(rootObject, results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,38 +6,77 @@ using Speckle.Sdk.Common;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Plugin;
|
||||
|
||||
public sealed class RevitIdleManager : AppIdleManager
|
||||
/// <remarks>
|
||||
/// Please do NOT try and refactor this class.
|
||||
/// Whether it's to try and generalize with the <see cref="IdleCallManager"/> class
|
||||
/// or to unnecessary try and make this class thread safe.
|
||||
/// This class is a simple singleton, targeted to a Revit's host app requirements
|
||||
/// where everything happens on the main thread, and we can avoid overly complex threading/thread-safty.
|
||||
///
|
||||
/// Previous good refactors with good intention have lead to poor debugging experiences, over-engineered threading,
|
||||
/// and low confidence in the reliability.
|
||||
/// </remarks>
|
||||
/// should be registered as singleton
|
||||
public class RevitIdleManager(RevitContext revitContext, ITopLevelExceptionHandler topLevelExceptionHandler)
|
||||
{
|
||||
private readonly UIApplication _uiApplication;
|
||||
private readonly IIdleCallManager _idleCallManager;
|
||||
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
|
||||
private readonly UIApplication _uiApplication = revitContext.UIApplication.NotNull();
|
||||
|
||||
private event EventHandler<IdlingEventArgs>? OnIdle;
|
||||
private readonly Dictionary<string, Func<Task>> _calls = new();
|
||||
private bool _hasSubscribed;
|
||||
|
||||
public RevitIdleManager(
|
||||
RevitContext revitContext,
|
||||
IIdleCallManager idleCallManager,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
IRevitTask revitTask
|
||||
)
|
||||
: base(idleCallManager)
|
||||
/// <summary>
|
||||
/// Defers the invocation of an <paramref name="action"/> until next Revit idle tick (deduped by name).
|
||||
/// The <paramref name="action"/> will be called only once.
|
||||
/// </summary>
|
||||
/// <param name="name">A key that prevents enqueuing duplicate events</param>
|
||||
/// <param name="action">The action to be invoked</param>
|
||||
/// <example>
|
||||
/// Some events in host app are triggered many times, we might get 10x per object
|
||||
/// Making this more like a deferred action, so we don't update the UI many times
|
||||
/// </example>
|
||||
/// <remarks>
|
||||
/// This function must be called on the main thread
|
||||
/// </remarks>
|
||||
public void SubscribeToIdle(string name, Action action)
|
||||
{
|
||||
_topLevelExceptionHandler = topLevelExceptionHandler;
|
||||
_uiApplication = revitContext.UIApplication.NotNull();
|
||||
_idleCallManager = idleCallManager;
|
||||
revitTask.Run(
|
||||
() => _uiApplication.Idling += (s, e) => OnIdle?.Invoke(s, e) // will be called on the main thread always and fixing the Revit exceptions on subscribing/unsubscribing Idle events
|
||||
SubscribeToIdle(
|
||||
name,
|
||||
() =>
|
||||
{
|
||||
action.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected override void AddEvent()
|
||||
/// <inheritdoc cref="SubscribeToIdle(string, Action)"/>
|
||||
public void SubscribeToIdle(string name, Func<Task> action)
|
||||
{
|
||||
_topLevelExceptionHandler.CatchUnhandled(() =>
|
||||
_calls[name] = action;
|
||||
|
||||
if (_hasSubscribed)
|
||||
{
|
||||
OnIdle += RevitAppOnIdle;
|
||||
});
|
||||
return;
|
||||
}
|
||||
_hasSubscribed = true;
|
||||
|
||||
_uiApplication.Idling += RevitAppOnIdle;
|
||||
}
|
||||
|
||||
private void RevitAppOnIdle(object? sender, IdlingEventArgs e) =>
|
||||
_idleCallManager.AppOnIdle(() => OnIdle -= RevitAppOnIdle);
|
||||
private void RevitAppOnIdle(object? sender, IdlingEventArgs e)
|
||||
{
|
||||
topLevelExceptionHandler.CatchUnhandled(() =>
|
||||
{
|
||||
foreach (KeyValuePair<string, Func<Task>> kvp in _calls)
|
||||
{
|
||||
topLevelExceptionHandler.FireAndForget(kvp.Value.Invoke);
|
||||
}
|
||||
|
||||
_calls.Clear();
|
||||
_uiApplication.Idling -= RevitAppOnIdle;
|
||||
|
||||
// setting last will delay entering re-subscription
|
||||
_hasSubscribed = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,9 +54,9 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\DetailLevelSetting.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\IRevitPlugin.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCommand.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitTask.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitExternalApplication.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitThreadContext.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
|
||||
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Grasshopper.Kernel;
|
||||
using Grasshopper.Kernel.Types;
|
||||
using Speckle.Connectors.GrasshopperShared.HostApp;
|
||||
using Speckle.Connectors.GrasshopperShared.Parameters;
|
||||
using Speckle.Connectors.GrasshopperShared.Properties;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
|
||||
namespace Speckle.Connectors.GrasshopperShared.Components.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Creates collections by matching name tree structure to elements tree structure.
|
||||
/// Each branch in the names tree corresponds to the same-path branch in the elements tree.
|
||||
/// </summary>
|
||||
[Guid("7E8F9A1B-2C3D-4E5F-6A7B-8C9D0E1F2A3B")]
|
||||
public class CollectionsByName : GH_Component
|
||||
{
|
||||
public override Guid ComponentGuid => GetType().GUID;
|
||||
protected override Bitmap Icon => Resources.speckle_collections_create; // TODO: Update to specific icon if available
|
||||
public override GH_Exposure Exposure => GH_Exposure.primary;
|
||||
|
||||
public CollectionsByName()
|
||||
: base(
|
||||
"Collections by Name",
|
||||
"CbN",
|
||||
"Creates collections by matching name tree structure to objects tree structure. Each branch in the names tree corresponds to the same-path branch in the objects tree.",
|
||||
ComponentCategories.PRIMARY_RIBBON,
|
||||
ComponentCategories.COLLECTIONS
|
||||
) { }
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
{
|
||||
pManager.AddTextParameter(
|
||||
"Names",
|
||||
"N",
|
||||
"Collection names (tree structure must match Objects tree structure)",
|
||||
GH_ParamAccess.tree
|
||||
);
|
||||
|
||||
pManager.AddGenericParameter(
|
||||
"Objects",
|
||||
"O",
|
||||
"Objects to group into collections (tree structure must match Names tree structure)",
|
||||
GH_ParamAccess.tree
|
||||
);
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
|
||||
pManager.AddParameter(
|
||||
new SpeckleCollectionParam(),
|
||||
"Collection",
|
||||
"C",
|
||||
"Root collection containing named sub-collections",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
{
|
||||
// access the tree data directly from parameters
|
||||
var namesParam = Params.Input[0];
|
||||
var elementsParam = Params.Input[1];
|
||||
|
||||
var namesTree = namesParam.VolatileData;
|
||||
var elementsTree = elementsParam.VolatileData;
|
||||
|
||||
// validate that both inputs have data
|
||||
if (namesTree.IsEmpty)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Names tree is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (elementsTree.IsEmpty)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Objects tree is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// validate tree structures match exactly
|
||||
if (!TreeStructuresMatch(namesTree, elementsTree))
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Tree structures and topologies must match exactly");
|
||||
return;
|
||||
}
|
||||
|
||||
// create root collection
|
||||
var rootCollection = CollectionHelpers.CreateRootCollection(InstanceGuid.ToString());
|
||||
|
||||
// process each path
|
||||
foreach (var path in namesTree.Paths)
|
||||
{
|
||||
var nameBranch = namesTree.get_Branch(path);
|
||||
var elementsBranch = elementsTree.get_Branch(path);
|
||||
|
||||
// validate name branch - throw if empty
|
||||
if (nameBranch.Count == 0)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Name branch at path {path} is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// validate name branch - just warn if multiple, use first
|
||||
if (nameBranch.Count > 1)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Warning,
|
||||
$"Name branch at path {path} has {nameBranch.Count} names - using first name only"
|
||||
);
|
||||
}
|
||||
|
||||
// get the collection name
|
||||
string collectionName = GetCollectionName(nameBranch[0]);
|
||||
if (string.IsNullOrWhiteSpace(collectionName))
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Invalid collection name at path {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
// skip empty element branches with warning
|
||||
if (elementsBranch.Count == 0)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Skipping empty elements branch at path {path}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// parse nested collection path (e.g. parent::child)
|
||||
var collectionNames = collectionName.Split(
|
||||
new[] { Constants.LAYER_PATH_DELIMITER },
|
||||
StringSplitOptions.RemoveEmptyEntries
|
||||
);
|
||||
|
||||
// create or get nested collection structure
|
||||
var targetCollection = GetOrCreateNestedCollection(rootCollection, collectionNames, elementsBranch, path);
|
||||
|
||||
// add elements to deepest collection
|
||||
AddElementsToCollection(targetCollection, elementsBranch, path);
|
||||
}
|
||||
|
||||
// validate collection has content
|
||||
if (rootCollection.Elements.Count == 0)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
"Collection contains no valid geometry. All branches were empty or contained unsupported types."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// validate for duplicate application IDs (following CreateCollection pattern)
|
||||
if (CollectionHelpers.HasDuplicateApplicationIds(rootCollection))
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
|
||||
return;
|
||||
}
|
||||
|
||||
da.SetData(0, new SpeckleCollectionWrapperGoo(rootCollection));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that two tree structures have exactly matching paths
|
||||
/// </summary>
|
||||
private bool TreeStructuresMatch(
|
||||
Grasshopper.Kernel.Data.IGH_Structure namesTree,
|
||||
Grasshopper.Kernel.Data.IGH_Structure elementsTree
|
||||
)
|
||||
{
|
||||
if (namesTree.PathCount != elementsTree.PathCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// check that all paths match exactly
|
||||
var namePaths = namesTree.Paths.ToList();
|
||||
var elementPaths = elementsTree.Paths.ToList();
|
||||
|
||||
for (int i = 0; i < namePaths.Count; i++)
|
||||
{
|
||||
if (namePaths[i].CompareTo(elementPaths[i]) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts collection name, handling GH_String and other text types
|
||||
/// </summary>
|
||||
private string GetCollectionName(object nameObj) =>
|
||||
nameObj switch
|
||||
{
|
||||
GH_String ghString => ghString.Value,
|
||||
IGH_Goo goo => goo.ToString(),
|
||||
_ => nameObj.ToString()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates a nested collection structure based on the collection names.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Handles paths like "parent::child::grandchild" by creating intermediate collections.
|
||||
/// </remarks>
|
||||
private SpeckleCollectionWrapper GetOrCreateNestedCollection(
|
||||
SpeckleCollectionWrapper rootCollection,
|
||||
string[] collectionNames,
|
||||
System.Collections.IList elementsBranch,
|
||||
Grasshopper.Kernel.Data.GH_Path path
|
||||
)
|
||||
{
|
||||
SpeckleCollectionWrapper currentCollection = rootCollection;
|
||||
var currentPath = new List<string>(rootCollection.Path);
|
||||
|
||||
foreach (var collectionName in collectionNames)
|
||||
{
|
||||
// build path for this level
|
||||
currentPath.Add(collectionName);
|
||||
|
||||
// check if child collection already exists
|
||||
var existingChild = currentCollection
|
||||
.Elements.OfType<SpeckleCollectionWrapper>()
|
||||
.FirstOrDefault(c => c.Name == collectionName);
|
||||
|
||||
if (existingChild != null)
|
||||
{
|
||||
// use existing collection
|
||||
currentCollection = existingChild;
|
||||
}
|
||||
else
|
||||
{
|
||||
// create new child collection
|
||||
var newChild = new SpeckleCollectionWrapper
|
||||
{
|
||||
Base = new Collection(),
|
||||
Name = collectionName,
|
||||
Path = currentPath.ToList(),
|
||||
Color = null,
|
||||
Material = null,
|
||||
Topology = null, // only set topology on leaf collections
|
||||
ApplicationId = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
currentCollection.Elements.Add(newChild);
|
||||
currentCollection = newChild;
|
||||
}
|
||||
}
|
||||
|
||||
// set topology on the final (leaf) collection
|
||||
currentCollection.Topology = GetBranchTopology(path, elementsBranch.Count);
|
||||
|
||||
return currentCollection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds elements from a branch to the target collection
|
||||
/// </summary>
|
||||
private void AddElementsToCollection(
|
||||
SpeckleCollectionWrapper targetCollection,
|
||||
System.Collections.IList elementsBranch,
|
||||
Grasshopper.Kernel.Data.GH_Path path
|
||||
)
|
||||
{
|
||||
foreach (var item in elementsBranch)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
// preserve nulls for topology (CNX-2855 pattern)
|
||||
targetCollection.Elements.Add(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
// convert to SpeckleWrapper if possible - cast to IGH_Goo first
|
||||
if (item is not IGH_Goo goo)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Warning,
|
||||
$"Unsupported object type in branch {path}: {item.GetType().Name}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var wrapper = goo.ToSpeckleObjectWrapper();
|
||||
if (wrapper == null)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Warning,
|
||||
$"Unsupported object type in branch {path}: {item.GetType().Name}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wrapper is ISpeckleCollectionObject collectionObject)
|
||||
{
|
||||
targetCollection.Elements.Add(collectionObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Warning,
|
||||
$"Object type {wrapper.GetType().Name} is not a valid collection element"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates topology string for a single branch (following GrasshopperHelpers.GetParamTopology pattern)
|
||||
/// </summary>
|
||||
private string GetBranchTopology(Grasshopper.Kernel.Data.GH_Path path, int count) =>
|
||||
$"{path.ToString(false)}-{count}";
|
||||
}
|
||||
+3
-53
@@ -47,7 +47,7 @@ public class CreateCollection : VariableParameterComponentBase
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess dataAccess)
|
||||
{
|
||||
var rootCollection = CreateRootCollection();
|
||||
var rootCollection = CollectionHelpers.CreateRootCollection(InstanceGuid.ToString());
|
||||
bool hasAnyInput = false;
|
||||
|
||||
foreach (var inputParam in Params.Input)
|
||||
@@ -73,14 +73,14 @@ public class CreateCollection : VariableParameterComponentBase
|
||||
}
|
||||
|
||||
// validate for duplicate application IDs across the entire collection hierarchy
|
||||
if (HasDuplicateApplicationIds(rootCollection))
|
||||
if (CollectionHelpers.HasDuplicateApplicationIds(rootCollection))
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
|
||||
return;
|
||||
}
|
||||
|
||||
// validate collection isn't empty (CNX-2855)
|
||||
if (rootCollection.Elements.Count == 0 || !rootCollection.Elements.Any(HasAnyValidContent))
|
||||
if (rootCollection.Elements.Count == 0 || !rootCollection.Elements.Any(CollectionHelpers.HasAnyValidContent))
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
@@ -233,56 +233,6 @@ public class CreateCollection : VariableParameterComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all application IDs are unique across the entire collection hierarchy.
|
||||
/// Shows an error if duplicates are found, indicating objects appear in multiple collections.
|
||||
/// </summary>
|
||||
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
|
||||
private bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
|
||||
{
|
||||
// args to CheckForDuplicateApplicationIds passed in since the method can recursively check
|
||||
var seenIds = new HashSet<string>();
|
||||
var duplicateIds = new HashSet<string>();
|
||||
|
||||
// iterate, create hash set and check all application IDs
|
||||
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
|
||||
|
||||
return duplicateIds.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively collects application IDs from all in the collection hierarchy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
|
||||
/// </remarks>
|
||||
private void ProcessAndCheckForDuplicateApplicationIds(
|
||||
SpeckleCollectionWrapper collection,
|
||||
HashSet<string> seenIds,
|
||||
HashSet<string> duplicateIds
|
||||
)
|
||||
{
|
||||
foreach (var element in collection.Elements)
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case null:
|
||||
break; // skip nulls (CNX-2855)
|
||||
case SpeckleCollectionWrapper childCollection:
|
||||
// recurse into child collections
|
||||
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
|
||||
break;
|
||||
|
||||
case SpeckleWrapper wrapper:
|
||||
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
|
||||
{
|
||||
duplicateIds.Add(wrapper.ApplicationId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IGH_VariableParameterComponent implementation
|
||||
public override bool CanInsertParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input;
|
||||
|
||||
|
||||
+209
-95
@@ -1,5 +1,6 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Grasshopper.Kernel;
|
||||
using Grasshopper.Kernel.Parameters;
|
||||
using Grasshopper.Kernel.Types;
|
||||
using Speckle.Connectors.GrasshopperShared.Components.BaseComponents;
|
||||
using Speckle.Connectors.GrasshopperShared.HostApp;
|
||||
@@ -12,7 +13,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
|
||||
/// Given a list of objects, this component will filter the list for objects that match the queries.
|
||||
/// </summary>
|
||||
[Guid("26AEA046-4DD4-4F61-8251-E92A6D2AC880")]
|
||||
public class FilterSpeckleObjects : GH_Component
|
||||
public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
{
|
||||
public override Guid ComponentGuid => GetType().GUID;
|
||||
protected override Bitmap Icon => Resources.speckle_objects_filter;
|
||||
@@ -49,17 +50,6 @@ public class FilterSpeckleObjects : GH_Component
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[3].Optional = true;
|
||||
|
||||
pManager.AddTextParameter(
|
||||
"Application Id",
|
||||
"aID",
|
||||
"Find objects with a matching applicationId",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[4].Optional = true;
|
||||
|
||||
pManager.AddTextParameter("Speckle Id", "sID", "Find objects with a matching Speckle id", GH_ParamAccess.item);
|
||||
Params.Input[5].Optional = true;
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
|
||||
@@ -103,101 +93,39 @@ public class FilterSpeckleObjects : GH_Component
|
||||
dataAccess.GetData(2, ref property);
|
||||
string material = "";
|
||||
dataAccess.GetData(3, ref material);
|
||||
|
||||
// optional parameters - only read if they've been added via ⊕
|
||||
string appId = "";
|
||||
dataAccess.GetData(4, ref appId);
|
||||
string speckleId = "";
|
||||
dataAccess.GetData(5, ref speckleId);
|
||||
int? appIdIndex = FindInputIndexByName("Application Id");
|
||||
int? speckleIdIndex = FindInputIndexByName("Speckle Id");
|
||||
|
||||
if (appIdIndex.HasValue)
|
||||
{
|
||||
dataAccess.GetData(appIdIndex.Value, ref appId);
|
||||
}
|
||||
|
||||
if (speckleIdIndex.HasValue)
|
||||
{
|
||||
dataAccess.GetData(speckleIdIndex.Value, ref speckleId);
|
||||
}
|
||||
|
||||
bool filterByAppId = appIdIndex.HasValue;
|
||||
bool filterBySpeckleId = speckleIdIndex.HasValue;
|
||||
|
||||
List<SpeckleWrapper> matchedObjects = new();
|
||||
List<SpeckleWrapper> removedObjects = new();
|
||||
for (int i = 0; i < objects.Count; i++)
|
||||
|
||||
foreach (SpeckleWrapper wrapper in objects.Cast<SpeckleWrapper>())
|
||||
{
|
||||
SpeckleWrapper wrapper = objects[i]!;
|
||||
|
||||
// filter by name
|
||||
if (!MatchesSearchPattern(name, wrapper.Name))
|
||||
if (MatchesAllFilters(wrapper, name, property, material, appId, filterByAppId, speckleId, filterBySpeckleId))
|
||||
{
|
||||
removedObjects.Add(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter by property
|
||||
bool foundProperty = false;
|
||||
if (string.IsNullOrEmpty(property))
|
||||
{
|
||||
foundProperty = true;
|
||||
matchedObjects.Add(wrapper);
|
||||
}
|
||||
else
|
||||
{
|
||||
SpecklePropertyGroupGoo? properties = wrapper is SpeckleDataObjectWrapper dataObjPropWrapper
|
||||
? dataObjPropWrapper.Properties
|
||||
: wrapper is SpeckleGeometryWrapper geoPropWrapper
|
||||
? geoPropWrapper.Properties
|
||||
: null;
|
||||
|
||||
if (properties is not null)
|
||||
{
|
||||
// use flattened properties to search ALL nested property keys
|
||||
// fix for [CNX-2512](https://linear.app/speckle/issue/CNX-2512/filter-objects-material-and-property-key-inputs-dont-work-as-expected)
|
||||
Dictionary<string, SpecklePropertyGoo> flattenedProps = properties.Flatten();
|
||||
foreach (string key in flattenedProps.Keys)
|
||||
{
|
||||
if (MatchesSearchPattern(property, key))
|
||||
{
|
||||
foundProperty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundProperty)
|
||||
{
|
||||
removedObjects.Add(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter by material name
|
||||
bool materialMatches = true;
|
||||
if (!string.IsNullOrEmpty(material))
|
||||
{
|
||||
materialMatches = false;
|
||||
|
||||
if (wrapper is SpeckleGeometryWrapper geoWrapper)
|
||||
{
|
||||
materialMatches = MatchesSearchPattern(material, geoWrapper.Material?.Name ?? "");
|
||||
}
|
||||
else if (wrapper is SpeckleDataObjectWrapper dataObjWrapper)
|
||||
{
|
||||
// check if ANY geometry in the data object has a matching material (not sure about this...)
|
||||
// fix for [CNX-2512](https://linear.app/speckle/issue/CNX-2512/filter-objects-material-and-property-key-inputs-dont-work-as-expected)
|
||||
materialMatches = dataObjWrapper.Geometries.Any(geo =>
|
||||
MatchesSearchPattern(material, geo.Material?.Name ?? "")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!materialMatches)
|
||||
{
|
||||
removedObjects.Add(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter by application id
|
||||
if (!MatchesSearchPattern(appId, wrapper.Base.applicationId ?? ""))
|
||||
{
|
||||
removedObjects.Add(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter by speckle id
|
||||
if (!MatchesSearchPattern(speckleId, wrapper.Base.id ?? ""))
|
||||
{
|
||||
removedObjects.Add(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
matchedObjects.Add(wrapper);
|
||||
}
|
||||
|
||||
// Set output objects
|
||||
@@ -214,4 +142,190 @@ public class FilterSpeckleObjects : GH_Component
|
||||
|
||||
return Operator.IsSymbolNameLike(target, searchPattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a wrapper matches all active filter criteria.
|
||||
/// </summary>
|
||||
private bool MatchesAllFilters(
|
||||
SpeckleWrapper wrapper,
|
||||
string name,
|
||||
string property,
|
||||
string material,
|
||||
string appId,
|
||||
bool filterByAppId,
|
||||
string speckleId,
|
||||
bool filterBySpeckleId
|
||||
)
|
||||
{
|
||||
// filter by name
|
||||
if (!MatchesSearchPattern(name, wrapper.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter by property
|
||||
if (!MatchesPropertyFilter(wrapper, property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter by material name
|
||||
if (!MatchesMaterialFilter(wrapper, material))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter by application id (only if parameter was added)
|
||||
if (filterByAppId && !MatchesSearchPattern(appId, wrapper.Base.applicationId ?? ""))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter by speckle id (only if parameter was added)
|
||||
if (filterBySpeckleId && !MatchesSearchPattern(speckleId, wrapper.Base.id ?? ""))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MatchesPropertyFilter(SpeckleWrapper wrapper, string property)
|
||||
{
|
||||
if (string.IsNullOrEmpty(property))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
SpecklePropertyGroupGoo? properties = wrapper is SpeckleDataObjectWrapper dataObjPropWrapper
|
||||
? dataObjPropWrapper.Properties
|
||||
: wrapper is SpeckleGeometryWrapper geoPropWrapper
|
||||
? geoPropWrapper.Properties
|
||||
: null;
|
||||
|
||||
if (properties is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// use flattened properties to search ALL nested property keys
|
||||
return properties.Flatten().Keys.Any(key => MatchesSearchPattern(property, key));
|
||||
}
|
||||
|
||||
private bool MatchesMaterialFilter(SpeckleWrapper wrapper, string material)
|
||||
{
|
||||
if (string.IsNullOrEmpty(material))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wrapper is SpeckleGeometryWrapper geoWrapper)
|
||||
{
|
||||
return MatchesSearchPattern(material, geoWrapper.Material?.Name ?? "");
|
||||
}
|
||||
|
||||
if (wrapper is SpeckleDataObjectWrapper dataObjWrapper)
|
||||
{
|
||||
// check if ANY geometry in the data object has a matching material
|
||||
return dataObjWrapper.Geometries.Any(geo => MatchesSearchPattern(material, geo.Material?.Name ?? ""));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the index of an input parameter by its Name.
|
||||
/// Returns null if the parameter doesn't exist.
|
||||
/// </summary>
|
||||
private int? FindInputIndexByName(string paramName)
|
||||
{
|
||||
for (int i = 0; i < Params.Input.Count; i++)
|
||||
{
|
||||
if (Params.Input[i].Name == paramName)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#region IGH_VariableParameterComponent
|
||||
|
||||
public bool CanInsertParameter(GH_ParameterSide side, int index)
|
||||
{
|
||||
if (side != GH_ParameterSide.Input)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// only allow inserting after the fixed parameters (index 4+)
|
||||
if (index < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// check how many optional params are already added (total inputs - 4 fixed)
|
||||
int addedOptionalCount = Params.Input.Count - 4;
|
||||
|
||||
// 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+)
|
||||
side == GH_ParameterSide.Input
|
||||
&& index >= 4;
|
||||
|
||||
/// <remarks>
|
||||
/// The ternary operator for NickName is needed due to a Grasshopper quirk where
|
||||
/// dynamically created parameters don't respect the "Draw Full Names" setting automatically.
|
||||
/// We check CanvasFullNames at creation time to set the appropriate NickName.
|
||||
/// This does not handle the case where the user toggles "Draw Full Names" while the
|
||||
/// component is already on the canvas. Handling that would require subscribing to
|
||||
/// Grasshopper.CentralSettings.CanvasFullNamesChanged event, which is overkill for now.
|
||||
/// </remarks>
|
||||
public IGH_Param CreateParameter(GH_ParameterSide side, int index)
|
||||
{
|
||||
bool hasAppId = FindInputIndexByName("Application Id").HasValue;
|
||||
bool hasSpeckleId = FindInputIndexByName("Speckle Id").HasValue;
|
||||
|
||||
if (!hasAppId)
|
||||
{
|
||||
return new Param_String
|
||||
{
|
||||
Name = "Application Id",
|
||||
NickName = Grasshopper.CentralSettings.CanvasFullNames ? "Application Id" : "aID", // see remarks
|
||||
Description = "Find objects with a matching applicationId",
|
||||
Access = GH_ParamAccess.item,
|
||||
Optional = true
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasSpeckleId)
|
||||
{
|
||||
return new Param_String
|
||||
{
|
||||
Name = "Speckle Id",
|
||||
NickName = Grasshopper.CentralSettings.CanvasFullNames ? "Speckle Id" : "sID", // see remarks
|
||||
Description = "Find objects with a matching Speckle id",
|
||||
Access = GH_ParamAccess.item,
|
||||
Optional = true
|
||||
};
|
||||
}
|
||||
|
||||
return new Param_String();
|
||||
}
|
||||
|
||||
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input && index >= 4;
|
||||
|
||||
public void VariableParameterMaintenance()
|
||||
{
|
||||
// ensure all optional parameters stay marked as optional
|
||||
for (int i = 4; i < Params.Input.Count; i++)
|
||||
{
|
||||
Params.Input[i].Optional = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
+10
-3
@@ -9,7 +9,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
|
||||
|
||||
[Guid("8D2E3F4A-1B5C-4E7F-9A8B-3C6D9E2F1A4B")]
|
||||
public class SpeckleBlockDefinitionPassthrough()
|
||||
: SpeckleSolveInstance(
|
||||
: SpecklePassthroughComponentBase(
|
||||
"Speckle Block Definition",
|
||||
"SBD",
|
||||
"Create or modify a Speckle Block Definition",
|
||||
@@ -21,6 +21,9 @@ public class SpeckleBlockDefinitionPassthrough()
|
||||
protected override Bitmap Icon => Resources.speckle_objects_block_def;
|
||||
public override GH_Exposure Exposure => GH_Exposure.tertiary;
|
||||
|
||||
protected override int FixedInputCount => 3;
|
||||
protected override int FixedOutputCount => 3;
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
{
|
||||
pManager.AddParameter(
|
||||
@@ -122,12 +125,16 @@ public class SpeckleBlockDefinitionPassthrough()
|
||||
result.Value.Name = inputName;
|
||||
}
|
||||
|
||||
// no need to process application Id.
|
||||
// New definitions should have a new appID generated in the new() constructor, and we want to preserve old appID otherwise for changetracking.
|
||||
// process application id (only if user provided one, otherwise preserve existing)
|
||||
if (TryGetApplicationIdInput(da, out string? inputAppId))
|
||||
{
|
||||
result.Value.ApplicationId = inputAppId;
|
||||
}
|
||||
|
||||
// set outputs
|
||||
da.SetData(0, result);
|
||||
da.SetDataList(1, result.Value.Objects.Select(o => o.CreateGoo()));
|
||||
da.SetData(2, result.Value.Name);
|
||||
SetApplicationIdOutput(da, result.Value.ApplicationId);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -9,7 +9,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
|
||||
|
||||
[Guid("2F8A9B1C-3D4E-5F6A-7B8C-9D0E1F2A3B4C")]
|
||||
public class SpeckleBlockInstancePassthrough()
|
||||
: SpeckleSolveInstance(
|
||||
: SpecklePassthroughComponentBase(
|
||||
"Speckle Block Instance",
|
||||
"SBI",
|
||||
"Create or modify a Speckle Block Instance",
|
||||
@@ -21,6 +21,9 @@ public class SpeckleBlockInstancePassthrough()
|
||||
protected override Bitmap Icon => Resources.speckle_objects_block_inst;
|
||||
public override GH_Exposure Exposure => GH_Exposure.tertiary;
|
||||
|
||||
protected override int FixedInputCount => 7;
|
||||
protected override int FixedOutputCount => 7;
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
{
|
||||
int instanceIndex = pManager.AddParameter(
|
||||
@@ -205,8 +208,11 @@ public class SpeckleBlockInstancePassthrough()
|
||||
result.Value.Material = inputMaterial.Value;
|
||||
}
|
||||
|
||||
// no need to process application id.
|
||||
// new appids are generated if this is a new object, otherwise the input object appID should be preserved for change tracking.
|
||||
// process application id (only if user provided one, otherwise preserve existing)
|
||||
if (TryGetApplicationIdInput(da, out string? inputAppId))
|
||||
{
|
||||
result.Value.ApplicationId = inputAppId;
|
||||
}
|
||||
|
||||
// Set outputs
|
||||
da.SetData(0, result);
|
||||
@@ -216,6 +222,7 @@ public class SpeckleBlockInstancePassthrough()
|
||||
da.SetData(4, result.Value.Properties);
|
||||
da.SetData(5, result.Value.Color);
|
||||
da.SetData(6, result.Value.Material);
|
||||
SetApplicationIdOutput(da, result.Value.ApplicationId);
|
||||
}
|
||||
|
||||
private Transform? ExtractTransform(IGH_Goo input) =>
|
||||
|
||||
+16
-4
@@ -8,7 +8,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
|
||||
|
||||
[Guid("5CE8AA40-7706-4893-853D-4C77604548FA")]
|
||||
public class SpeckleDataObjectPassthrough()
|
||||
: SpeckleSolveInstance(
|
||||
: SpecklePassthroughComponentBase(
|
||||
"Speckle Data Object",
|
||||
"SDO",
|
||||
"Create or modify a Speckle Data Object",
|
||||
@@ -20,6 +20,9 @@ public class SpeckleDataObjectPassthrough()
|
||||
protected override Bitmap Icon => Resources.speckle_objects_dataobject;
|
||||
public override GH_Exposure Exposure => GH_Exposure.secondary;
|
||||
|
||||
protected override int FixedInputCount => 4;
|
||||
protected override int FixedOutputCount => 5;
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
{
|
||||
int objIndex = pManager.AddParameter(
|
||||
@@ -158,9 +161,17 @@ public class SpeckleDataObjectPassthrough()
|
||||
result.Properties = inputProperties;
|
||||
}
|
||||
|
||||
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
|
||||
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
|
||||
result.ApplicationId ??= Guid.NewGuid().ToString();
|
||||
// process application id (only if user provided one)
|
||||
if (TryGetApplicationIdInput(da, out string? inputAppId))
|
||||
{
|
||||
result.ApplicationId = inputAppId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
|
||||
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
|
||||
result.ApplicationId ??= Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
// get the path
|
||||
string? path =
|
||||
@@ -172,5 +183,6 @@ public class SpeckleDataObjectPassthrough()
|
||||
da.SetData(2, result.Name);
|
||||
da.SetData(3, result.Properties);
|
||||
da.SetData(4, path);
|
||||
SetApplicationIdOutput(da, result.ApplicationId);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -10,7 +10,7 @@ namespace Speckle.Connectors.GrasshopperShared.Components.Objects;
|
||||
|
||||
[Guid("F9418610-ACAE-4417-B010-19EBEA6A121F")]
|
||||
public class SpeckleGeometryPassthrough()
|
||||
: SpeckleSolveInstance(
|
||||
: SpecklePassthroughComponentBase(
|
||||
"Speckle Geometry",
|
||||
"SG",
|
||||
"Create or modify a Speckle Geometry",
|
||||
@@ -22,6 +22,9 @@ public class SpeckleGeometryPassthrough()
|
||||
protected override Bitmap Icon => Resources.speckle_objects_geometry;
|
||||
public override GH_Exposure Exposure => GH_Exposure.secondary;
|
||||
|
||||
protected override int FixedInputCount => 6;
|
||||
protected override int FixedOutputCount => 7;
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
{
|
||||
int objIndex = pManager.AddGenericParameter(
|
||||
@@ -220,8 +223,11 @@ public class SpeckleGeometryPassthrough()
|
||||
result.Material = inputMaterial.Value;
|
||||
}
|
||||
|
||||
// no need to process application Id.
|
||||
// New definitions should have a new appID generated in the new() constructor, and we want to preserve old appID otherwise for changetracking.
|
||||
// process application id (only if user provided one, otherwise preserve existing)
|
||||
if (TryGetApplicationIdInput(da, out string? inputAppId))
|
||||
{
|
||||
result.ApplicationId = inputAppId;
|
||||
}
|
||||
|
||||
// get the path
|
||||
string? path =
|
||||
@@ -235,6 +241,7 @@ public class SpeckleGeometryPassthrough()
|
||||
da.SetData(4, result.Color);
|
||||
da.SetData(5, result.Material);
|
||||
da.SetData(6, path);
|
||||
SetApplicationIdOutput(da, result.ApplicationId);
|
||||
}
|
||||
|
||||
// keeps the geometry and wrapped base the same while assigning all other props from the inut wrapper
|
||||
|
||||
+7
-1
@@ -87,6 +87,7 @@ public class SendAsyncComponent : GH_AsyncComponent<SendAsyncComponent>
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
|
||||
{
|
||||
pManager.AddParameter(new SpeckleUrlModelResourceParam());
|
||||
pManager.AddTextParameter("Version ID", "V", "ID of the created version", GH_ParamAccess.item);
|
||||
}
|
||||
|
||||
public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
|
||||
@@ -321,6 +322,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
|
||||
|
||||
private Stopwatch? _stopwatch;
|
||||
public SpeckleUrlModelResource? OutputParam { get; set; }
|
||||
public string? OutputVersionId { get; set; }
|
||||
private List<(GH_RuntimeMessageLevel, string)> RuntimeMessages { get; } = new();
|
||||
|
||||
public override WorkerInstance<SendAsyncComponent> Duplicate(string id, CancellationToken cancellationToken)
|
||||
@@ -332,6 +334,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
|
||||
{
|
||||
_stopwatch = new Stopwatch();
|
||||
_stopwatch.Start();
|
||||
OutputVersionId = null;
|
||||
}
|
||||
|
||||
public override void SetData(IGH_DataAccess da)
|
||||
@@ -342,6 +345,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
|
||||
{
|
||||
Parent.JustPastedIn = false;
|
||||
da.SetData(0, Parent.OutputParam);
|
||||
da.SetData(1, OutputVersionId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -357,6 +361,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
|
||||
}
|
||||
|
||||
da.SetData(0, OutputParam);
|
||||
da.SetData(1, OutputVersionId);
|
||||
|
||||
Parent.CurrentComponentState = ComponentState.UpToDate;
|
||||
Parent.OutputParam = OutputParam; // ref the outputs in the parent too, so we can serialise them on write/read
|
||||
@@ -373,7 +378,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
|
||||
*/
|
||||
Parent.AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Remark,
|
||||
$"Successfully published to Speckle. Right-click on the component to view online."
|
||||
"Successfully published to Speckle. Right-click on the component to view online."
|
||||
);
|
||||
Parent.AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Remark,
|
||||
@@ -471,6 +476,7 @@ public class SendComponentWorker : WorkerInstance<SendAsyncComponent>
|
||||
result.VersionId
|
||||
);
|
||||
OutputParam = createdVersion;
|
||||
OutputVersionId = result.VersionId;
|
||||
Parent.Url = $"{createdVersion.Account.Server}/projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}";
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -36,9 +36,10 @@ public class SendComponentInput
|
||||
}
|
||||
}
|
||||
|
||||
public class SendComponentOutput(SpeckleUrlModelResource? resource)
|
||||
public class SendComponentOutput(SpeckleUrlModelResource? resource, string? versionId = null)
|
||||
{
|
||||
public SpeckleUrlModelResource? Resource { get; } = resource;
|
||||
public string? VersionId { get; } = versionId;
|
||||
}
|
||||
|
||||
public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, SendComponentOutput>
|
||||
@@ -86,8 +87,11 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
|
||||
pManager.AddBooleanParameter("Run", "r", "Run the publish operation", GH_ParamAccess.item);
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
|
||||
{
|
||||
pManager.AddParameter(new SpeckleUrlModelResourceParam());
|
||||
pManager.AddTextParameter("Version ID", "V", "ID of the created version", GH_ParamAccess.item);
|
||||
}
|
||||
|
||||
protected override SendComponentInput GetInput(IGH_DataAccess da)
|
||||
{
|
||||
@@ -134,6 +138,7 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
|
||||
else
|
||||
{
|
||||
da.SetData(0, result.Resource);
|
||||
da.SetData(1, result.VersionId);
|
||||
Message = "Done";
|
||||
}
|
||||
}
|
||||
@@ -216,7 +221,7 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
|
||||
|
||||
using var client = clientFactory.Create(account);
|
||||
var sendInfo = await input.Resource.GetSendInfo(client, cancellationToken).ConfigureAwait(false);
|
||||
await sendOperation
|
||||
var result = await sendOperation
|
||||
.Execute(
|
||||
new List<SpeckleCollectionWrapperGoo> { collectionToSend },
|
||||
sendInfo,
|
||||
@@ -244,6 +249,6 @@ public class SendComponent : SpeckleTaskCapableComponent<SendComponentInput, Sen
|
||||
sendInfo.ModelId
|
||||
);
|
||||
Url = $"{sendInfo.Account.serverInfo.url}/projects/{sendInfo.ProjectId}/models/{sendInfo.ModelId}";
|
||||
return new SendComponentOutput(createdVersionResource);
|
||||
return new SendComponentOutput(createdVersionResource, result.VersionId);
|
||||
}
|
||||
}
|
||||
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
using GH_IO.Serialization;
|
||||
using Grasshopper.Kernel;
|
||||
using Grasshopper.Kernel.Parameters;
|
||||
|
||||
namespace Speckle.Connectors.GrasshopperShared.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for passthrough components with "hidden" Application ID parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Users can click ⊕ to add an optional Application ID input and output.
|
||||
/// </remarks>
|
||||
public abstract class SpecklePassthroughComponentBase : SpeckleSolveInstance, IGH_VariableParameterComponent
|
||||
{
|
||||
private const string APP_ID_NAME = "Application Id";
|
||||
private const string APP_ID_NICKNAME = "aID";
|
||||
private const string APP_ID_DESCRIPTION = "The application id of the Speckle objects";
|
||||
|
||||
protected abstract int FixedInputCount { get; }
|
||||
protected abstract int FixedOutputCount { get; }
|
||||
|
||||
private bool HasApplicationIdParam => Params.Input.Count > FixedInputCount;
|
||||
|
||||
protected SpecklePassthroughComponentBase(
|
||||
string name,
|
||||
string nickname,
|
||||
string description,
|
||||
string category,
|
||||
string subCategory
|
||||
)
|
||||
: base(name, nickname, description, category, subCategory) { }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the optional Application Id input. Returns true if user provided a valid value.
|
||||
/// </summary>
|
||||
protected bool TryGetApplicationIdInput(IGH_DataAccess da, out string? applicationId)
|
||||
{
|
||||
applicationId = null;
|
||||
|
||||
if (!HasApplicationIdParam)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string appId = string.Empty;
|
||||
if (da.GetData(FixedInputCount, ref appId))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(appId))
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Warning,
|
||||
"Empty Application Id ignored - existing or auto-generated id will be used"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
applicationId = appId;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Application Id output (if the parameter exists).
|
||||
/// </summary>
|
||||
protected void SetApplicationIdOutput(IGH_DataAccess da, string? applicationId)
|
||||
{
|
||||
if (!HasApplicationIdParam)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
da.SetData(FixedOutputCount, applicationId);
|
||||
}
|
||||
|
||||
public bool CanInsertParameter(GH_ParameterSide side, int index)
|
||||
{
|
||||
// only allow inserting if not yet added
|
||||
if (HasApplicationIdParam)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// only allow at the end position
|
||||
return side switch
|
||||
{
|
||||
GH_ParameterSide.Input => index == FixedInputCount,
|
||||
GH_ParameterSide.Output => index == FixedOutputCount,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public bool CanRemoveParameter(GH_ParameterSide side, int index)
|
||||
{
|
||||
if (!HasApplicationIdParam)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return side switch
|
||||
{
|
||||
GH_ParameterSide.Input => index == FixedInputCount,
|
||||
GH_ParameterSide.Output => index == FixedOutputCount,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The ternary for NickName handles a Grasshopper quirk where dynamically created parameters
|
||||
/// don't respect the "Draw Full Names" setting automatically.
|
||||
/// </remarks>
|
||||
public IGH_Param CreateParameter(GH_ParameterSide side, int index)
|
||||
{
|
||||
// when adding on either side, add both input and output together
|
||||
if (side == GH_ParameterSide.Input && Params.Output.Count == FixedOutputCount)
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => AddApplicationIdOutput());
|
||||
}
|
||||
else if (side == GH_ParameterSide.Output && Params.Input.Count == FixedInputCount)
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => AddApplicationIdInput());
|
||||
}
|
||||
|
||||
return CreateApplicationIdParam();
|
||||
}
|
||||
|
||||
public bool DestroyParameter(GH_ParameterSide side, int index)
|
||||
{
|
||||
// when removing from either side, remove both input and output together
|
||||
if (side == GH_ParameterSide.Input && index == FixedInputCount && Params.Output.Count > FixedOutputCount)
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => RemoveApplicationIdOutput());
|
||||
}
|
||||
else if (side == GH_ParameterSide.Output && index == FixedOutputCount && Params.Input.Count > FixedInputCount)
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => RemoveApplicationIdInput());
|
||||
}
|
||||
|
||||
return side switch
|
||||
{
|
||||
GH_ParameterSide.Input => index == FixedInputCount,
|
||||
GH_ParameterSide.Output => index == FixedOutputCount,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public void VariableParameterMaintenance()
|
||||
{
|
||||
// ensure the Application Id input stays optional
|
||||
if (HasApplicationIdParam && Params.Input.Count > FixedInputCount)
|
||||
{
|
||||
Params.Input[FixedInputCount].Optional = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static IGH_Param CreateApplicationIdParam() =>
|
||||
new Param_String
|
||||
{
|
||||
Name = APP_ID_NAME,
|
||||
NickName = Grasshopper.CentralSettings.CanvasFullNames ? APP_ID_NAME : APP_ID_NICKNAME,
|
||||
Description = APP_ID_DESCRIPTION,
|
||||
Access = GH_ParamAccess.item,
|
||||
Optional = true
|
||||
};
|
||||
|
||||
private void AddApplicationIdInput()
|
||||
{
|
||||
if (Params.Input.Count > FixedInputCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Params.RegisterInputParam(CreateApplicationIdParam());
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(true);
|
||||
}
|
||||
|
||||
private void AddApplicationIdOutput()
|
||||
{
|
||||
if (Params.Output.Count > FixedOutputCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Params.RegisterOutputParam(CreateApplicationIdParam());
|
||||
Params.OnParametersChanged();
|
||||
ExpireSolution(true);
|
||||
}
|
||||
|
||||
private void RemoveApplicationIdInput()
|
||||
{
|
||||
if (Params.Input.Count <= FixedInputCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Params.UnregisterInputParameter(Params.Input[FixedInputCount]);
|
||||
Params.OnParametersChanged();
|
||||
ExpireSolution(true);
|
||||
}
|
||||
|
||||
private void RemoveApplicationIdOutput()
|
||||
{
|
||||
if (Params.Output.Count <= FixedOutputCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Params.UnregisterOutputParameter(Params.Output[FixedOutputCount]);
|
||||
Params.OnParametersChanged();
|
||||
ExpireSolution(true);
|
||||
}
|
||||
|
||||
public override bool Write(GH_IWriter writer)
|
||||
{
|
||||
var result = base.Write(writer);
|
||||
writer.SetBoolean("HasApplicationIdParam", HasApplicationIdParam);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Read(GH_IReader reader)
|
||||
{
|
||||
var result = base.Read(reader);
|
||||
// parameters are restored by GH serialization, this flag is for reference
|
||||
bool hasAppIdParam = false;
|
||||
reader.TryGetBoolean("HasApplicationIdParam", ref hasAppIdParam);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using Speckle.Connectors.GrasshopperShared.Parameters;
|
||||
using Speckle.Sdk.Models.Collections;
|
||||
|
||||
namespace Speckle.Connectors.GrasshopperShared.Components.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper methods for collection components to avoid code duplication
|
||||
/// </summary>
|
||||
public static class CollectionHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a root collection wrapper with default values
|
||||
/// </summary>
|
||||
public static SpeckleCollectionWrapper CreateRootCollection(string instanceGuid) =>
|
||||
new SpeckleCollectionWrapper
|
||||
{
|
||||
Base = new Collection(),
|
||||
Name = "Unnamed",
|
||||
Path = new List<string> { "Unnamed" },
|
||||
Color = null,
|
||||
Material = null,
|
||||
ApplicationId = instanceGuid
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all application IDs are unique across the entire collection hierarchy.
|
||||
/// </summary>
|
||||
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
|
||||
public static bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
|
||||
{
|
||||
var seenIds = new HashSet<string>();
|
||||
var duplicateIds = new HashSet<string>();
|
||||
|
||||
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
|
||||
|
||||
return duplicateIds.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively collects application IDs from all wrappers in the collection hierarchy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
|
||||
/// </remarks>
|
||||
private static void ProcessAndCheckForDuplicateApplicationIds(
|
||||
SpeckleCollectionWrapper collection,
|
||||
HashSet<string> seenIds,
|
||||
HashSet<string> duplicateIds
|
||||
)
|
||||
{
|
||||
foreach (var element in collection.Elements)
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case null:
|
||||
break; // skip nulls (CNX-2855)
|
||||
case SpeckleCollectionWrapper childCollection:
|
||||
// recurse into child collections
|
||||
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
|
||||
break;
|
||||
|
||||
case SpeckleWrapper wrapper:
|
||||
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
|
||||
{
|
||||
duplicateIds.Add(wrapper.ApplicationId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively checks if collection or any descendants contain valid geometry/data objects
|
||||
/// </summary>
|
||||
public static bool HasAnyValidContent(ISpeckleCollectionObject? element) =>
|
||||
element switch
|
||||
{
|
||||
SpeckleGeometryWrapper => true,
|
||||
SpeckleDataObjectWrapper => true,
|
||||
SpeckleCollectionWrapper collection => collection.Elements.Any(HasAnyValidContent),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
+8
@@ -225,6 +225,14 @@ internal sealed class LocalToGlobalMapHandler
|
||||
|
||||
var entry = _dataObjectInstanceRegistry.GetEntries()[dataObjectId];
|
||||
var resolvedGeometries = ResolveInstanceProxiesToGeometries(entry.InstanceProxies);
|
||||
|
||||
var primitiveConverted = dataObject
|
||||
.displayValue.Where(item => item is not InstanceProxy)
|
||||
.SelectMany(item => SpeckleConversionContext.Current.ConvertToHost(item))
|
||||
.ToList();
|
||||
|
||||
resolvedGeometries.AddRange(ConvertToGeometryWrappers(primitiveConverted));
|
||||
|
||||
var dataObjectWrapper = CreateDataObjectWrapper(dataObject, resolvedGeometries, path, objectCollection);
|
||||
|
||||
CollectionRebuilder.AppendSpeckleGrasshopperObject(dataObjectWrapper, path, _colorUnpacker, _materialUnpacker);
|
||||
|
||||
+3
@@ -14,6 +14,7 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\BaseComponents\ValueSet.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\BaseComponents\VariableParameterComponentBase.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CollectionPathsSelector.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CollectionsByName.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\CreateCollection.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Collections\ExpandCollection.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Objects\ExpandSpeckleProperties.cs" />
|
||||
@@ -41,7 +42,9 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\VersionMenuHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\WorkspaceMenuHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\Operations\Wizard\SpeckleOperationWizard.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\SpecklePassthroughComponentBase.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Components\SpeckleSolveInstance.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\CollectionHelpers.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extras\StateTag.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\KeyWatcher.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\GrasshopperBlockUnpacker.cs" />
|
||||
|
||||
@@ -122,7 +122,9 @@ public class RhinoLayerBaker : TraversalContextUnpacker
|
||||
continue;
|
||||
}
|
||||
|
||||
var cleanNewLayerName = RhinoUtils.CleanLayerName(collection.name);
|
||||
var cleanNewLayerName = string.IsNullOrWhiteSpace(collection.name)
|
||||
? "unnamed"
|
||||
: RhinoUtils.CleanLayerName(collection.name);
|
||||
|
||||
if (!ModelComponent.IsValidComponentName(cleanNewLayerName))
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ public static class RhinoUtils
|
||||
public static string CleanLayerName(string str)
|
||||
{
|
||||
var sb = new StringBuilder(str.Length);
|
||||
bool lastWasSpace = true;
|
||||
|
||||
foreach (char c in str)
|
||||
{
|
||||
@@ -30,10 +31,29 @@ public static class RhinoUtils
|
||||
if (s_replaceWithHyphen.Contains(c))
|
||||
{
|
||||
sb.Append('-');
|
||||
lastWasSpace = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collapse double spaces into one and skip leading spaces.
|
||||
// e.g. " Items Name " -> "Items Name"
|
||||
if (c == ' ')
|
||||
{
|
||||
if (!lastWasSpace)
|
||||
{
|
||||
sb.Append(c);
|
||||
lastWasSpace = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(c);
|
||||
lastWasSpace = false;
|
||||
}
|
||||
|
||||
if (sb.Length > 0 && sb[^1] == ' ')
|
||||
{
|
||||
sb.Length--;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
|
||||
+7
-22
@@ -75,8 +75,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
|
||||
)
|
||||
{
|
||||
using var activity = _activityFactory.Start("Build");
|
||||
using var sendPipeline = new Speckle.Sdk.Pipeline.Send();
|
||||
|
||||
// 0 - Init the root
|
||||
Collection rootObjectCollection = new() { name = _converterSettings.Current.Document.Name ?? "Unnamed document" };
|
||||
rootObjectCollection["units"] = _converterSettings.Current.SpeckleUnits;
|
||||
@@ -99,7 +97,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
|
||||
// 3 - Convert atomic objects
|
||||
List<SendConversionResult> results = new(atomicObjects.Count);
|
||||
int count = 0;
|
||||
|
||||
using (var _ = _activityFactory.Start("Convert all"))
|
||||
{
|
||||
foreach (RhinoObject rhinoObject in atomicObjects)
|
||||
@@ -111,8 +108,9 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
|
||||
Layer layer = _converterSettings.Current.Document.Layers[rhinoObject.Attributes.LayerIndex];
|
||||
Collection collectionHost = _layerUnpacker.GetHostObjectCollection(layer, rootObjectCollection);
|
||||
|
||||
var result = await ConvertRhinoObject(rhinoObject, collectionHost, instanceProxies, projectId, sendPipeline);
|
||||
var result = ConvertRhinoObject(rhinoObject, collectionHost, instanceProxies, projectId);
|
||||
results.Add(result);
|
||||
|
||||
++count;
|
||||
onOperationProgressed.Report(new("Converting", (double)count / atomicObjects.Count));
|
||||
await Task.Yield();
|
||||
@@ -151,23 +149,18 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
|
||||
}
|
||||
}
|
||||
|
||||
await sendPipeline.Process(rootObjectCollection);
|
||||
await sendPipeline.WaitForUpload();
|
||||
|
||||
return new RootObjectBuilderResult(new Collection() { name = "ignore" }, results);
|
||||
return new RootObjectBuilderResult(rootObjectCollection, results);
|
||||
}
|
||||
|
||||
private async Task<SendConversionResult> ConvertRhinoObject(
|
||||
private SendConversionResult ConvertRhinoObject(
|
||||
RhinoObject rhinoObject,
|
||||
Collection collectionHost,
|
||||
IReadOnlyDictionary<string, InstanceProxy> instanceProxies,
|
||||
string projectId,
|
||||
Sdk.Pipeline.Send sendPipeline
|
||||
string projectId
|
||||
)
|
||||
{
|
||||
string applicationId = rhinoObject.Id.ToString();
|
||||
string sourceType = rhinoObject.ObjectType.ToString();
|
||||
bool wasCached = false;
|
||||
try
|
||||
{
|
||||
// get from cache or convert:
|
||||
@@ -181,7 +174,6 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
|
||||
else if (_sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
|
||||
{
|
||||
converted = value;
|
||||
wasCached = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -202,17 +194,10 @@ public class RhinoRootObjectBuilder : IRootObjectBuilder<RhinoObject>
|
||||
converted["properties"] = properties;
|
||||
}
|
||||
|
||||
// process in pipeline
|
||||
var reference = await sendPipeline.Process(converted).ConfigureAwait(false);
|
||||
if (!wasCached)
|
||||
{
|
||||
_sendConversionCache.AppendSendResult(projectId, applicationId, reference);
|
||||
}
|
||||
|
||||
// add to host
|
||||
collectionHost.elements.Add(reference);
|
||||
collectionHost.elements.Add(converted);
|
||||
|
||||
return new(Status.SUCCESS, applicationId, sourceType, reference);
|
||||
return new(Status.SUCCESS, applicationId, sourceType, converted);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
|
||||
+6
-1
@@ -2,11 +2,12 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Rhino.PlugIns;
|
||||
using Speckle.Connectors.Common;
|
||||
using Speckle.Connectors.DUI;
|
||||
using Speckle.Connectors.DUI.Models;
|
||||
using Speckle.Connectors.Rhino.DependencyInjection;
|
||||
using Speckle.Converters.Rhino;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Models.Extensions;
|
||||
|
||||
|
||||
namespace Speckle.Connectors.Rhino.Plugin;
|
||||
|
||||
///<summary>
|
||||
@@ -50,6 +51,10 @@ public class SpeckleConnectorsRhinoPlugin : PlugIn
|
||||
|
||||
// but the Rhino connector has `.rhp` as it is extension.
|
||||
Container = services.BuildServiceProvider();
|
||||
// FORCE INITIALIZATION RhinoDocumentStore to register event handlers for BeginOpenDocument and EndOpenDocument
|
||||
// this is needed when the user opens a Rhino file by double clicking the file,
|
||||
// instead of opening a file in an already running Rhino instance
|
||||
Container.GetRequiredService<DocumentModelStore>();
|
||||
Container.UseDUI();
|
||||
|
||||
return LoadReturnCode.Success;
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
namespace Speckle.Converter.Navisworks.Constants;
|
||||
|
||||
public static class InstanceConstants
|
||||
{
|
||||
public const string GEOMETRY_ID_PREFIX = "geom_";
|
||||
public const string DEFINITION_ID_PREFIX = "def_";
|
||||
public const string INSTANCE_ID_PREFIX = "instance_";
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace Speckle.Converter.Navisworks.Constants;
|
||||
|
||||
public static class MaterialConstants
|
||||
{
|
||||
public const string DEFAULT_MATERIAL_NAME_PREFIX = "NavisworksMaterial_";
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Speckle.Converter.Navisworks.Constants;
|
||||
|
||||
public static class PathConstants
|
||||
{
|
||||
public const char SEPARATOR = '/';
|
||||
public const string MATERIAL_SEPARATOR = "::";
|
||||
public const string SET_SEPARATOR = ">";
|
||||
}
|
||||
+2
-9
@@ -4,15 +4,8 @@ namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||
|
||||
public class ClassPropertiesExtractor
|
||||
{
|
||||
public Dictionary<string, object?>? GetClassProperties(NAV.ModelItem modelItem)
|
||||
{
|
||||
if (modelItem == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelItem));
|
||||
}
|
||||
|
||||
return ExtractClassProperties(modelItem);
|
||||
}
|
||||
public Dictionary<string, object?> GetClassProperties(NAV.ModelItem modelItem) =>
|
||||
modelItem == null ? throw new ArgumentNullException(nameof(modelItem)) : ExtractClassProperties(modelItem);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts property sets from a NAV.ModelItem and adds them to a dictionary,
|
||||
|
||||
+15
-11
@@ -1,19 +1,23 @@
|
||||
using Speckle.Sdk.Models;
|
||||
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||
|
||||
public class DisplayValueExtractor(GeometryToSpeckleConverter geometryConverter)
|
||||
public class DisplayValueExtractor(
|
||||
GeometryToSpeckleConverter geometryConverter,
|
||||
IElementSelectionService elementSelectionService
|
||||
)
|
||||
{
|
||||
internal List<Base> GetDisplayValue(NAV.ModelItem modelItem) =>
|
||||
modelItem == null
|
||||
? throw new ArgumentNullException(nameof(modelItem))
|
||||
: !modelItem.HasGeometry
|
||||
? ([])
|
||||
: !IsElementVisible(modelItem)
|
||||
? []
|
||||
:
|
||||
// this can be meshes or the instance reference objects
|
||||
// the un transformed objects stored in a separate collection
|
||||
geometryConverter.Convert(modelItem);
|
||||
: !modelItem.HasGeometry || !elementSelectionService.IsVisible(modelItem)
|
||||
? []
|
||||
: GeometryConverter.Convert(modelItem);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying geometry converter for accessing cache statistics.
|
||||
/// </summary>
|
||||
internal GeometryToSpeckleConverter GeometryConverter { get; } =
|
||||
geometryConverter ?? throw new ArgumentNullException(nameof(geometryConverter));
|
||||
}
|
||||
|
||||
+2
-9
@@ -3,18 +3,11 @@ using Speckle.Converters.Common;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||
|
||||
public class ModelPropertiesExtractor
|
||||
public class ModelPropertiesExtractor(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
|
||||
{
|
||||
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
|
||||
|
||||
public ModelPropertiesExtractor(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
internal Dictionary<string, object?>? GetModelProperties(NAV.Model model)
|
||||
{
|
||||
if (_settingsStore.Current.User.ExcludeProperties)
|
||||
if (settingsStore.Current.User.ExcludeProperties)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class PropertySetsExtractor(
|
||||
|
||||
/// <summary>
|
||||
/// Extracts property sets from a NAV.ModelItem and adds them to a dictionary,
|
||||
/// PropertySets are specific to the host application source appended to Navisworks and therefore
|
||||
/// PropertySets are the specific set per host application source appended to Navisworks and therefore
|
||||
/// arbitrary in nature.
|
||||
/// </summary>
|
||||
/// <param name="modelItem">The NAV.ModelItem from which property sets are extracted.</param>
|
||||
|
||||
+6
@@ -28,6 +28,12 @@ public class RevitBuiltInCategoryExtractor(IPropertyConverter converter) : IRevi
|
||||
|
||||
converter.Reset();
|
||||
|
||||
// Check if item.Model is null before accessing Units
|
||||
if (item.Model == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert using per-object model units and current UI units
|
||||
var nameObj = converter.ConvertPropertyValue(v, item.Model.Units, item.DisplayName);
|
||||
var name = nameObj?.ToString();
|
||||
|
||||
+2
-9
@@ -78,15 +78,8 @@ public abstract class BasePropertyHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> CreatePropertyDictionary(Dictionary<string, object?> properties)
|
||||
{
|
||||
var propertyDict = new Dictionary<string, object?>();
|
||||
foreach (var prop in properties.Where(prop => IsValidPropertyValue(prop.Value)))
|
||||
{
|
||||
propertyDict[prop.Key] = prop.Value;
|
||||
}
|
||||
return propertyDict;
|
||||
}
|
||||
private static Dictionary<string, object?> CreatePropertyDictionary(Dictionary<string, object?> properties) =>
|
||||
properties.Where(prop => IsValidPropertyValue(prop.Value)).ToDictionary(prop => prop.Key, prop => prop.Value);
|
||||
|
||||
protected static bool IsValidPropertyValue(object? value) => value != null && !string.IsNullOrEmpty(value.ToString());
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public class HierarchicalPropertyHandler(
|
||||
|
||||
public override Dictionary<string, object?> GetProperties(NAV.ModelItem modelItem)
|
||||
{
|
||||
var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem) ?? [];
|
||||
var propertyDict = classPropertiesExtractor.GetClassProperties(modelItem);
|
||||
|
||||
// Interop-lite mapping for Revit built-in categories
|
||||
if (_mapRevit && revitCategoryExtractor.TryGetBuiltInCategory(modelItem, out var builtInCategory))
|
||||
|
||||
+10
-11
@@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Converter.Navisworks.Constants.Registers;
|
||||
using Speckle.Converter.Navisworks.Helpers;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
@@ -42,19 +43,17 @@ public static class NavisworksConverterServiceRegistration
|
||||
serviceCollection.AddScoped<PrimitiveProcessor>();
|
||||
serviceCollection.AddScoped<PropertySetsExtractor>();
|
||||
|
||||
// Register element selection service
|
||||
serviceCollection.AddScoped<ElementSelectionService>();
|
||||
|
||||
// Register geometry conversion
|
||||
serviceCollection.AddScoped<DisplayValueExtractor>();
|
||||
serviceCollection.AddScoped<GeometryToSpeckleConverter>();
|
||||
|
||||
// Register dual shared geometry stores for instancing pattern (.NET Framework compatible)
|
||||
// Store 1: For geometry definitions (Mesh, Curve, etc.) - Store 2: For InstanceDefinitionProxy objects
|
||||
serviceCollection.AddScoped<InstanceStoreManager>();
|
||||
|
||||
// Register ISharedGeometryStore interface using the geometry definitions store for backward compatibility
|
||||
serviceCollection.AddScoped<ISharedGeometryStore>(provider =>
|
||||
provider.GetRequiredService<InstanceStoreManager>().GeometryDefinitionsStore
|
||||
);
|
||||
|
||||
serviceCollection.AddScoped<GeometryToSpeckleConverter>(sp =>
|
||||
{
|
||||
var settingsStore = sp.GetRequiredService<IConverterSettingsStore<NavisworksConversionSettings>>();
|
||||
var registry = sp.GetRequiredService<IInstanceFragmentRegistry>();
|
||||
return new GeometryToSpeckleConverter(settingsStore.Current, registry);
|
||||
});
|
||||
// Register settings resolved from factory
|
||||
serviceCollection.AddScoped<NavisworksConversionSettings>(sp =>
|
||||
sp.GetRequiredService<INavisworksConversionSettingsFactory>().Current
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Converter.Navisworks.Geometry;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Geometry;
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
|
||||
public readonly struct SafeBoundingBox
|
||||
{
|
||||
public SafeVertex Center { get; }
|
||||
@@ -103,6 +106,7 @@ public readonly struct SafeVertex
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
|
||||
public readonly struct SafePoint
|
||||
{
|
||||
public SafeVertex Vertex { get; }
|
||||
|
||||
+5
-22
@@ -1,4 +1,4 @@
|
||||
using Speckle.Converter.Navisworks.Constants;
|
||||
using static Speckle.Converter.Navisworks.Constants.PathConstants;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Helpers;
|
||||
|
||||
@@ -29,7 +29,7 @@ public static class ElementSelectionHelper
|
||||
var pathIndex =
|
||||
modelItemPathId.PathId == "a"
|
||||
? $"{modelItemPathId.ModelIndex}" // Root-level model item
|
||||
: $"{modelItemPathId.ModelIndex}{PathConstants.SEPARATOR}{modelItemPathId.PathId}"; // Nested model item
|
||||
: $"{modelItemPathId.ModelIndex}{SEPARATOR}{modelItemPathId.PathId}"; // Nested model item
|
||||
|
||||
return pathIndex;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public static class ElementSelectionHelper
|
||||
throw new ArgumentNullException(nameof(indexPath));
|
||||
}
|
||||
|
||||
int separatorIndex = indexPath.IndexOf(PathConstants.MATERIAL_SEPARATOR, StringComparison.Ordinal);
|
||||
int separatorIndex = indexPath.IndexOf(MATERIAL_SEPARATOR, StringComparison.Ordinal);
|
||||
return separatorIndex > 0 ? indexPath[..separatorIndex] : indexPath;
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ public static class ElementSelectionHelper
|
||||
// Extract just the path part if the indexPath contains a material signature
|
||||
string pathToResolve = GetCleanPath(indexPath);
|
||||
|
||||
var indexPathParts = pathToResolve.Split(PathConstants.SEPARATOR);
|
||||
var indexPathParts = pathToResolve.Split(SEPARATOR);
|
||||
|
||||
var modelIndex = int.Parse(indexPathParts[0]);
|
||||
var pathId = string.Join(PathConstants.SEPARATOR.ToString(), indexPathParts.Skip(1));
|
||||
var pathId = string.Join(SEPARATOR.ToString(), indexPathParts.Skip(1));
|
||||
|
||||
// assign the first part of indexPathParts to modelIndex and parse it to int, the second part to pathId string
|
||||
NAV.DocumentParts.ModelItemPathId modelItemPathId = new() { ModelIndex = modelIndex, PathId = pathId };
|
||||
@@ -72,23 +72,6 @@ public static class ElementSelectionHelper
|
||||
return modelItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a Navisworks <see cref="NAV.ModelItem"/> and all its ancestors are visible.
|
||||
/// </summary>
|
||||
/// <param name="modelItem">The model item to check for visibility.</param>
|
||||
/// <returns>True if the item and all ancestors are visible; otherwise, false.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="modelItem"/> is null.</exception>
|
||||
public static bool IsElementVisible(NAV.ModelItem modelItem)
|
||||
{
|
||||
if (modelItem == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelItem));
|
||||
}
|
||||
|
||||
// Check visibility status for the item and its ancestors
|
||||
return modelItem.AncestorsAndSelf.All(item => !item.IsHidden);
|
||||
}
|
||||
|
||||
public static IEnumerable<NAV.ModelItem> ResolveGeometryLeafNodes(NAV.ModelItem modelItem) =>
|
||||
modelItem.DescendantsAndSelf.Where(x => x.HasGeometry);
|
||||
}
|
||||
|
||||
+304
-2
@@ -1,4 +1,24 @@
|
||||
namespace Speckle.Converter.Navisworks.Helpers;
|
||||
using Speckle.Objects.Geometry;
|
||||
|
||||
// ReSharper disable UnusedMember.Local
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Helpers;
|
||||
|
||||
public readonly record struct Aabb(double MinX, double MinY, double MinZ, double MaxX, double MaxY, double MaxZ)
|
||||
{
|
||||
private static bool IsNearlyZero(double value, double epsilon = 1e-9) => Math.Abs(value) <= epsilon;
|
||||
|
||||
// public bool IsValid => !(MinX == 0 && MinY == 0 && MinZ == 0 && MaxX == 0 && MaxY == 0 && MaxZ == 0);
|
||||
public bool IsValid =>
|
||||
!(
|
||||
IsNearlyZero(MinX)
|
||||
&& IsNearlyZero(MinY)
|
||||
&& IsNearlyZero(MinZ)
|
||||
&& IsNearlyZero(MaxX)
|
||||
&& IsNearlyZero(MaxY)
|
||||
&& IsNearlyZero(MaxZ)
|
||||
);
|
||||
}
|
||||
|
||||
public static class GeometryHelpers
|
||||
{
|
||||
@@ -7,10 +27,292 @@ public static class GeometryHelpers
|
||||
/// </summary>
|
||||
/// <param name="vectorA">The first comparison vector.</param>
|
||||
/// <param name="vectorB">The second comparison vector.</param>
|
||||
/// <param name="tolerance">The tolerance value for the comparison. Default is 1e-9.</param>
|
||||
/// <param name="tolerance">The tolerance value for the comparison. The default is 1e-9.</param>
|
||||
/// <returns>True if the vectors match within the tolerance; otherwise, false.</returns>
|
||||
internal static bool VectorMatch(NAV.Vector3D vectorA, NAV.Vector3D vectorB, double tolerance = 1e-9) =>
|
||||
Math.Abs(vectorA.X - vectorB.X) < tolerance
|
||||
&& Math.Abs(vectorA.Y - vectorB.Y) < tolerance
|
||||
&& Math.Abs(vectorA.Z - vectorB.Z) < tolerance;
|
||||
|
||||
internal static double[] InvertRigid(double[] m)
|
||||
{
|
||||
// Rigid: [ R 0; t 1 ] in row major
|
||||
// inv = [ R^T 0; -t*R^T 1 ]
|
||||
var inv = new double[16];
|
||||
|
||||
// transpose 3x3 rotation
|
||||
inv[0] = m[0];
|
||||
inv[1] = m[4];
|
||||
inv[2] = m[8];
|
||||
inv[3] = 0;
|
||||
inv[4] = m[1];
|
||||
inv[5] = m[5];
|
||||
inv[6] = m[9];
|
||||
inv[7] = 0;
|
||||
inv[8] = m[2];
|
||||
inv[9] = m[6];
|
||||
inv[10] = m[10];
|
||||
inv[11] = 0;
|
||||
|
||||
var tx = m[12];
|
||||
var ty = m[13];
|
||||
var tz = m[14];
|
||||
|
||||
// -t * R^T
|
||||
inv[12] = -(tx * inv[0] + ty * inv[4] + tz * inv[8]);
|
||||
inv[13] = -(tx * inv[1] + ty * inv[5] + tz * inv[9]);
|
||||
inv[14] = -(tx * inv[2] + ty * inv[6] + tz * inv[10]);
|
||||
inv[15] = 1;
|
||||
|
||||
return inv;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two 4x4 matrices in row-major order.
|
||||
/// Used to compute instance transforms: inverse(definitionWorld) × instanceWorld
|
||||
/// </summary>
|
||||
internal static double[] MultiplyMatrices4X4(double[] a, double[] b)
|
||||
{
|
||||
var result = new double[16];
|
||||
for (int row = 0; row < 4; row++)
|
||||
{
|
||||
for (int col = 0; col < 4; col++)
|
||||
{
|
||||
double sum = 0;
|
||||
for (int k = 0; k < 4; k++)
|
||||
{
|
||||
sum += a[row * 4 + k] * b[k * 4 + col];
|
||||
}
|
||||
result[row * 4 + col] = sum;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TransformPointInPlace(double[] m, ref double x, ref double y, ref double z)
|
||||
{
|
||||
var nx = x * m[0] + y * m[4] + z * m[8] + m[12];
|
||||
var ny = x * m[1] + y * m[5] + z * m[9] + m[13];
|
||||
var nz = x * m[2] + y * m[6] + z * m[10] + m[14];
|
||||
x = nx;
|
||||
y = ny;
|
||||
z = nz;
|
||||
}
|
||||
|
||||
// Used, for instance, validation - unbakes geometry from world space to definition space
|
||||
internal static void UnbakeMeshVertices(Mesh mesh, double[] invWorld)
|
||||
{
|
||||
for (int i = 0; i < mesh.vertices.Count; i += 3)
|
||||
{
|
||||
double x = mesh.vertices[i];
|
||||
double y = mesh.vertices[i + 1];
|
||||
double z = mesh.vertices[i + 2];
|
||||
|
||||
TransformPointInPlace(invWorld, ref x, ref y, ref z);
|
||||
|
||||
mesh.vertices[i] = x;
|
||||
mesh.vertices[i + 1] = y;
|
||||
mesh.vertices[i + 2] = z;
|
||||
}
|
||||
}
|
||||
|
||||
// Used, for instance, validation - unbakes geometry from world space to definition space
|
||||
internal static void UnbakeLine(Line line, double[] invWorld)
|
||||
{
|
||||
double sx = line.start.x,
|
||||
sy = line.start.y,
|
||||
sz = line.start.z;
|
||||
double ex = line.end.x,
|
||||
ey = line.end.y,
|
||||
ez = line.end.z;
|
||||
|
||||
TransformPointInPlace(invWorld, ref sx, ref sy, ref sz);
|
||||
TransformPointInPlace(invWorld, ref ex, ref ey, ref ez);
|
||||
|
||||
line.start.x = sx;
|
||||
line.start.y = sy;
|
||||
line.start.z = sz;
|
||||
line.end.x = ex;
|
||||
line.end.y = ey;
|
||||
line.end.z = ez;
|
||||
}
|
||||
|
||||
internal static Aabb Aabb(Mesh mesh)
|
||||
{
|
||||
double minX = double.PositiveInfinity,
|
||||
minY = double.PositiveInfinity,
|
||||
minZ = double.PositiveInfinity;
|
||||
double maxX = double.NegativeInfinity,
|
||||
maxY = double.NegativeInfinity,
|
||||
maxZ = double.NegativeInfinity;
|
||||
|
||||
for (int i = 0; i < mesh.vertices.Count; i += 3)
|
||||
{
|
||||
var x = mesh.vertices[i];
|
||||
var y = mesh.vertices[i + 1];
|
||||
var z = mesh.vertices[i + 2];
|
||||
|
||||
if (x < minX)
|
||||
{
|
||||
minX = x;
|
||||
}
|
||||
|
||||
if (y < minY)
|
||||
{
|
||||
minY = y;
|
||||
}
|
||||
|
||||
if (z < minZ)
|
||||
{
|
||||
minZ = z;
|
||||
}
|
||||
|
||||
if (x > maxX)
|
||||
{
|
||||
maxX = x;
|
||||
}
|
||||
|
||||
if (y > maxY)
|
||||
{
|
||||
maxY = y;
|
||||
}
|
||||
|
||||
if (z > maxZ)
|
||||
{
|
||||
maxZ = z;
|
||||
}
|
||||
}
|
||||
|
||||
return new Aabb(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
|
||||
private static bool NearlyEqual(double a, double b, double eps) => Math.Abs(a - b) <= eps;
|
||||
|
||||
private static bool AabbEqual(Aabb a, Aabb b, double eps) =>
|
||||
NearlyEqual(a.MinX, b.MinX, eps)
|
||||
&& NearlyEqual(a.MinY, b.MinY, eps)
|
||||
&& NearlyEqual(a.MinZ, b.MinZ, eps)
|
||||
&& NearlyEqual(a.MaxX, b.MaxX, eps)
|
||||
&& NearlyEqual(a.MaxY, b.MaxY, eps)
|
||||
&& NearlyEqual(a.MaxZ, b.MaxZ, eps);
|
||||
|
||||
internal static Aabb ComputeUnbakedAabb(PrimitiveProcessor processor, double[] invWorld)
|
||||
{
|
||||
var hasAny = false;
|
||||
|
||||
double minX = double.PositiveInfinity,
|
||||
minY = double.PositiveInfinity,
|
||||
minZ = double.PositiveInfinity;
|
||||
double maxX = double.NegativeInfinity,
|
||||
maxY = double.NegativeInfinity,
|
||||
maxZ = double.NegativeInfinity;
|
||||
|
||||
void AddPoint(double x, double y, double z)
|
||||
{
|
||||
TransformPointInPlace(invWorld, ref x, ref y, ref z);
|
||||
|
||||
hasAny = true;
|
||||
if (x < minX)
|
||||
{
|
||||
minX = x;
|
||||
}
|
||||
|
||||
if (y < minY)
|
||||
{
|
||||
minY = y;
|
||||
}
|
||||
|
||||
if (z < minZ)
|
||||
{
|
||||
minZ = z;
|
||||
}
|
||||
|
||||
if (x > maxX)
|
||||
{
|
||||
maxX = x;
|
||||
}
|
||||
|
||||
if (y > maxY)
|
||||
{
|
||||
maxY = y;
|
||||
}
|
||||
|
||||
if (z > maxZ)
|
||||
{
|
||||
maxZ = z;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var t in processor.Triangles)
|
||||
{
|
||||
AddPoint(t.Vertex1.X, t.Vertex1.Y, t.Vertex1.Z);
|
||||
AddPoint(t.Vertex2.X, t.Vertex2.Y, t.Vertex2.Z);
|
||||
AddPoint(t.Vertex3.X, t.Vertex3.Y, t.Vertex3.Z);
|
||||
}
|
||||
|
||||
foreach (var l in processor.Lines)
|
||||
{
|
||||
AddPoint(l.Start.X, l.Start.Y, l.Start.Z);
|
||||
AddPoint(l.End.X, l.End.Y, l.End.Z);
|
||||
}
|
||||
|
||||
return hasAny ? new Aabb(minX, minY, minZ, maxX, maxY, maxZ) : default;
|
||||
}
|
||||
|
||||
private static void Acc(
|
||||
double[] m,
|
||||
double x,
|
||||
double y,
|
||||
double z,
|
||||
ref double minX,
|
||||
ref double minY,
|
||||
ref double minZ,
|
||||
ref double maxX,
|
||||
ref double maxY,
|
||||
ref double maxZ
|
||||
)
|
||||
{
|
||||
// apply transform (row major with translation at 12,13,14 as per your usage)
|
||||
var nx = x * m[0] + y * m[4] + z * m[8] + m[12];
|
||||
var ny = x * m[1] + y * m[5] + z * m[9] + m[13];
|
||||
var nz = x * m[2] + y * m[6] + z * m[10] + m[14];
|
||||
|
||||
if (nx < minX)
|
||||
{
|
||||
minX = nx;
|
||||
}
|
||||
|
||||
if (ny < minY)
|
||||
{
|
||||
minY = ny;
|
||||
}
|
||||
|
||||
if (nz < minZ)
|
||||
{
|
||||
minZ = nz;
|
||||
}
|
||||
|
||||
if (nx > maxX)
|
||||
{
|
||||
maxX = nx;
|
||||
}
|
||||
|
||||
if (ny > maxY)
|
||||
{
|
||||
maxY = ny;
|
||||
}
|
||||
|
||||
if (nz > maxZ)
|
||||
{
|
||||
maxZ = nz;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool NearlyEqual(Aabb a, Aabb b, double eps) =>
|
||||
Math.Abs(a.MinX - b.MinX) <= eps
|
||||
&& Math.Abs(a.MinY - b.MinY) <= eps
|
||||
&& Math.Abs(a.MinZ - b.MinZ) <= eps
|
||||
&& Math.Abs(a.MaxX - b.MaxX) <= eps
|
||||
&& Math.Abs(a.MaxY - b.MaxY) <= eps
|
||||
&& Math.Abs(a.MaxZ - b.MaxZ) <= eps;
|
||||
}
|
||||
|
||||
+12
-6
@@ -5,6 +5,11 @@ using Speckle.DoubleNumerics;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Callback processor for Navisworks COM primitive generation.
|
||||
/// WARNING: COM interop bottleneck - fragment.GenerateSimplePrimitives() has significant marshaling overhead.
|
||||
/// WARNING: InwSimpleVertex.coord returns Array (COM object) requiring 3 GetValue() calls per vertex.
|
||||
/// </summary>
|
||||
public class PrimitiveProcessor : InwSimplePrimitivesCB
|
||||
{
|
||||
private readonly List<double> _coords = [];
|
||||
@@ -52,13 +57,13 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB
|
||||
var safeLine = new SafeLine(vD1, vD2);
|
||||
AddLine(safeLine);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
catch (ArgumentException)
|
||||
{
|
||||
Console.WriteLine($"ArgumentException caught: {ex.Message}");
|
||||
// Invalid line geometry - skip
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Console.WriteLine($"InvalidOperationException caught: {ex.Message}");
|
||||
// Invalid line geometry - skip
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +83,6 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB
|
||||
AddPoint(safePoint);
|
||||
}
|
||||
|
||||
// TODO: Needed for Splines
|
||||
public void SnapPoint(InwSimpleVertex? v1) => Point(v1);
|
||||
|
||||
public void Triangle(InwSimpleVertex? v1, InwSimpleVertex? v2, InwSimpleVertex? v3)
|
||||
@@ -101,7 +105,6 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB
|
||||
IsUpright
|
||||
);
|
||||
|
||||
// Capture values immediately in our safe struct
|
||||
var safeTriangle = new SafeTriangle(vD1, vD2, vD3);
|
||||
|
||||
var indexPointer = Faces.Count;
|
||||
@@ -165,6 +168,9 @@ public class PrimitiveProcessor : InwSimplePrimitivesCB
|
||||
return new NAV.Vector3D(vectorDoubleX, vectorDoubleY, vectorDoubleZ);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WARNING: Called for every vertex - COM marshaling overhead from Array cast and 3 GetValue() calls.
|
||||
/// </summary>
|
||||
private static Vector3 VectorFromVertex(InwSimpleVertex v)
|
||||
{
|
||||
var arrayV = (Array)v.coord;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace Speckle.Converter.Navisworks.Constants;
|
||||
|
||||
public static class PathConstants
|
||||
{
|
||||
public const char SEPARATOR = '/';
|
||||
public const string MATERIAL_SEPARATOR = "::";
|
||||
public const string SET_SEPARATOR = ">";
|
||||
}
|
||||
|
||||
public static class InstanceConstants
|
||||
{
|
||||
public const string GEOMETRY_ID_PREFIX = "geom_";
|
||||
public const string DEFINITION_ID_PREFIX = "def_";
|
||||
}
|
||||
|
||||
public static class MaterialConstants
|
||||
{
|
||||
public const string DEFAULT_MATERIAL_NAME_PREFIX = "NavisworksMaterial_";
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
namespace Speckle.Converter.Navisworks.Paths;
|
||||
|
||||
public readonly record struct PathKey
|
||||
{
|
||||
internal readonly int[] Data;
|
||||
internal readonly int Hash;
|
||||
|
||||
public static readonly IEqualityComparer<PathKey> Comparer = new PathKeyComparer();
|
||||
|
||||
private PathKey(int[] data)
|
||||
{
|
||||
this.Data = data ?? throw new ArgumentNullException(nameof(data));
|
||||
Hash = ComputeHash(data);
|
||||
}
|
||||
|
||||
public static PathKey FromComArray(Array arr)
|
||||
{
|
||||
if (arr == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(arr));
|
||||
}
|
||||
|
||||
if (arr.Rank != 1)
|
||||
{
|
||||
throw new ArgumentException("Expected 1D array.", nameof(arr));
|
||||
}
|
||||
|
||||
int lb = arr.GetLowerBound(0);
|
||||
int len = arr.GetLength(0);
|
||||
|
||||
var data = new int[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
data[i] = (int)arr.GetValue(lb + i);
|
||||
}
|
||||
|
||||
return new PathKey(data);
|
||||
}
|
||||
|
||||
private static int ComputeHash(int[] data)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int h = 17;
|
||||
|
||||
// ReSharper disable once ForCanBeConvertedToForeach
|
||||
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
h = h * 31 + data[i];
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchesComArray(Array arr)
|
||||
{
|
||||
if (Data == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arr.Rank != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int lb = arr.GetLowerBound(0);
|
||||
int len = arr.GetLength(0);
|
||||
if (len != Data.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if ((int)arr.GetValue(lb + i) != Data[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Data == null || Data.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return string.Join(",", Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a compact string representation using the hash value as an unsigned integer.
|
||||
/// Suitable for use as application IDs and definition IDs.
|
||||
/// This avoids negative numbers in IDs by treating the hash as unsigned.
|
||||
/// </summary>
|
||||
public string ToHashString() => unchecked((uint)Hash).ToString();
|
||||
}
|
||||
|
||||
internal sealed class PathKeyComparer : IEqualityComparer<PathKey>
|
||||
{
|
||||
public bool Equals(PathKey x, PathKey y)
|
||||
{
|
||||
if (ReferenceEquals(x.Data, y.Data))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x.Data.Length != y.Data.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||
for (int i = 0; i < x.Data.Length; i++)
|
||||
{
|
||||
if (x.Data[i] != y.Data[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(PathKey obj) => obj.Hash;
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
using Speckle.Converter.Navisworks.Helpers;
|
||||
using Speckle.Converter.Navisworks.Paths;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Constants.Registers;
|
||||
|
||||
public interface IInstanceFragmentRegistry
|
||||
{
|
||||
bool TryGetGroup(PathKey instancePath, out PathKey groupKey);
|
||||
void RegisterGroup(PathKey groupKey, HashSet<PathKey> instancePaths);
|
||||
void MarkConverted(PathKey instancePath);
|
||||
|
||||
IEnumerable<PathKey> GetConvertedPaths();
|
||||
Dictionary<PathKey, List<PathKey>> BuildGroupToConvertedPaths();
|
||||
|
||||
bool TryGetDefinitionWorld(PathKey groupKey, out double[] definitionWorld);
|
||||
void EnsureDefinitionWorld(PathKey groupKey, double[] definitionWorld);
|
||||
|
||||
bool TryGetInstanceWorld(PathKey instancePath, out double[] instanceWorld);
|
||||
void SetInstanceWorld(PathKey instancePath, double[] instanceWorld);
|
||||
|
||||
bool HasDefinitionGeometry(PathKey groupKey);
|
||||
void StoreDefinitionGeometry(PathKey groupKey, List<Base> geometry);
|
||||
bool TryGetDefinitionGeometry(PathKey groupKey, out List<Base> geometry);
|
||||
|
||||
Dictionary<PathKey, List<Base>> GetAllDefinitionGeometries();
|
||||
List<PathKey> GetAllGroupKeys();
|
||||
|
||||
void RegisterInstanceObservation(
|
||||
PathKey groupKey,
|
||||
PathKey instancePath,
|
||||
double[] instanceWorld,
|
||||
PrimitiveProcessor processor
|
||||
);
|
||||
}
|
||||
|
||||
public sealed class InstanceFragmentRegistry : IInstanceFragmentRegistry
|
||||
{
|
||||
private readonly Dictionary<PathKey, PathKey> _pathToGroup = new(PathKey.Comparer);
|
||||
private readonly HashSet<PathKey> _converted = new(PathKey.Comparer);
|
||||
|
||||
private readonly Dictionary<PathKey, double[]> _groupToDefinitionWorld = new(PathKey.Comparer);
|
||||
private readonly Dictionary<PathKey, double[]> _pathToInstanceWorld = new(PathKey.Comparer);
|
||||
private readonly Dictionary<PathKey, Aabb> _groupSignature = new(PathKey.Comparer);
|
||||
private readonly Dictionary<PathKey, List<Base>> _groupDefinitions = new(PathKey.Comparer);
|
||||
|
||||
public bool TryGetGroup(PathKey instancePath, out PathKey groupKey) =>
|
||||
_pathToGroup.TryGetValue(instancePath, out groupKey);
|
||||
|
||||
public void RegisterGroup(PathKey groupKey, HashSet<PathKey> instancePaths)
|
||||
{
|
||||
foreach (var p in instancePaths)
|
||||
{
|
||||
_pathToGroup[p] = groupKey;
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkConverted(PathKey instancePath) => _converted.Add(instancePath);
|
||||
|
||||
public IEnumerable<PathKey> GetConvertedPaths() => _converted;
|
||||
|
||||
public Dictionary<PathKey, List<PathKey>> BuildGroupToConvertedPaths()
|
||||
{
|
||||
var map = new Dictionary<PathKey, List<PathKey>>(PathKey.Comparer);
|
||||
|
||||
foreach (var instancePath in _converted)
|
||||
{
|
||||
if (!_pathToGroup.TryGetValue(instancePath, out var groupKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!map.TryGetValue(groupKey, out var list))
|
||||
{
|
||||
list = [];
|
||||
map.Add(groupKey, list);
|
||||
}
|
||||
|
||||
list.Add(instancePath);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public bool TryGetDefinitionWorld(PathKey groupKey, out double[] definitionWorld) =>
|
||||
_groupToDefinitionWorld.TryGetValue(groupKey, out definitionWorld);
|
||||
|
||||
public void EnsureDefinitionWorld(PathKey groupKey, double[] definitionWorld)
|
||||
{
|
||||
if (!_groupToDefinitionWorld.ContainsKey(groupKey))
|
||||
{
|
||||
_groupToDefinitionWorld[groupKey] = definitionWorld;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetInstanceWorld(PathKey instancePath, out double[] instanceWorld) =>
|
||||
_pathToInstanceWorld.TryGetValue(instancePath, out instanceWorld);
|
||||
|
||||
public void SetInstanceWorld(PathKey instancePath, double[] instanceWorld) =>
|
||||
_pathToInstanceWorld[instancePath] = instanceWorld;
|
||||
|
||||
public bool HasDefinitionGeometry(PathKey groupKey) => _groupDefinitions.ContainsKey(groupKey);
|
||||
|
||||
public void StoreDefinitionGeometry(PathKey groupKey, List<Base> geometry) => _groupDefinitions[groupKey] = geometry;
|
||||
|
||||
public bool TryGetDefinitionGeometry(PathKey groupKey, out List<Base> geometry) =>
|
||||
_groupDefinitions.TryGetValue(groupKey, out geometry);
|
||||
|
||||
public Dictionary<PathKey, List<Base>> GetAllDefinitionGeometries() => new(_groupDefinitions, PathKey.Comparer);
|
||||
|
||||
public List<PathKey> GetAllGroupKeys() => _groupDefinitions.Keys.ToList();
|
||||
|
||||
public void RegisterInstanceObservation(
|
||||
PathKey groupKey,
|
||||
PathKey instancePath,
|
||||
double[] instanceWorld,
|
||||
PrimitiveProcessor processor
|
||||
)
|
||||
{
|
||||
if (instanceWorld == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instanceWorld));
|
||||
}
|
||||
|
||||
if (instanceWorld.Length != 16)
|
||||
{
|
||||
throw new ArgumentException("Expected 16 doubles for a 4x4 matrix.", nameof(instanceWorld));
|
||||
}
|
||||
|
||||
// Store instanceWorld for later retrieval (needed for unbaking validation)
|
||||
SetInstanceWorld(instancePath, instanceWorld);
|
||||
|
||||
var inv = GeometryHelpers.InvertRigid(instanceWorld);
|
||||
{
|
||||
var sig = GeometryHelpers.ComputeUnbakedAabb(processor, inv);
|
||||
|
||||
if (!sig.IsValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_groupSignature.TryGetValue(groupKey, out Aabb _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_groupSignature[groupKey] = sig;
|
||||
_groupToDefinitionWorld[groupKey] = instanceWorld;
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
2026-01-08 10:51:03.776 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 10:51:05.537 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 10:51:05.553 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 10:51:05.554 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 10:51:05.554 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 10:58:37.951 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 10:58:39.205 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 10:58:39.216 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 11:02:08.672 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 11:02:10.209 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 11:02:10.217 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 11:26:14.925 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 11:26:16.553 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 11:26:16.565 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 11:26:16.565 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 11:26:16.565 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 11:26:16.566 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 11:26:16.566 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 11:26:16.566 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 11:37:00.295 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 11:37:01.596 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 11:37:01.603 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 11:37:01.604 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 11:37:01.604 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 11:50:22.903 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 11:50:26.086 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 11:50:26.106 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 11:50:26.106 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 11:50:26.108 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 11:50:26.108 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 11:50:26.108 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 11:50:26.109 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 12:30:43.116 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 12:34:11.025 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 12:35:49.650 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 12:37:52.006 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 12:39:59.783 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 12:40:40.564 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 12:41:30.382 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 13:37:46.631 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 13:41:25.630 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 13:45:17.514 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 13:49:52.715 +04:00 [INF] Creating settings for document: P30339B-ASAB_MERGED_05-JAN-2026.nwd
|
||||
2026-01-08 14:13:08.532 +04:00 [INF] Initialized logger inside Navisworks 2024/3.13.2. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 14:13:13.320 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 14:13:13.331 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 14:19:54.036 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 14:19:55.983 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 14:19:55.997 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 14:19:55.997 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 14:19:55.997 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 14:19:55.998 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 14:19:55.998 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 14:19:55.998 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 14:20:41.471 +04:00 [INF] Initialized logger inside Navisworks 2024/3.10.0. Path info C:\Users\m.zewita\AppData\Roaming C:\Users\m.zewita\AppData\Roaming.
|
||||
2026-01-08 14:20:43.212 +04:00 [INF] Bridge bound to front end name topLevelExceptionHandlerBinding
|
||||
2026-01-08 14:20:43.234 +04:00 [INF] Bridge bound to front end name testBinding
|
||||
2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name configBinding
|
||||
2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name accountsBinding
|
||||
2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name selectionBinding
|
||||
2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name sendBinding
|
||||
2026-01-08 14:20:43.235 +04:00 [INF] Bridge bound to front end name baseBinding
|
||||
2026-01-08 14:21:39.602 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:21:50.199 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:22:09.387 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:22:19.184 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:22:48.117 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:22:50.623 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:22:51.498 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:23:20.216 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:24:33.297 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:24:34.115 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:24:34.619 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:24:40.552 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:25:02.785 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:29:09.648 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:29:57.011 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:30:35.781 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:31:57.387 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
2026-01-08 14:33:59.100 +04:00 [ERR] Speckle.Connectors.DUI.Bindings.SendOperationManager operation was not successful
|
||||
System.InvalidOperationException: Sequence contains no matching element
|
||||
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at Speckle.Connector.Navisworks.Operations.Send.Settings.ToSpeckleSettingsManagerNavisworks.GetVisualRepresentationMode(SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Operations/Send/Settings/ToSpeckleSettingsManagerNavisworks.cs:line 35
|
||||
at Speckle.Connector.Navisworks.Bindings.NavisworksSendBinding.InitializeConverterSettings(IServiceProvider serviceProvider, SenderModelCard modelCard) in /_/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Bindings/NavisworksSendBinding.cs:line 89
|
||||
at Speckle.Connectors.DUI.Bindings.SendOperationManager.<Process>d__11`1.MoveNext() in /_/DUI3/Speckle.Connectors.DUI/Bindings/SendOperationManager.cs:line 71
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Services;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public class ElementSelectionService : IElementSelectionService
|
||||
{
|
||||
private readonly Dictionary<Guid, bool> _visibleCache = new();
|
||||
|
||||
public string GetModelItemPath(NAV.ModelItem modelItem) => ResolveModelItemToIndexPath(modelItem);
|
||||
|
||||
public NAV.ModelItem GetModelItemFromPath(string path) => ResolveIndexPathToModelItem(path);
|
||||
|
||||
public bool IsVisible(NAV.ModelItem modelItem)
|
||||
{
|
||||
var key = modelItem.InstanceGuid;
|
||||
if (_visibleCache.TryGetValue(key, out var isVisible))
|
||||
{
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
// Check and cache ancestors, short-circuit on first hidden
|
||||
foreach (var item in modelItem.AncestorsAndSelf)
|
||||
{
|
||||
if (!_visibleCache.TryGetValue(item.InstanceGuid, out var visible))
|
||||
{
|
||||
visible = !item.IsHidden;
|
||||
_visibleCache[item.InstanceGuid] = visible;
|
||||
}
|
||||
|
||||
if (!visible) // Ancestor is hidden, item must be hidden
|
||||
{
|
||||
// Cache the result for this item too
|
||||
_visibleCache[key] = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// All ancestors visible
|
||||
_visibleCache[key] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<NAV.ModelItem> GetGeometryNodes(NAV.ModelItem modelItem) => ResolveGeometryLeafNodes(modelItem);
|
||||
}
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
// using Microsoft.Extensions.Logging;
|
||||
using Speckle.Converter.Navisworks.Constants;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Simple wrapper class that manages two SharedGeometryStores instances for dual instancing pattern.
|
||||
/// Provides easy access to both mesh definitions store and instance definition proxies store.
|
||||
/// </summary>
|
||||
public class InstanceStoreManager(
|
||||
// ILogger<InstanceStoreManager> logger
|
||||
)
|
||||
{
|
||||
// private readonly ILogger<InstanceStoreManager> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
/// <summary>
|
||||
/// Store for geometry definitions (geometry data) - untransformed base geometries.
|
||||
/// </summary>
|
||||
internal SharedGeometryStore GeometryDefinitionsStore { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Store for InstanceDefinitionProxy objects that reference geometry definitions.
|
||||
/// </summary>
|
||||
internal SharedGeometryStore InstanceDefinitionProxiesStore { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clears both stores for a new conversion session.
|
||||
/// Should be called at the start of each conversion.
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
GeometryDefinitionsStore.Clear();
|
||||
InstanceDefinitionProxiesStore.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all instance definition proxies from the store, cast to their specific type.
|
||||
/// Useful for adding to root collection at end of conversion.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<InstanceDefinitionProxy> GetInstanceDefinitionProxies()
|
||||
{
|
||||
var proxies = InstanceDefinitionProxiesStore.Geometries.OfType<InstanceDefinitionProxy>().ToList().AsReadOnly();
|
||||
// _logger.LogDebug("GetInstanceDefinitionProxies returning {Count} proxies", proxies.Count);
|
||||
return proxies;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all geometry definitions from the geometry definitions store.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<Base> GetGeometryDefinitions() => [.. GeometryDefinitionsStore.Geometries.ToList().AsReadOnly()];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a geometry definition by its application ID from the geometry definitions store.
|
||||
/// </summary>
|
||||
/// <returns>The geometry if found, null otherwise.</returns>
|
||||
public Base? GetGeometryDefinition(string fragmentId) =>
|
||||
GeometryDefinitionsStore.Geometries.FirstOrDefault(g =>
|
||||
g.applicationId == $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an instance definition proxy by its application ID.
|
||||
/// </summary>
|
||||
/// <returns>The instance definition proxy if found, null otherwise.</returns>
|
||||
public InstanceDefinitionProxy? GetInstanceDefinitionProxy(string fragmentId) =>
|
||||
InstanceDefinitionProxiesStore
|
||||
.Geometries.OfType<InstanceDefinitionProxy>()
|
||||
.FirstOrDefault(p => p.applicationId == $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}");
|
||||
|
||||
/// <summary>
|
||||
/// Adds geometry definitions and corresponding instance definition proxy for shared geometry.
|
||||
/// This is a convenience method that handles both stores in one call.
|
||||
/// Supports all geometry primitive types (Mesh, Lines, Points).
|
||||
/// </summary>
|
||||
/// <param name="fragmentId">The fragment-based application ID.</param>
|
||||
/// <param name="geometries">The untransformed base geometries (meshes, lines, points).</param>
|
||||
/// <returns>True if geometries were added (new geometry), false if they already existed.</returns>
|
||||
public bool AddSharedGeometry(string fragmentId, List<Base> geometries)
|
||||
{
|
||||
// _logger.LogDebug("AddSharedGeometry called for FragmentId={FragmentId}, GeometryCount={Count}", fragmentId, geometries.Count);
|
||||
|
||||
if (geometries.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var geometriesAdded = false;
|
||||
var proxyAdded = false;
|
||||
|
||||
// Create prefixed IDs using base fragment hash
|
||||
var definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}";
|
||||
var geometryApplicationIds = new List<string>();
|
||||
|
||||
// _logger.LogDebug("Using DefinitionId={DefinitionId}", definitionId);
|
||||
|
||||
// Add each geometry definition with a unique index suffix
|
||||
for (var i = 0; i < geometries.Count; i++)
|
||||
{
|
||||
var geometry = geometries[i];
|
||||
var geometryId =
|
||||
geometries.Count == 1
|
||||
? $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}"
|
||||
: $"{InstanceConstants.GEOMETRY_ID_PREFIX}{fragmentId}_{i}";
|
||||
|
||||
if (!GeometryDefinitionsStore.Contains(geometryId))
|
||||
{
|
||||
geometry.applicationId = geometryId;
|
||||
var added = GeometryDefinitionsStore.Add(geometry);
|
||||
geometriesAdded = geometriesAdded || added;
|
||||
// _logger.LogDebug("Added geometry definition: {GeometryId}, Type={Type}, Success={Success}", geometryId, geometry.GetType().Name, added);
|
||||
}
|
||||
else
|
||||
{
|
||||
// _logger.LogDebug("Geometry definition already exists: {GeometryId}", geometryId);
|
||||
}
|
||||
|
||||
geometryApplicationIds.Add(geometryId);
|
||||
}
|
||||
|
||||
// Add instance definition proxy if not exists
|
||||
if (!InstanceDefinitionProxiesStore.Contains(definitionId))
|
||||
{
|
||||
var definitionProxy = new InstanceDefinitionProxy
|
||||
{
|
||||
applicationId = definitionId,
|
||||
name = $"Shared Geometry {fragmentId[..8]}...", // Show first 8 chars for readability
|
||||
objects = geometryApplicationIds,
|
||||
maxDepth = 0
|
||||
};
|
||||
proxyAdded = InstanceDefinitionProxiesStore.Add(definitionProxy);
|
||||
}
|
||||
else
|
||||
{
|
||||
// _logger.LogDebug("Instance definition proxy already exists: {DefinitionId}", definitionId);
|
||||
}
|
||||
|
||||
var conversionSucceededResult = geometriesAdded || proxyAdded;
|
||||
return conversionSucceededResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if shared geometry already exists in the stores.
|
||||
/// Uses the instance definition proxy as the authoritative check since it references all geometries.
|
||||
/// </summary>
|
||||
/// <param name="fragmentId">The fragment-based application ID.</param>
|
||||
/// <returns>True if the instance definition proxy exists for this fragment.</returns>
|
||||
public bool ContainsSharedGeometry(string fragmentId) =>
|
||||
InstanceDefinitionProxiesStore.Contains($"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}");
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ public class PropertyConverter(IUiUnitsCache uiUnitsCache) : IPropertyConverter
|
||||
{ NAV.VariantDataType.IdentifierString, (v, _) => v.ToIdentifierString() },
|
||||
{ NAV.VariantDataType.Int32, (v, _) => v.ToInt32() },
|
||||
{ NAV.VariantDataType.Double, (v, _) => v.ToDouble() },
|
||||
// Angle as dictionary with units
|
||||
// Angle as a dictionary with units
|
||||
{ NAV.VariantDataType.DoubleAngle, (v, t) => NumObj(t.name, v.ToDoubleAngle(), "Degrees") },
|
||||
// Length → dictionary in UI units
|
||||
{
|
||||
|
||||
+13
-18
@@ -2,28 +2,23 @@
|
||||
|
||||
namespace Speckle.Converter.Navisworks.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the settings used for Navisworks conversions.
|
||||
/// </summary>
|
||||
public record NavisworksConversionSettings(Derived Derived, User User);
|
||||
|
||||
// Derived from Navisworks Application
|
||||
public record Derived(
|
||||
NAV.Document Document, // The active Navisworks document to be processed.
|
||||
SafeBoundingBox ModelBoundingBox, // The bounding box of the model.
|
||||
SafeVector TransformVector, // Transformation vector applied to the model.
|
||||
bool IsUpright, // Indicates if the model's orientation is upright relative to canonical up.
|
||||
string SpeckleUnits // Units used in Speckle for standardised measurements.
|
||||
NAV.Document Document,
|
||||
SafeBoundingBox ModelBoundingBox,
|
||||
SafeVector TransformVector,
|
||||
bool IsUpright,
|
||||
string SpeckleUnits
|
||||
);
|
||||
|
||||
// Optional settings for conversion to be offered in UI
|
||||
public record User(
|
||||
OriginMode OriginMode, // Defines the base point for transformations.
|
||||
bool IncludeInternalProperties, // Whether to include internal Navisworks properties in the output.
|
||||
bool ConvertHiddenElements, // Whether to include hidden elements during the conversion process.
|
||||
RepresentationMode VisualRepresentationMode, // Specifies the visual representation mode.
|
||||
bool CoalescePropertiesFromFirstObjectAncestor, // Whether to merge properties from the first object ancestor.
|
||||
bool ExcludeProperties, // Whether to exclude properties from the output.
|
||||
bool PreserveModelHierarchy, // Whether to maintain the full model hierarchy during conversion.
|
||||
bool RevitCategoryMapping // Optional mapping to Revit categories (if applicable).
|
||||
OriginMode OriginMode,
|
||||
bool IncludeInternalProperties,
|
||||
bool ConvertHiddenElements,
|
||||
RepresentationMode VisualRepresentationMode,
|
||||
bool CoalescePropertiesFromFirstObjectAncestor,
|
||||
bool ExcludeProperties,
|
||||
bool PreserveModelHierarchy,
|
||||
bool RevitCategoryMapping = true
|
||||
);
|
||||
|
||||
+10
-49
@@ -7,39 +7,21 @@ using Speckle.InterfaceGenerator;
|
||||
namespace Speckle.Converter.Navisworks.Settings;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public class NavisworksConversionSettingsFactory : INavisworksConversionSettingsFactory
|
||||
public class NavisworksConversionSettingsFactory(
|
||||
IHostToSpeckleUnitConverter<NAV.Units> unitsConverter,
|
||||
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
|
||||
ILogger<NavisworksConversionSettingsFactory> logger
|
||||
) : INavisworksConversionSettingsFactory
|
||||
{
|
||||
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
|
||||
private readonly ILogger<NavisworksConversionSettingsFactory> _logger;
|
||||
private readonly IHostToSpeckleUnitConverter<NAV.Units> _unitsConverter;
|
||||
|
||||
private NAV.Document? _document;
|
||||
private SafeBoundingBox _modelBoundingBox;
|
||||
private bool _convertHiddenElements;
|
||||
private OriginMode _originMode;
|
||||
|
||||
public NavisworksConversionSettingsFactory(
|
||||
IHostToSpeckleUnitConverter<NAV.Units> unitsConverter,
|
||||
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
|
||||
ILogger<NavisworksConversionSettingsFactory> logger
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_settingsStore = settingsStore;
|
||||
_unitsConverter = unitsConverter;
|
||||
}
|
||||
|
||||
public NavisworksConversionSettings Current => _settingsStore.Current;
|
||||
public NavisworksConversionSettings Current => settingsStore.Current;
|
||||
|
||||
private static readonly NAV.Vector3D s_canonicalUp = new(0, 0, 1);
|
||||
|
||||
private OriginMode _originMode;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of NavisworksConversionSettings with calculated values.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when no active document is found or document units cannot be converted.
|
||||
/// </exception>
|
||||
public NavisworksConversionSettings Create(
|
||||
OriginMode originMode,
|
||||
RepresentationMode visualRepresentationMode,
|
||||
@@ -60,7 +42,7 @@ public class NavisworksConversionSettingsFactory : INavisworksConversionSettings
|
||||
throw new InvalidOperationException("No active document found.");
|
||||
}
|
||||
|
||||
var units = _unitsConverter.ConvertOrThrow(_document.Units);
|
||||
var units = unitsConverter.ConvertOrThrow(_document.Units);
|
||||
if (string.IsNullOrEmpty(units))
|
||||
{
|
||||
throw new InvalidOperationException("Document units could not be converted.");
|
||||
@@ -96,7 +78,7 @@ public class NavisworksConversionSettingsFactory : INavisworksConversionSettings
|
||||
private void InitializeDocument()
|
||||
{
|
||||
_document = NavisworksApp.ActiveDocument ?? throw new InvalidOperationException("No active document found.");
|
||||
_logger.LogInformation("Creating settings for document: {DocumentName}", _document.Title);
|
||||
logger.LogInformation("Creating settings for document: {DocumentName}", _document.Title);
|
||||
_modelBoundingBox = new SafeBoundingBox(_document.GetBoundingBox(_convertHiddenElements));
|
||||
}
|
||||
|
||||
@@ -109,36 +91,15 @@ public class NavisworksConversionSettingsFactory : INavisworksConversionSettings
|
||||
_ => throw new NotSupportedException($"OriginMode {_originMode} is not supported.")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the transformation vector based on the project base point.
|
||||
/// </summary>
|
||||
/// <returns>The calculated transformation vector.</returns>
|
||||
/// <remarks>
|
||||
/// This uses mocked project base point data and should be replaced with actual logic
|
||||
/// when finally integrating with UI or external configurations.
|
||||
/// </remarks>
|
||||
private SafeVector CalculateProjectBasePointTransform()
|
||||
{
|
||||
// TODO: Replace with actual logic to fetch project base point and units from UI or settings
|
||||
// WARNING: Mocked data - replace with actual UI/settings when implementing project base point
|
||||
var projectBasePoint = new SafeVector(10, 20, 0);
|
||||
// ReSharper disable once ConvertToConstant.Local
|
||||
var projectBasePointUnits = NAV.Units.Meters;
|
||||
|
||||
var scale = NAV.UnitConversion.ScaleFactor(projectBasePointUnits, _document!.Units);
|
||||
|
||||
// The transformation vector is the negative of the project base point, scaled to the source units.
|
||||
// These units are independent of the Speckle units, and because they are from user input.
|
||||
return new SafeVector(-projectBasePoint.X * scale, -projectBasePoint.Y * scale, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the transformation vector based on the bounding box center offset from the origin.
|
||||
/// </summary>
|
||||
/// <returns>The calculated transformation vector.</returns>
|
||||
/// <remarks>
|
||||
/// This uses the document active model bounding box center as the base point for the transformation.
|
||||
/// Assumes no translation in the Z-axis.
|
||||
/// </remarks>
|
||||
private SafeVector CalculateBoundingBoxTransform() =>
|
||||
new(-_modelBoundingBox.Center.X, -_modelBoundingBox.Center.Y, 0);
|
||||
}
|
||||
|
||||
+36
-33
@@ -9,39 +9,42 @@
|
||||
<Import_RootNamespace>Speckle.Converters.NavisworksShared</Import_RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)PathConstants.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Constants\InstanceConstants.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Constants\MaterialConstants.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Constants\PathConstants.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ClassPropertiesExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\DisplayValueExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\ModelPropertiesExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\PropertySetsExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataExtractors\RevitBuiltInCategoryExtractor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\BasePropertyHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\HierarchicalPropertyHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\IPropertyHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DataHandlers\StandardPropertyHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\NavisworksConverterServiceRegistration.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ArrayExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Geometries\Primitives.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Geometries\TransformMatrix.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ColorConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HierarchyHelper.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ElementSelectionHelper.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PrimitiveProcessor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\PropertyHelpers.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GeometryHelpers.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Paths\PathKey.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Registers\InstanceFragmentRegistry.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\ElementSelectionService.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\PropertyConversion.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\UIUnits.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\SharedGeometryStores.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\InstanceStoreManager.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs"/>
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Models\"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettingsFactory.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\NavisworksRootToSpeckleConverter.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoundingBoxToSpeckleRawConverter.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\ModelItemTopLevelConverterToSpeckle.cs"/>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\UIUnits.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\SharedGeometryStores.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\GeometryToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Services\NavisworksToSpeckleUnitConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\ConversionModes.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettings.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Settings\NavisworksConversionSettingsFactory.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\NavisworksRootToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\BoundingBoxToSpeckleRawConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\ModelItemTopLevelConverterToSpeckle.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+3
-19
@@ -1,6 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Converter.Navisworks.Helpers;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.Common.Registration;
|
||||
@@ -8,23 +6,9 @@ using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||
|
||||
public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter
|
||||
public class NavisworksRootToSpeckleConverter(IConverterManager<IToSpeckleTopLevelConverter> toSpeckle)
|
||||
: IRootToSpeckleConverter
|
||||
{
|
||||
private readonly IConverterManager<IToSpeckleTopLevelConverter> _toSpeckle;
|
||||
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
|
||||
private readonly ILogger<NavisworksRootToSpeckleConverter> _logger;
|
||||
|
||||
public NavisworksRootToSpeckleConverter(
|
||||
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
|
||||
ILogger<NavisworksRootToSpeckleConverter> logger,
|
||||
IConverterManager<IToSpeckleTopLevelConverter> toSpeckle
|
||||
)
|
||||
{
|
||||
_toSpeckle = toSpeckle;
|
||||
_logger = logger;
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
public Base Convert(object target)
|
||||
{
|
||||
if (target == null)
|
||||
@@ -38,7 +22,7 @@ public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter
|
||||
}
|
||||
|
||||
Type type = target.GetType();
|
||||
var objectConverter = _toSpeckle.ResolveConverter(type, true);
|
||||
var objectConverter = toSpeckle.ResolveConverter(type);
|
||||
Base result = objectConverter.Convert(modelItem);
|
||||
result.applicationId = ElementSelectionHelper.ResolveModelItemToIndexPath(modelItem);
|
||||
|
||||
|
||||
+4
-10
@@ -6,28 +6,22 @@ using Speckle.Objects.Primitive;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.ToSpeckle.Raw;
|
||||
|
||||
public class BoundingBoxToSpeckleRawConverter : ITypedConverter<NAV.BoundingBox3D, Box>
|
||||
public class BoundingBoxToSpeckleRawConverter(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
|
||||
: ITypedConverter<NAV.BoundingBox3D, Box>
|
||||
{
|
||||
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
|
||||
|
||||
public BoundingBoxToSpeckleRawConverter(IConverterSettingsStore<NavisworksConversionSettings> settingsStore)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
}
|
||||
|
||||
public Box Convert(object target) => Convert((NAV.BoundingBox3D)target);
|
||||
|
||||
public Box Convert(NAV.BoundingBox3D? target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return default!; // returns null for reference types (Box is a reference type)
|
||||
return null!; // returns null for reference types (Box is a reference type)
|
||||
}
|
||||
|
||||
var minPoint = target.Min;
|
||||
var maxPoint = target.Max;
|
||||
|
||||
var units = _settingsStore.Current.Derived.SpeckleUnits;
|
||||
var units = settingsStore.Current.Derived.SpeckleUnits;
|
||||
|
||||
var basePlane = new Plane
|
||||
{
|
||||
|
||||
+337
-547
@@ -1,47 +1,44 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Autodesk.Navisworks.Api.Interop.ComApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Converter.Navisworks.Constants;
|
||||
using Speckle.Converter.Navisworks.Constants.Registers;
|
||||
using Speckle.Converter.Navisworks.Geometry;
|
||||
using Speckle.Converter.Navisworks.Helpers;
|
||||
using Speckle.Converter.Navisworks.Services;
|
||||
using Speckle.Converter.Navisworks.Paths;
|
||||
using Speckle.Converter.Navisworks.Settings;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Objects.Geometry;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Instances;
|
||||
using static Speckle.Converter.Navisworks.Constants.InstanceConstants;
|
||||
using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
|
||||
// ReSharper disable HeuristicUnreachableCode
|
||||
#pragma warning disable CS0162 // Unreachable code detected
|
||||
|
||||
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||
|
||||
/// <remarks>
|
||||
/// Memory Safety: All COM objects (InwSelectionPathsColl, InwOaPath, InwOaFragmentList) are explicitly
|
||||
/// released using Marshal.ReleaseComObject in try-finally blocks to prevent memory leaks.
|
||||
/// NAV.Color objects are disposed using 'using' statements as they implement IDisposable.
|
||||
/// </remarks>
|
||||
public class GeometryToSpeckleConverter(
|
||||
/// <summary>
|
||||
/// WARNING: Uses COM interop - cannot use public ModelGeometry API.
|
||||
/// Process: ModelItem → InwOaPath3 → InwOaFragmentList → InwOaFragment3 → primitives → Speckle geometry
|
||||
///
|
||||
/// COM overhead: ~13.7ms per item (99.5% of the time) - cannot be optimized from C#
|
||||
/// All COM objects are properly released in try-finally blocks to prevent memory leaks.
|
||||
/// </summary>
|
||||
public sealed class GeometryToSpeckleConverter(
|
||||
NavisworksConversionSettings settings,
|
||||
InstanceStoreManager instanceStoreManager,
|
||||
ILogger<GeometryToSpeckleConverter> logger
|
||||
IInstanceFragmentRegistry registry
|
||||
)
|
||||
{
|
||||
private readonly NavisworksConversionSettings _settings =
|
||||
settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
|
||||
private readonly IInstanceFragmentRegistry _registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||
private readonly bool _isUpright = settings.Derived.IsUpright;
|
||||
private readonly SafeVector _transformVector = settings.Derived.TransformVector;
|
||||
private const double SCALE = 1.0;
|
||||
private static readonly Matrix4x4 s_identityMatrix = new(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
private static readonly double[] s_identityTransform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
||||
private const bool ENABLE_INSTANCING = true;
|
||||
private readonly Dictionary<PathKey, int> _groupMemberCounts = new(PathKey.Comparer);
|
||||
|
||||
private readonly InstanceStoreManager _instanceStoreManager =
|
||||
instanceStoreManager ?? throw new ArgumentNullException(nameof(instanceStoreManager));
|
||||
|
||||
private readonly ILogger<GeometryToSpeckleConverter> _logger =
|
||||
logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
internal List<SSM.Base> Convert(NAV.ModelItem modelItem)
|
||||
internal List<Base> Convert(NAV.ModelItem modelItem)
|
||||
{
|
||||
if (modelItem == null)
|
||||
{
|
||||
@@ -53,10 +50,10 @@ public class GeometryToSpeckleConverter(
|
||||
return [];
|
||||
}
|
||||
|
||||
var comSelection = ComApiBridge.ToInwOpSelection([modelItem]);
|
||||
NAV.ModelItemCollection collection = new() { modelItem };
|
||||
var comSelection = ComApiBridge.ToInwOpSelection(modelItemCollection: collection);
|
||||
try
|
||||
{
|
||||
var fragmentStack = new Stack<InwOaFragment3>();
|
||||
var paths = comSelection.Paths();
|
||||
if (paths == null)
|
||||
{
|
||||
@@ -64,32 +61,87 @@ public class GeometryToSpeckleConverter(
|
||||
}
|
||||
try
|
||||
{
|
||||
if (paths.Count > 0)
|
||||
{
|
||||
var firstPath = paths.Cast<InwOaPath>().First();
|
||||
var fragmentsCollection = firstPath.Fragments();
|
||||
try
|
||||
{
|
||||
if (fragmentsCollection.Count > 1)
|
||||
{
|
||||
return ProcessSharedGeometry(paths, fragmentStack);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fragmentsCollection != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(fragmentsCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
var allResults = new List<Base>(5);
|
||||
|
||||
foreach (InwOaPath path in paths)
|
||||
{
|
||||
CollectFragments(path, fragmentStack);
|
||||
if (path.ArrayData is not Array pathArr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var itemPathKey = PathKey.FromComArray(pathArr);
|
||||
|
||||
if (!_registry.TryGetGroup(itemPathKey, out var groupKey))
|
||||
{
|
||||
var members = DiscoverInstancePathsFromFragments(path);
|
||||
members.Add(itemPathKey);
|
||||
groupKey = itemPathKey;
|
||||
_registry.RegisterGroup(groupKey, members);
|
||||
_groupMemberCounts[groupKey] = members.Count;
|
||||
}
|
||||
|
||||
var processor = new PrimitiveProcessor(_isUpright);
|
||||
ProcessPathFragments(path, itemPathKey, groupKey, processor);
|
||||
|
||||
if (!_registry.TryGetInstanceWorld(itemPathKey, out var instanceWorld))
|
||||
{
|
||||
var geometries = ProcessGeometries([processor]);
|
||||
_registry.MarkConverted(itemPathKey);
|
||||
allResults.AddRange(geometries);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_groupMemberCounts.TryGetValue(groupKey, out var memberCount) && memberCount == 1)
|
||||
{
|
||||
var geometries = ProcessGeometries([processor]);
|
||||
_registry.MarkConverted(itemPathKey);
|
||||
allResults.AddRange(geometries);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ENABLE_INSTANCING && !_registry.HasDefinitionGeometry(groupKey))
|
||||
{
|
||||
var geometries = ProcessGeometries([processor]);
|
||||
|
||||
// Transform matrix to Z-up space if model is Y-up, matching vertex transformation
|
||||
var transformedWorld = _isUpright ? instanceWorld : TransformMatrixYUpToZUp(instanceWorld);
|
||||
var invDefWorld = GeometryHelpers.InvertRigid(transformedWorld);
|
||||
var definitionGeometry = UnbakeGeometry(geometries, invDefWorld);
|
||||
var groupKeyHash = groupKey.ToHashString();
|
||||
for (int i = 0; i < definitionGeometry.Count; i++)
|
||||
{
|
||||
definitionGeometry[i].applicationId = $"{GEOMETRY_ID_PREFIX}{groupKeyHash}_{i}";
|
||||
}
|
||||
|
||||
_registry.StoreDefinitionGeometry(groupKey, definitionGeometry);
|
||||
}
|
||||
|
||||
if (ENABLE_INSTANCING)
|
||||
{
|
||||
// Transform matrix to Z-up space if model is Y-up, matching vertex transformation
|
||||
var transformedWorld = _isUpright ? instanceWorld : TransformMatrixYUpToZUp(instanceWorld);
|
||||
var instanceProxy = new InstanceProxy
|
||||
{
|
||||
definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{groupKey.ToHashString()}",
|
||||
transform = ConvertToMatrix4X4(transformedWorld),
|
||||
units = _settings.Derived.SpeckleUnits,
|
||||
applicationId = $"{InstanceConstants.INSTANCE_ID_PREFIX}{itemPathKey.ToHashString()}",
|
||||
maxDepth = 0
|
||||
};
|
||||
|
||||
_registry.MarkConverted(itemPathKey);
|
||||
allResults.Add(instanceProxy);
|
||||
}
|
||||
else
|
||||
{
|
||||
var geometries = ProcessGeometries([processor]);
|
||||
_registry.MarkConverted(itemPathKey);
|
||||
allResults.AddRange(geometries);
|
||||
}
|
||||
}
|
||||
|
||||
return ProcessFragments(fragmentStack, paths, true);
|
||||
return allResults;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -103,18 +155,113 @@ public class GeometryToSpeckleConverter(
|
||||
Marshal.ReleaseComObject(comSelection);
|
||||
}
|
||||
}
|
||||
collection.Dispose();
|
||||
}
|
||||
|
||||
private static void CollectFragments(InwOaPath path, Stack<InwOaFragment3> fragmentStack)
|
||||
private static HashSet<PathKey> DiscoverInstancePathsFromFragments(InwOaPath path)
|
||||
{
|
||||
var set = new HashSet<PathKey>(PathKey.Comparer);
|
||||
var fragments = path.Fragments();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var fragment in fragments.OfType<InwOaFragment3>())
|
||||
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
|
||||
{
|
||||
if (AreFragmentPathsEqual(fragment, path))
|
||||
GC.KeepAlive(fragment);
|
||||
|
||||
InwOaPath? fragPath = fragment.path;
|
||||
if (fragPath?.ArrayData is not Array fragPathArr)
|
||||
{
|
||||
fragmentStack.Push(fragment);
|
||||
continue;
|
||||
}
|
||||
|
||||
var fragmentPathKey = PathKey.FromComArray(fragPathArr);
|
||||
set.Add(fragmentPathKey);
|
||||
|
||||
Marshal.ReleaseComObject(fragPath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fragments != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(fragments);
|
||||
}
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private void ProcessPathFragments(InwOaPath path, PathKey itemPathKey, PathKey groupKey, PrimitiveProcessor processor)
|
||||
{
|
||||
var observed = false;
|
||||
var fragments = path.Fragments();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
|
||||
{
|
||||
GC.KeepAlive(fragment);
|
||||
|
||||
InwOaPath? fragPath = null;
|
||||
InwLTransform3f3? transform = null;
|
||||
|
||||
try
|
||||
{
|
||||
fragPath = fragment.path;
|
||||
if (fragPath?.ArrayData is not Array fragPathArr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!itemPathKey.MatchesComArray(fragPathArr))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
transform = fragment.GetLocalToWorldMatrix() as InwLTransform3f3;
|
||||
if (transform == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (transform.Matrix is not Array matrixArray)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var instanceWorld = ConvertArrayToDouble(matrixArray);
|
||||
if (instanceWorld.Length != 16)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
processor.LocalToWorldTransformation = instanceWorld;
|
||||
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
|
||||
|
||||
if (observed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processor.Triangles.Count <= 0 && processor.Lines.Count <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_registry.RegisterInstanceObservation(groupKey, itemPathKey, instanceWorld, processor);
|
||||
observed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transform != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(transform);
|
||||
}
|
||||
if (fragPath != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(fragPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,95 +274,9 @@ public class GeometryToSpeckleConverter(
|
||||
}
|
||||
}
|
||||
|
||||
private List<SSM.Base> ProcessSharedGeometry(InwSelectionPathsColl paths, Stack<InwOaFragment3> fragmentStack)
|
||||
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
|
||||
{
|
||||
var fragmentId = GenerateFragmentId(paths);
|
||||
|
||||
if (string.IsNullOrEmpty(fragmentId))
|
||||
{
|
||||
foreach (InwOaPath path in paths)
|
||||
{
|
||||
CollectFragments(path, fragmentStack);
|
||||
}
|
||||
|
||||
return ProcessFragments(fragmentStack, paths, true);
|
||||
}
|
||||
|
||||
if (_instanceStoreManager.ContainsSharedGeometry(fragmentId))
|
||||
{
|
||||
return CreateInstanceReference(fragmentId, paths);
|
||||
}
|
||||
|
||||
foreach (InwOaPath path in paths)
|
||||
{
|
||||
CollectFragments(path, fragmentStack);
|
||||
}
|
||||
|
||||
var baseGeometries = ExtractUntransformedGeometry(fragmentStack);
|
||||
|
||||
return baseGeometries.Count == 0 || !_instanceStoreManager.AddSharedGeometry(fragmentId, baseGeometries)
|
||||
? ProcessFragments(fragmentStack, paths) // default false flag for isSingleObject
|
||||
: CreateInstanceReference(fragmentId, paths);
|
||||
}
|
||||
|
||||
private List<SSM.Base> ProcessFragments(
|
||||
Stack<InwOaFragment3> fragmentStack,
|
||||
InwSelectionPathsColl paths,
|
||||
bool isSingleObject = false
|
||||
)
|
||||
{
|
||||
var callbackListeners = new List<PrimitiveProcessor>();
|
||||
|
||||
foreach (InwOaPath path in paths)
|
||||
{
|
||||
var processor = new PrimitiveProcessor(_isUpright);
|
||||
|
||||
foreach (var fragment in fragmentStack)
|
||||
{
|
||||
var matrix = fragment.GetLocalToWorldMatrix();
|
||||
var transform = matrix as InwLTransform3f3;
|
||||
if (transform?.Matrix is not Array matrixArray)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fragmentsForCount = path.Fragments();
|
||||
int fragmentCount;
|
||||
try
|
||||
{
|
||||
fragmentCount = fragmentsForCount?.Count ?? 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fragmentsForCount != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(fragmentsForCount);
|
||||
}
|
||||
}
|
||||
|
||||
double[] makeNoChange = s_identityTransform;
|
||||
double[] transformMatrix = ConvertArrayToDouble(matrixArray);
|
||||
|
||||
processor.LocalToWorldTransformation =
|
||||
isSingleObject || fragmentCount == 1 ? transformMatrix : (IEnumerable<double>)makeNoChange;
|
||||
|
||||
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
|
||||
}
|
||||
|
||||
callbackListeners.Add(processor);
|
||||
}
|
||||
|
||||
return ProcessGeometries(callbackListeners);
|
||||
}
|
||||
|
||||
private static bool AreFragmentPathsEqual(InwOaFragment3 fragment, InwOaPath path) =>
|
||||
fragment.path?.ArrayData is Array fragmentPathData
|
||||
&& path.ArrayData is Array pathData
|
||||
&& AreFragmentPathsEqual(fragmentPathData, pathData);
|
||||
|
||||
private List<SSM.Base> ProcessGeometries(List<PrimitiveProcessor> processors)
|
||||
{
|
||||
var baseGeometries = new List<SSM.Base>();
|
||||
var baseGeometries = new List<Base>(processors.Count * 2);
|
||||
|
||||
foreach (var processor in processors)
|
||||
{
|
||||
@@ -225,17 +286,13 @@ public class GeometryToSpeckleConverter(
|
||||
baseGeometries.Add(mesh);
|
||||
}
|
||||
|
||||
if (processor.Lines.Count > 0)
|
||||
if (processor.Lines.Count <= 0)
|
||||
{
|
||||
var lines = CreateLines(processor.Lines);
|
||||
baseGeometries.AddRange(lines);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processor.Points.Count > 0)
|
||||
{
|
||||
var points = CreatePoints(processor.Points);
|
||||
baseGeometries.AddRange(points);
|
||||
}
|
||||
var lines = CreateLines(processor.Lines);
|
||||
baseGeometries.AddRange(lines);
|
||||
}
|
||||
|
||||
return baseGeometries;
|
||||
@@ -243,13 +300,12 @@ public class GeometryToSpeckleConverter(
|
||||
|
||||
private Mesh CreateMesh(IReadOnlyList<SafeTriangle> triangles)
|
||||
{
|
||||
var vertices = new List<double>();
|
||||
var faces = new List<int>();
|
||||
var vertices = new List<double>(triangles.Count * 9);
|
||||
var faces = new List<int>(triangles.Count * 4);
|
||||
|
||||
for (var t = 0; t < triangles.Count; t++)
|
||||
{
|
||||
var triangle = triangles[t];
|
||||
|
||||
vertices.AddRange(
|
||||
[
|
||||
(triangle.Vertex1.X + _transformVector.X) * SCALE,
|
||||
@@ -274,412 +330,35 @@ public class GeometryToSpeckleConverter(
|
||||
};
|
||||
}
|
||||
|
||||
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines) =>
|
||||
lines
|
||||
.Select(line => new Line
|
||||
{
|
||||
start = new Point(
|
||||
(line.Start.X + _transformVector.X) * SCALE,
|
||||
(line.Start.Y + _transformVector.Y) * SCALE,
|
||||
(line.Start.Z + _transformVector.Z) * SCALE,
|
||||
_settings.Derived.SpeckleUnits
|
||||
),
|
||||
end = new Point(
|
||||
(line.End.X + _transformVector.X) * SCALE,
|
||||
(line.End.Y + _transformVector.Y) * SCALE,
|
||||
(line.End.Z + _transformVector.Z) * SCALE,
|
||||
_settings.Derived.SpeckleUnits
|
||||
),
|
||||
units = _settings.Derived.SpeckleUnits
|
||||
})
|
||||
.ToList();
|
||||
|
||||
private List<Point> CreatePoints(IReadOnlyList<SafePoint> points) =>
|
||||
points
|
||||
.Select(point => new Point(
|
||||
(point.Vertex.X + _transformVector.X) * SCALE,
|
||||
(point.Vertex.Y + _transformVector.Y) * SCALE,
|
||||
(point.Vertex.Z + _transformVector.Z) * SCALE,
|
||||
_settings.Derived.SpeckleUnits
|
||||
))
|
||||
.ToList();
|
||||
|
||||
public string GenerateFragmentId(InwSelectionPathsColl paths)
|
||||
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines)
|
||||
{
|
||||
try
|
||||
var result = new List<Line>(lines.Count);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var fragmentHashes = new List<string>();
|
||||
|
||||
foreach (var fragments in from InwOaPath path in paths select path.Fragments())
|
||||
{
|
||||
try
|
||||
result.Add(
|
||||
new Line
|
||||
{
|
||||
var fragmentIndex = 0;
|
||||
foreach (InwOaFragment3 fragment in fragments.OfType<InwOaFragment3>())
|
||||
{
|
||||
if (fragment.path?.ArrayData is not Array pathData || pathData.Length == 0)
|
||||
{
|
||||
fragmentIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (pathData.Rank != 1)
|
||||
{
|
||||
var fragmentHashFallback = TrySimpleArrayEnumeration(pathData, fragmentIndex);
|
||||
if (!string.IsNullOrEmpty(fragmentHashFallback))
|
||||
{
|
||||
fragmentHashes.Add(fragmentHashFallback);
|
||||
}
|
||||
|
||||
fragmentIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var lowerBound = pathData.GetLowerBound(0);
|
||||
var upperBound = pathData.GetUpperBound(0);
|
||||
|
||||
var arrayLength = upperBound - lowerBound + 1;
|
||||
var pathInts = new int[arrayLength];
|
||||
|
||||
for (int i = lowerBound; i <= upperBound; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = pathData.GetValue(i);
|
||||
var arrayIndex = i - lowerBound;
|
||||
pathInts[arrayIndex] = System.Convert.ToInt32(value);
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or InvalidCastException)
|
||||
{
|
||||
var errorType = ex is COMException ? "COM array access failed" : "Type conversion failed";
|
||||
_logger.LogDebug(ex, "{ErrorType} at index {Index}", errorType, i);
|
||||
}
|
||||
}
|
||||
|
||||
var fragmentHash = string.Join("_", pathInts);
|
||||
fragmentHashes.Add(fragmentHash);
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or IndexOutOfRangeException or RankException)
|
||||
{
|
||||
var errorType = ex switch
|
||||
{
|
||||
COMException => "COM access failed",
|
||||
IndexOutOfRangeException => "Array bounds exceeded",
|
||||
RankException => "Array rank mismatch",
|
||||
_ => "Error"
|
||||
};
|
||||
_logger.LogDebug(
|
||||
ex,
|
||||
"{ErrorType} processing fragment {FragmentIndex}, trying simple enumeration",
|
||||
errorType,
|
||||
fragmentIndex
|
||||
);
|
||||
|
||||
var fragmentHash = TrySimpleArrayEnumeration(pathData, fragmentIndex);
|
||||
if (!string.IsNullOrEmpty(fragmentHash))
|
||||
{
|
||||
fragmentHashes.Add(fragmentHash);
|
||||
}
|
||||
|
||||
fragmentIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
fragmentIndex++;
|
||||
}
|
||||
start = new Point(
|
||||
(line.Start.X + _transformVector.X) * SCALE,
|
||||
(line.Start.Y + _transformVector.Y) * SCALE,
|
||||
(line.Start.Z + _transformVector.Z) * SCALE,
|
||||
_settings.Derived.SpeckleUnits
|
||||
),
|
||||
end = new Point(
|
||||
(line.End.X + _transformVector.X) * SCALE,
|
||||
(line.End.Y + _transformVector.Y) * SCALE,
|
||||
(line.End.Z + _transformVector.Z) * SCALE,
|
||||
_settings.Derived.SpeckleUnits
|
||||
),
|
||||
units = _settings.Derived.SpeckleUnits
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fragments != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(fragments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fragmentHashes.Count > 0)
|
||||
{
|
||||
fragmentHashes.Sort();
|
||||
var rawData = string.Join("__", fragmentHashes);
|
||||
var fragmentId = HashRawData(rawData);
|
||||
return fragmentId;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
);
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or InvalidCastException or IndexOutOfRangeException)
|
||||
{
|
||||
var errorType = ex switch
|
||||
{
|
||||
COMException => "COM access failed",
|
||||
InvalidCastException => "Type conversion failed",
|
||||
IndexOutOfRangeException => "Array bounds exceeded",
|
||||
_ => "Error"
|
||||
};
|
||||
_logger.LogWarning(ex, "{ErrorType} generating fragment ID", errorType);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private string TrySimpleArrayEnumeration(Array pathData, int fragmentIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var values = new List<string>();
|
||||
var maxAttempts = Math.Min(pathData.Length, 20);
|
||||
|
||||
for (int i = 0; i < maxAttempts; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = pathData.GetValue(i);
|
||||
var convertedValue = System.Convert.ToInt32(value);
|
||||
values.Add(convertedValue.ToString());
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (InvalidCastException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Type conversion failed at index {Index}", i);
|
||||
}
|
||||
}
|
||||
|
||||
if (values.Count <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return string.Join("_", values);
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "COM enumeration failed for fragment {FragmentIndex}", fragmentIndex);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string HashRawData(string rawData)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var inputBytes = Encoding.UTF8.GetBytes(rawData);
|
||||
var hashBytes = sha256.ComputeHash(inputBytes);
|
||||
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
private List<SSM.Base> ExtractUntransformedGeometry(Stack<InwOaFragment3> fragmentStack)
|
||||
{
|
||||
var processor = new PrimitiveProcessor(_isUpright);
|
||||
|
||||
foreach (var fragment in fragmentStack)
|
||||
{
|
||||
processor.LocalToWorldTransformation = s_identityTransform;
|
||||
|
||||
fragment.GenerateSimplePrimitives(nwEVertexProperty.eNORMAL, processor);
|
||||
}
|
||||
|
||||
var geometries = new List<SSM.Base>();
|
||||
|
||||
if (processor.Triangles.Count > 0)
|
||||
{
|
||||
geometries.Add(CreateMesh(processor.Triangles));
|
||||
}
|
||||
|
||||
if (processor.Lines.Count > 0)
|
||||
{
|
||||
geometries.AddRange(CreateLines(processor.Lines));
|
||||
}
|
||||
|
||||
if (processor.Points.Count > 0)
|
||||
{
|
||||
geometries.AddRange(CreatePoints(processor.Points));
|
||||
}
|
||||
|
||||
return geometries;
|
||||
}
|
||||
|
||||
private List<SSM.Base> CreateInstanceReference(string fragmentId, InwSelectionPathsColl paths)
|
||||
{
|
||||
var transform = ExtractInstanceTransform(paths);
|
||||
|
||||
var instanceReference = new InstanceProxy
|
||||
{
|
||||
definitionId = $"{InstanceConstants.DEFINITION_ID_PREFIX}{fragmentId}",
|
||||
transform = transform,
|
||||
units = _settings.Derived.SpeckleUnits,
|
||||
maxDepth = 0,
|
||||
applicationId = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
return [instanceReference];
|
||||
}
|
||||
|
||||
private Matrix4x4 ExtractInstanceTransform(InwSelectionPathsColl paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
return s_identityMatrix;
|
||||
}
|
||||
|
||||
var firstPath = paths.Cast<InwOaPath>().First();
|
||||
var fragments = firstPath.Fragments();
|
||||
try
|
||||
{
|
||||
if (fragments.Count == 0)
|
||||
{
|
||||
return s_identityMatrix;
|
||||
}
|
||||
|
||||
var fragmentStack = new Stack<InwOaFragment3>();
|
||||
|
||||
foreach (var frag in fragments.OfType<InwOaFragment3>())
|
||||
{
|
||||
if (frag.path?.ArrayData is not Array pathData1 || firstPath.ArrayData is not Array pathData2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pathArray1 = pathData1.Cast<int>().ToArray();
|
||||
var pathArray2 = pathData2.Cast<int>().ToArray();
|
||||
|
||||
if (pathArray1.Length == pathArray2.Length && pathArray1.SequenceEqual(pathArray2))
|
||||
{
|
||||
fragmentStack.Push(frag);
|
||||
}
|
||||
}
|
||||
|
||||
var fragment = fragmentStack.First();
|
||||
var matrix = fragment.GetLocalToWorldMatrix();
|
||||
|
||||
if (matrix is InwLTransform3f3 { Matrix: Array matrixArray })
|
||||
{
|
||||
var transformArray = ConvertArrayToDouble(matrixArray);
|
||||
var transformedMatrix = ApplyCoordinateTransform(transformArray);
|
||||
|
||||
var newMatrix = new Matrix4x4(
|
||||
transformedMatrix[0],
|
||||
transformedMatrix[1],
|
||||
transformedMatrix[2],
|
||||
transformedMatrix[3],
|
||||
transformedMatrix[4],
|
||||
transformedMatrix[5],
|
||||
transformedMatrix[6],
|
||||
transformedMatrix[7],
|
||||
transformedMatrix[8],
|
||||
transformedMatrix[9],
|
||||
transformedMatrix[10],
|
||||
transformedMatrix[11],
|
||||
transformedMatrix[12],
|
||||
transformedMatrix[13],
|
||||
transformedMatrix[14],
|
||||
transformedMatrix[15]
|
||||
);
|
||||
|
||||
return Matrix4x4.Transpose(newMatrix);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fragments != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(fragments);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or InvalidCastException or NullReferenceException)
|
||||
{
|
||||
var errorType = ex switch
|
||||
{
|
||||
COMException => "COM access failed",
|
||||
InvalidCastException => "Transform matrix type conversion failed",
|
||||
NullReferenceException => "Null reference",
|
||||
_ => "Error"
|
||||
};
|
||||
_logger.LogWarning(ex, "{ErrorType} extracting instance transform", errorType);
|
||||
}
|
||||
|
||||
return s_identityMatrix;
|
||||
}
|
||||
|
||||
private double[] ApplyCoordinateTransform(double[] matrixArray)
|
||||
{
|
||||
var result = new double[16];
|
||||
Array.Copy(matrixArray, result, 16);
|
||||
|
||||
// Apply Y-up to Z-up basis correction for non-upright models.
|
||||
// When meshes are converted, vertices undergo the transformation (x, y, z) -> (x, -z, y)
|
||||
// via TransformVectorToOrientation in PrimitiveProcessor. Instance transforms must be
|
||||
// converted to the same basis using the conjugation: T_zup = C * T_yup * C^(-1)
|
||||
// where C is the Y-up to Z-up change of basis matrix.
|
||||
if (!_isUpright)
|
||||
{
|
||||
result = ApplyYUpToZUpBasisChange(result);
|
||||
}
|
||||
|
||||
result[12] = (result[12] + _transformVector.X) * SCALE;
|
||||
result[13] = (result[13] + _transformVector.Y) * SCALE;
|
||||
result[14] = (result[14] + _transformVector.Z) * SCALE;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the Y-up to Z-up basis change to a 4x4 transformation matrix.
|
||||
/// This performs the conjugation T_zup = C * T_yup * C^(-1) where C represents
|
||||
/// the coordinate transformation (x, y, z) -> (x, -z, y).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The change of basis matrix C and its inverse C^(-1) for (x, y, z) -> (x, -z, y):
|
||||
/// C = | 1 0 0 0 | C^(-1) = | 1 0 0 0 |
|
||||
/// | 0 0 -1 0 | | 0 0 1 0 |
|
||||
/// | 0 1 0 0 | | 0 -1 0 0 |
|
||||
/// | 0 0 0 1 | | 0 0 0 1 |
|
||||
///
|
||||
/// For a matrix T with elements [m0...m15] in row-major order:
|
||||
/// | m0 m1 m2 m3 |
|
||||
/// | m4 m5 m6 m7 |
|
||||
/// | m8 m9 m10 m11 |
|
||||
/// | m12 m13 m14 m15 |
|
||||
///
|
||||
/// The result of C * T * C^(-1) transforms the basis while preserving
|
||||
/// the geometric meaning of the transformation in the new coordinate system.
|
||||
/// </remarks>
|
||||
private static double[] ApplyYUpToZUpBasisChange(double[] m) =>
|
||||
// Compute C * T * C^(-1) where:
|
||||
// C converts Y-up to Z-up: (x, y, z) -> (x, -z, y)
|
||||
// This ensures instance transforms operate correctly on Z-up geometry.
|
||||
//
|
||||
// The multiplication is performed analytically for efficiency.
|
||||
// Given input matrix m (row-major), the result is:
|
||||
[
|
||||
m[0],
|
||||
-m[2],
|
||||
m[1],
|
||||
m[3], // Row 0: unchanged x, swapped and negated y/z
|
||||
-m[8],
|
||||
m[10],
|
||||
-m[9],
|
||||
-m[11], // Row 1: from row 2, with sign changes
|
||||
m[4],
|
||||
-m[6],
|
||||
m[5],
|
||||
m[7], // Row 2: from row 1, with sign changes
|
||||
m[12],
|
||||
-m[14],
|
||||
m[13],
|
||||
m[15] // Row 3 (translation): swap y/z, negate new y
|
||||
];
|
||||
|
||||
private static double[] ConvertArrayToDouble(Array arr)
|
||||
{
|
||||
if (arr.Rank != 1)
|
||||
@@ -696,6 +375,117 @@ public class GeometryToSpeckleConverter(
|
||||
return doubleArray;
|
||||
}
|
||||
|
||||
private static bool AreFragmentPathsEqual(Array a1, Array a2) =>
|
||||
a1.Length == a2.Length && a1.Cast<int>().SequenceEqual(a2.Cast<int>());
|
||||
/// <summary>
|
||||
/// VALIDATION HELPER: Unbakes geometry from world space to definition space. Creates copies of the geometry and
|
||||
/// applies inverse transform to move from world coordinates back to definition/local space.
|
||||
/// Used for visual validation of instance detection.
|
||||
/// </summary>
|
||||
private static List<Base> UnbakeGeometry(List<Base> bakedGeometry, double[] invWorld)
|
||||
{
|
||||
var result = new List<Base>(bakedGeometry.Count);
|
||||
|
||||
foreach (var item in bakedGeometry)
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case Mesh mesh:
|
||||
{
|
||||
// Create a copy to avoid mutating the original
|
||||
var unbaked = new Mesh
|
||||
{
|
||||
vertices = [.. mesh.vertices],
|
||||
faces = mesh.faces,
|
||||
units = mesh.units
|
||||
};
|
||||
GeometryHelpers.UnbakeMeshVertices(unbaked, invWorld);
|
||||
result.Add(unbaked);
|
||||
break;
|
||||
}
|
||||
case Line line:
|
||||
{
|
||||
var unbaked = new Line
|
||||
{
|
||||
start = new Point(line.start.x, line.start.y, line.start.z, line.start.units),
|
||||
end = new Point(line.end.x, line.end.y, line.end.z, line.end.units),
|
||||
units = line.units
|
||||
};
|
||||
GeometryHelpers.UnbakeLine(unbaked, invWorld);
|
||||
result.Add(unbaked);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
result.Add(item); // Pass through unknown types
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a 4x4 matrix from Y-up to Z-up coordinate system.
|
||||
/// Applies P * M * P^-1 where P is the coordinate transform (x, y, z) -> (x, -z, y).
|
||||
/// </summary>
|
||||
private static double[] TransformMatrixYUpToZUp(double[] m)
|
||||
{
|
||||
// P swaps Y↔Z with sign: (x,y,z) -> (x,-z,y)
|
||||
// P = | 1 0 0 0 | P^-1 = | 1 0 0 0 |
|
||||
// | 0 0 -1 0 | | 0 0 1 0 |
|
||||
// | 0 1 0 0 | | 0 -1 0 0 |
|
||||
// | 0 0 0 1 | | 0 0 0 1 |
|
||||
// Result = P * M * P^-1
|
||||
|
||||
var result = new double[16]; //
|
||||
|
||||
// Column 0 (X basis): unchanged in X, swap Y↔Z
|
||||
result[0] = m[0];
|
||||
result[4] = m[8];
|
||||
result[8] = -m[4];
|
||||
result[12] = m[12];
|
||||
|
||||
// Column 1 (Y basis): comes from -Z
|
||||
result[1] = -m[2];
|
||||
result[5] = -m[10];
|
||||
result[9] = m[6];
|
||||
result[13] = -m[14];
|
||||
|
||||
// Column 2 (Z basis): comes from Y
|
||||
result[2] = m[1];
|
||||
result[6] = m[9];
|
||||
result[10] = -m[5];
|
||||
result[14] = m[13];
|
||||
|
||||
// Column 3 (homogeneous): unchanged
|
||||
result[3] = m[3];
|
||||
result[7] = m[7];
|
||||
result[11] = m[11];
|
||||
result[15] = m[15];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Matrix4x4 ConvertToMatrix4X4(double[] matrix) =>
|
||||
matrix.Length == 16
|
||||
? Matrix4x4.Transpose(
|
||||
new Matrix4x4
|
||||
{
|
||||
M11 = matrix[0],
|
||||
M12 = matrix[1],
|
||||
M13 = matrix[2],
|
||||
M14 = matrix[3],
|
||||
M21 = matrix[4],
|
||||
M22 = matrix[5],
|
||||
M23 = matrix[6],
|
||||
M24 = matrix[7],
|
||||
M31 = matrix[8],
|
||||
M32 = matrix[9],
|
||||
M33 = matrix[10],
|
||||
M34 = matrix[11],
|
||||
M41 = matrix[12],
|
||||
M42 = matrix[13],
|
||||
M43 = matrix[14],
|
||||
M44 = matrix[15]
|
||||
}
|
||||
)
|
||||
: throw new ArgumentException("Matrix must have exactly 16 elements", nameof(matrix));
|
||||
}
|
||||
|
||||
+19
-42
@@ -8,42 +8,20 @@ using Speckle.Sdk.Models.Collections;
|
||||
|
||||
namespace Speckle.Converter.Navisworks.ToSpeckle;
|
||||
|
||||
/// <summary>
|
||||
/// Converts Navisworks ModelItem objects to Speckle Base objects.
|
||||
/// </summary>
|
||||
[NameAndRankValue(typeof(NAV.ModelItem), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
|
||||
public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
public class ModelItemToToSpeckleConverter(
|
||||
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
|
||||
StandardPropertyHandler standardHandler,
|
||||
HierarchicalPropertyHandler hierarchicalHandler,
|
||||
DisplayValueExtractor displayValueExtractor
|
||||
) : IToSpeckleTopLevelConverter
|
||||
{
|
||||
private readonly StandardPropertyHandler _standardHandler;
|
||||
private readonly HierarchicalPropertyHandler _hierarchicalHandler;
|
||||
private readonly IConverterSettingsStore<NavisworksConversionSettings> _settingsStore;
|
||||
private readonly DisplayValueExtractor _displayValueExtractor;
|
||||
|
||||
public ModelItemToToSpeckleConverter(
|
||||
IConverterSettingsStore<NavisworksConversionSettings> settingsStore,
|
||||
StandardPropertyHandler standardHandler,
|
||||
HierarchicalPropertyHandler hierarchicalHandler,
|
||||
DisplayValueExtractor displayValueExtractor
|
||||
)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
_standardHandler = standardHandler;
|
||||
_hierarchicalHandler = hierarchicalHandler;
|
||||
_displayValueExtractor = displayValueExtractor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Navisworks object to a Speckle Base object.
|
||||
/// </summary>
|
||||
/// <param name="target">The object to convert.</param>
|
||||
/// <returns>The converted Speckle Base object.</returns>
|
||||
public Base Convert(object target) =>
|
||||
target == null ? throw new ArgumentNullException(nameof(target)) : Convert((NAV.ModelItem)target);
|
||||
|
||||
// Converts a Navisworks ModelItem into a Speckle Base object
|
||||
private Base Convert(NAV.ModelItem target)
|
||||
{
|
||||
IPropertyHandler handler = ShouldMergeProperties(target) ? _hierarchicalHandler : _standardHandler;
|
||||
IPropertyHandler handler = ShouldMergeProperties(target) ? hierarchicalHandler : standardHandler;
|
||||
var name = GetObjectName(target);
|
||||
|
||||
return target.HasGeometry
|
||||
@@ -51,28 +29,29 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
: CreateNonGeometryObject(target, name, handler);
|
||||
}
|
||||
|
||||
// There are in fact only two types of objects: geometry and non-geometry, the latter being collections of other objects
|
||||
private NavisworksObject CreateGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) =>
|
||||
new()
|
||||
private NavisworksObject CreateGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler)
|
||||
{
|
||||
var displayValue = displayValueExtractor.GetDisplayValue(target);
|
||||
|
||||
var geometryObject = new NavisworksObject
|
||||
{
|
||||
units = settingsStore.Current.Derived.SpeckleUnits,
|
||||
name = name,
|
||||
displayValue = _displayValueExtractor.GetDisplayValue(target),
|
||||
properties = _settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target),
|
||||
units = _settingsStore.Current.Derived.SpeckleUnits,
|
||||
properties = settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target),
|
||||
displayValue = displayValue
|
||||
};
|
||||
|
||||
return geometryObject;
|
||||
}
|
||||
|
||||
private Collection CreateNonGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) =>
|
||||
new()
|
||||
{
|
||||
name = name,
|
||||
elements = [],
|
||||
["properties"] = _settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target),
|
||||
["properties"] = settingsStore.Current.User.ExcludeProperties ? [] : propertyHandler.GetProperties(target),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether properties should be merged from ancestors.
|
||||
/// Only geometry objects should have their properties merged.... For now.
|
||||
/// </summary>
|
||||
private static bool ShouldMergeProperties(NAV.ModelItem target) => target.HasGeometry;
|
||||
|
||||
private static string GetObjectName(NAV.ModelItem target)
|
||||
@@ -81,8 +60,6 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
|
||||
|
||||
var firstObjectAncestor = target.FindFirstObjectAncestor();
|
||||
|
||||
// while the target node name is null keep cycling through parent objects until displayname is not null or empty OR object is firstObjectAncestor
|
||||
|
||||
while (string.IsNullOrEmpty(targetName) && target != firstObjectAncestor)
|
||||
{
|
||||
target = target.Parent;
|
||||
|
||||
@@ -75,7 +75,8 @@ public sealed class DisplayValueExtractor
|
||||
}
|
||||
return areaDisplay;
|
||||
|
||||
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView + apply reference point transform
|
||||
// Rebar: get_Geometry() returns null, use GetTransformedCenterlineCurves/GetFullGeometryForView
|
||||
// Reference point transform is handled by point converters during conversion
|
||||
case DB.Structure.Rebar rebar:
|
||||
return _converterSettings.Current.SendRebarsAsVolumetric
|
||||
? GetRebarVolumetricDisplayValue(rebar)
|
||||
@@ -108,15 +109,16 @@ public sealed class DisplayValueExtractor
|
||||
using DB.Transform? documentToLocal = localToDocument?.Inverse;
|
||||
|
||||
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
|
||||
using DB.Transform? compoundTransform =
|
||||
localToDocument is not null && documentToWorld is not null
|
||||
? documentToWorld.Multiply(localToDocument)
|
||||
: localToDocument;
|
||||
|
||||
DB.Transform? localToWorld = compoundTransform ?? documentToWorld;
|
||||
DB.Transform? localToWorld = (localToDocument, documentToWorld) switch
|
||||
{
|
||||
(not null, not null) => documentToWorld.Multiply(localToDocument),
|
||||
(not null, null) => localToDocument,
|
||||
(null, not null) => documentToWorld,
|
||||
(null, null) => null
|
||||
};
|
||||
|
||||
var collections = GetSortedGeometryFromElement(element, options, documentToLocal);
|
||||
return ProcessGeometryCollections(element, collections, localToWorld);
|
||||
return ProcessGeometryCollections(element, collections, localToWorld, localToDocument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -168,13 +170,15 @@ public sealed class DisplayValueExtractor
|
||||
/// Converts sorted geometry into DisplayValueResults <see cref="ElementTopLevelConverterToSpeckle"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Applies localToWorld only to curves, points, polylines.
|
||||
/// Meshes remain in symbol space to generate correct instance proxies and avoid duplicates.
|
||||
/// Meshes get localToWorld attached as transform metadata (for instancing).
|
||||
/// Curves, polylines, and points get curveTransform applied (instance transform only) -
|
||||
/// reference point transform is handled by the point converters.
|
||||
/// </remarks>
|
||||
private List<DisplayValueResult> ProcessGeometryCollections(
|
||||
DB.Element element,
|
||||
GeometryCollections collections,
|
||||
DB.Transform? localToWorld
|
||||
DB.Transform? localToWorld,
|
||||
DB.Transform? curveTransform
|
||||
)
|
||||
{
|
||||
var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids);
|
||||
@@ -196,16 +200,24 @@ public sealed class DisplayValueExtractor
|
||||
|
||||
foreach (var curve in collections.Curves)
|
||||
{
|
||||
var transformedCurve = localToWorld != null ? curve.CreateTransformed(localToWorld) : curve;
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
|
||||
if (curveTransform is not null)
|
||||
{
|
||||
using var transformedCurve = curve.CreateTransformed(curveTransform);
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
|
||||
}
|
||||
else
|
||||
{
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var polyline in collections.Polylines)
|
||||
{
|
||||
if (localToWorld != null)
|
||||
if (curveTransform is not null)
|
||||
{
|
||||
var coords = polyline.GetCoordinates().Select(p => localToWorld.OfPoint(p)).ToList();
|
||||
using var transformedPolyline = DB.PolyLine.Create(coords);
|
||||
var coords = polyline.GetCoordinates();
|
||||
var transformedCoords = coords.Select(coord => curveTransform.OfPoint(coord)).ToList();
|
||||
using var transformedPolyline = DB.PolyLine.Create(transformedCoords);
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(transformedPolyline)));
|
||||
}
|
||||
else
|
||||
@@ -216,9 +228,9 @@ public sealed class DisplayValueExtractor
|
||||
|
||||
foreach (var point in collections.Points)
|
||||
{
|
||||
if (localToWorld != null)
|
||||
if (curveTransform is not null)
|
||||
{
|
||||
using var transformedPoint = DB.Point.Create(localToWorld.OfPoint(point.Coord));
|
||||
using var transformedPoint = DB.Point.Create(curveTransform.OfPoint(point.Coord));
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(transformedPoint)));
|
||||
}
|
||||
else
|
||||
@@ -558,7 +570,7 @@ public sealed class DisplayValueExtractor
|
||||
{
|
||||
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
|
||||
SortGeometry(rebar, collections, geometryElements, null);
|
||||
return ProcessGeometryCollections(rebar, collections, documentToWorld);
|
||||
return ProcessGeometryCollections(rebar, collections, documentToWorld, null);
|
||||
}
|
||||
|
||||
// Return empty list if no geometry is found - imo not critical
|
||||
@@ -599,20 +611,11 @@ public sealed class DisplayValueExtractor
|
||||
)
|
||||
);
|
||||
}
|
||||
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
|
||||
|
||||
List<DisplayValueResult> displayValue = new();
|
||||
foreach (var curve in curves)
|
||||
{
|
||||
if (documentToWorld is not null)
|
||||
{
|
||||
using var transformedCurve = curve.CreateTransformed(documentToWorld);
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(transformedCurve)));
|
||||
}
|
||||
else
|
||||
{
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
|
||||
}
|
||||
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
|
||||
}
|
||||
|
||||
return displayValue;
|
||||
@@ -630,7 +633,7 @@ public sealed class DisplayValueExtractor
|
||||
{
|
||||
var collections = GetSortedGeometryFromElement(element, null, null);
|
||||
// pass null for transform - curves are already in correct document coordinates
|
||||
return ProcessGeometryCollections(element, collections, null);
|
||||
return ProcessGeometryCollections(element, collections, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -639,9 +642,10 @@ public sealed class DisplayValueExtractor
|
||||
/// and reduce the risk of parameter ordering errors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="Solids"/> and <see cref="Meshes"/> are transformed to local coordinate space in SortGeometry.
|
||||
/// <see cref="Solids"/> and <see cref="Meshes"/> are transformed to symbol space in SortGeometry.
|
||||
/// <see cref="Curves"/>, <see cref="Polylines"/>, and <see cref="Points"/> remain in their original coordinate space
|
||||
/// and are transformed to world space during processing in ProcessGeometryCollections.
|
||||
/// and receive only the instance transform (if any) in ProcessGeometryCollections - reference point
|
||||
/// transform is handled by the point converters during conversion.
|
||||
/// </remarks>
|
||||
private sealed record GeometryCollections
|
||||
{
|
||||
|
||||
+1
@@ -61,6 +61,7 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveArrArrayToSpecklePolycurveConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveArrayConversionToSpeckle.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveConversionToSpeckle.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\CurveOriginToPlaneConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\EllipseToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\HermiteSplineToSpeckleConverter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Raw\Geometry\LineConversionToSpeckle.cs" />
|
||||
|
||||
+32
-21
@@ -2,6 +2,7 @@ using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.DoubleNumerics;
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Extensions;
|
||||
@@ -10,11 +11,14 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
|
||||
|
||||
/// <summary>
|
||||
/// Converts local to global maps to direct shapes.
|
||||
/// Spirit of the LocalToGlobalMap, we can't pass that object directly here bc it lives in Connectors.Common which I (ogu) don't want to bother with it.
|
||||
/// All this is poc that should be burned, once we enable proper block support to revit.
|
||||
/// When atomicObject comes from an InstanceProxy displayValue, parentDataObject
|
||||
/// provides the original DataObject's metadata (category, name) for semantic preservation.
|
||||
/// </summary>
|
||||
public class LocalToGlobalToDirectShapeConverter
|
||||
: ITypedConverter<(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix), DB.DirectShape>
|
||||
: ITypedConverter<
|
||||
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject),
|
||||
DB.DirectShape
|
||||
>
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly ITypedConverter<(Matrix4x4 matrix, string units), DB.Transform> _transformConverter;
|
||||
@@ -28,22 +32,13 @@ public class LocalToGlobalToDirectShapeConverter
|
||||
_transformConverter = transformConverter;
|
||||
}
|
||||
|
||||
public DB.DirectShape Convert((Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix) target)
|
||||
public DB.DirectShape Convert(
|
||||
(Base atomicObject, IReadOnlyCollection<Matrix4x4> matrix, DataObject? parentDataObject) target
|
||||
)
|
||||
{
|
||||
// 1- set ds category
|
||||
// NOTE: previously, builtInCategory was on the atomicObject level. this was subsequently moved to properties
|
||||
string? category = null;
|
||||
|
||||
// NOTE: no longer limited to DataObject since the introduction of mapper
|
||||
// The change from `if (target.atomicObject is DataObject dataObject)` is very hacky, but nothing else to do for now
|
||||
// TODO: better define prop interfaces for different applications
|
||||
if (
|
||||
target.atomicObject["properties"] is Dictionary<string, object?> properties
|
||||
&& properties.TryGetValue("builtInCategory", out var builtInCategory)
|
||||
)
|
||||
{
|
||||
category = builtInCategory?.ToString();
|
||||
}
|
||||
var category = ExtractBuiltInCategory(target.parentDataObject, target.atomicObject);
|
||||
var name = target.parentDataObject?.name ?? target.atomicObject.TryGetName();
|
||||
|
||||
var dsCategory = DB.BuiltInCategory.OST_GenericModel;
|
||||
if (category is not null)
|
||||
@@ -62,10 +57,6 @@ public class LocalToGlobalToDirectShapeConverter
|
||||
// 2 - init DirectShape
|
||||
var result = DB.DirectShape.CreateElement(_converterSettings.Current.Document, new DB.ElementId(dsCategory));
|
||||
|
||||
// NOTE: this should technically be in a property extraction class / helper method
|
||||
// This change is localised to [CNX-1825](https://linear.app/speckle/issue/CNX-1825/set-directshape-name)
|
||||
// TODO: Property extraction is a greater conversation which needs to be had: [CNX-1830](https://linear.app/speckle/issue/CNX-1830/data-exchange-investigations)
|
||||
var name = target.atomicObject.TryGetName();
|
||||
if (name is not null)
|
||||
{
|
||||
result.SetName(name);
|
||||
@@ -121,4 +112,24 @@ public class LocalToGlobalToDirectShapeConverter
|
||||
result.SetShape(transformedGeometries);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? ExtractBuiltInCategory(DataObject? parentDataObject, Base atomicObject)
|
||||
{
|
||||
// Try parent DataObject first (for InstanceProxy displayValue case)
|
||||
if (parentDataObject?.properties.TryGetValue("builtInCategory", out var cat) == true)
|
||||
{
|
||||
return cat?.ToString();
|
||||
}
|
||||
|
||||
// Fallback to atomicObject properties
|
||||
if (
|
||||
atomicObject["properties"] is Dictionary<string, object?> props
|
||||
&& props.TryGetValue("builtInCategory", out var fallbackCat)
|
||||
)
|
||||
{
|
||||
return fallbackCat?.ToString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-4
@@ -1,3 +1,4 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.RevitShared.Extensions;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
@@ -80,24 +81,45 @@ public class ClassPropertiesExtractor
|
||||
// get room id if applicable (only for FamilyInstance elements)
|
||||
if (familyInstance.Room is not null)
|
||||
{
|
||||
elementProperties.Add("roomId", familyInstance.Room.Id.ToString());
|
||||
elementProperties.Add("roomApplicationId", familyInstance.Room.UniqueId.ToString());
|
||||
}
|
||||
|
||||
// get space id if applicable (only for FamilyInstance elements)
|
||||
if (familyInstance.Space is not null)
|
||||
{
|
||||
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
|
||||
elementProperties.Add("spaceApplicationId", familyInstance.Space.UniqueId.ToString());
|
||||
}
|
||||
|
||||
// get toRoom and fromRoom for FamilyInstance elements with those properties (e.g. Doors)
|
||||
if (familyInstance.ToRoom is not null)
|
||||
{
|
||||
elementProperties.Add("toRoomId", familyInstance.ToRoom.Id.ToString());
|
||||
elementProperties.Add("toRoomApplicationId", familyInstance.ToRoom.UniqueId.ToString());
|
||||
}
|
||||
|
||||
if (familyInstance.FromRoom is not null)
|
||||
{
|
||||
elementProperties.Add("fromRoomId", familyInstance.FromRoom.Id.ToString());
|
||||
elementProperties.Add("fromRoomApplicationId", familyInstance.FromRoom.UniqueId.ToString());
|
||||
}
|
||||
|
||||
Element? parent = null;
|
||||
|
||||
#if REVIT2023_OR_GREATER
|
||||
BuiltInCategory bic = familyInstance.Category.BuiltInCategory;
|
||||
#else
|
||||
// Cast for 2022 and older
|
||||
BuiltInCategory bic = (BuiltInCategory)familyInstance.Category.Id.IntegerValue;
|
||||
#endif
|
||||
|
||||
if (bic == BuiltInCategory.OST_CurtainWallMullions || bic == BuiltInCategory.OST_CurtainWallPanels)
|
||||
{
|
||||
parent = familyInstance.Host;
|
||||
}
|
||||
|
||||
parent ??= familyInstance.SuperComponent;
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
elementProperties.Add("parentApplicationId", parent.UniqueId);
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (!e.IsFatal())
|
||||
|
||||
+9
-10
@@ -1,6 +1,5 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.RevitShared.Services;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
using Speckle.Objects.Primitive;
|
||||
|
||||
@@ -10,34 +9,34 @@ public class ArcToSpeckleConverter : ITypedConverter<DB.Arc, SOG.Arc>
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
|
||||
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ScalingServiceToSpeckle _scalingService;
|
||||
private readonly ITypedConverter<
|
||||
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
|
||||
SOG.Plane
|
||||
> _curveOriginToPlaneConverter;
|
||||
|
||||
public ArcToSpeckleConverter(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
|
||||
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
|
||||
ScalingServiceToSpeckle scalingService
|
||||
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter
|
||||
)
|
||||
{
|
||||
_converterSettings = converterSettings;
|
||||
_xyzToPointConverter = xyzToPointConverter;
|
||||
_planeConverter = planeConverter;
|
||||
_scalingService = scalingService;
|
||||
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
|
||||
}
|
||||
|
||||
public SOG.Arc Convert(DB.Arc target)
|
||||
{
|
||||
// Revit arcs are always counterclockwise in the arc normal direction. This aligns with Speckle arc plane convention.
|
||||
var arcPlane = DB.Plane.CreateByOriginAndBasis(target.Center, target.XDirection, target.YDirection);
|
||||
|
||||
DB.XYZ start = target.Evaluate(0, true);
|
||||
DB.XYZ end = target.Evaluate(1, true);
|
||||
DB.XYZ mid = target.Evaluate(0.5, true);
|
||||
|
||||
return new SOG.Arc()
|
||||
{
|
||||
plane = _planeConverter.Convert(arcPlane),
|
||||
plane = _curveOriginToPlaneConverter.Convert(
|
||||
(target.Center, target.XDirection, target.YDirection, target.Normal)
|
||||
),
|
||||
units = _converterSettings.Current.SpeckleUnits,
|
||||
endPoint = _xyzToPointConverter.Convert(end),
|
||||
startPoint = _xyzToPointConverter.Convert(start),
|
||||
|
||||
+13
-12
@@ -9,17 +9,20 @@ public class BoundingBoxXYZToSpeckleConverter : ITypedConverter<DB.BoundingBoxXY
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
|
||||
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<
|
||||
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
|
||||
SOG.Plane
|
||||
> _curveOriginToPlaneConverter;
|
||||
|
||||
public BoundingBoxXYZToSpeckleConverter(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
|
||||
ITypedConverter<DB.Plane, SOG.Plane> planeConverter
|
||||
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter
|
||||
)
|
||||
{
|
||||
_converterSettings = converterSettings;
|
||||
_xyzToPointConverter = xyzToPointConverter;
|
||||
_planeConverter = planeConverter;
|
||||
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
|
||||
}
|
||||
|
||||
public SOG.Box Convert(DB.BoundingBoxXYZ target)
|
||||
@@ -30,21 +33,19 @@ public class BoundingBoxXYZToSpeckleConverter : ITypedConverter<DB.BoundingBoxXY
|
||||
|
||||
// get the base plane of the bounding box from the transform
|
||||
var transform = target.Transform;
|
||||
var plane = DB.Plane.CreateByOriginAndBasis(
|
||||
transform.Origin,
|
||||
transform.BasisX.Normalize(),
|
||||
transform.BasisY.Normalize()
|
||||
);
|
||||
|
||||
var box = new SOG.Box()
|
||||
// assemble components for getting origin plane
|
||||
var xDir = transform.BasisX.Normalize();
|
||||
var yDir = transform.BasisY.Normalize();
|
||||
var normal = xDir.CrossProduct(yDir).Normalize();
|
||||
|
||||
return new SOG.Box()
|
||||
{
|
||||
xSize = new Interval { start = min.x, end = max.x },
|
||||
ySize = new Interval { start = min.y, end = max.y },
|
||||
zSize = new Interval { start = min.z, end = max.z },
|
||||
plane = _planeConverter.Convert(plane),
|
||||
plane = _curveOriginToPlaneConverter.Convert((transform.Origin, xDir, yDir, normal)),
|
||||
units = _converterSettings.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
return box;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-12
@@ -8,33 +8,32 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
|
||||
public class CircleToSpeckleConverter : ITypedConverter<DB.Arc, SOG.Circle>
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<
|
||||
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
|
||||
SOG.Plane
|
||||
> _curveOriginToPlaneConverter;
|
||||
private readonly ScalingServiceToSpeckle _scalingService;
|
||||
|
||||
public CircleToSpeckleConverter(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
|
||||
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter,
|
||||
ScalingServiceToSpeckle scalingService
|
||||
)
|
||||
{
|
||||
_converterSettings = converterSettings;
|
||||
_planeConverter = planeConverter;
|
||||
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
|
||||
_scalingService = scalingService;
|
||||
}
|
||||
|
||||
public SOG.Circle Convert(DB.Arc target)
|
||||
{
|
||||
public SOG.Circle Convert(DB.Arc target) =>
|
||||
// POC: should we check for arc of 360 and throw? Original CircleToSpeckle did not do this.
|
||||
|
||||
// see https://forums.autodesk.com/t5/revit-api-forum/how-to-retrieve-startangle-and-endangle-of-arc-object/td-p/7637128
|
||||
var arcPlane = DB.Plane.CreateByNormalAndOrigin(target.Normal, target.Center);
|
||||
var c = new SOG.Circle()
|
||||
new()
|
||||
{
|
||||
plane = _planeConverter.Convert(arcPlane),
|
||||
plane = _curveOriginToPlaneConverter.Convert(
|
||||
(target.Center, target.XDirection, target.YDirection, target.Normal)
|
||||
),
|
||||
radius = _scalingService.ScaleLength(target.Radius),
|
||||
units = _converterSettings.Current.SpeckleUnits
|
||||
};
|
||||
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
using Speckle.Converters.Common;
|
||||
using Speckle.Converters.Common.Objects;
|
||||
using Speckle.Converters.RevitShared.Settings;
|
||||
|
||||
namespace Speckle.Converters.RevitShared.ToSpeckle;
|
||||
|
||||
/// <summary>
|
||||
/// Converts curve origin plane components directly to Speckle plane.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Revit's <see cref="DB.Plane.CreateByOriginAndBasis"/> fails when the origin is ~10+ km from the internal origin,
|
||||
/// even though the documented limit is 16 km. This is a known API limitation:
|
||||
/// https://forums.autodesk.com/t5/revit-api-forum/the-input-point-lies-outside-of-revit-design-limits/td-p/12689066
|
||||
///</para>
|
||||
/// <para>
|
||||
/// This converter bypasses Revit plane creation entirely and builds Speckle planes directly from
|
||||
/// the curve's origin and basis vectors.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class CurveOriginToPlaneConverter
|
||||
: ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane>
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly ITypedConverter<DB.XYZ, SOG.Point> _xyzToPointConverter;
|
||||
private readonly ITypedConverter<DB.XYZ, SOG.Vector> _xyzToVectorConverter;
|
||||
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
|
||||
|
||||
public CurveOriginToPlaneConverter(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
ITypedConverter<DB.XYZ, SOG.Point> xyzToPointConverter,
|
||||
ITypedConverter<DB.XYZ, SOG.Vector> xyzToVectorConverter,
|
||||
ITypedConverter<DB.Plane, SOG.Plane> planeConverter
|
||||
)
|
||||
{
|
||||
_converterSettings = converterSettings;
|
||||
_xyzToPointConverter = xyzToPointConverter;
|
||||
_xyzToVectorConverter = xyzToVectorConverter;
|
||||
_planeConverter = planeConverter;
|
||||
}
|
||||
|
||||
public SOG.Plane Convert((DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal) target)
|
||||
{
|
||||
// within limits? then use standard Revit plane creation
|
||||
if (DB.XYZ.IsWithinLengthLimits(target.origin))
|
||||
{
|
||||
using var revitPlane = DB.Plane.CreateByOriginAndBasis(target.origin, target.xDir, target.yDir);
|
||||
return _planeConverter.Convert(revitPlane);
|
||||
}
|
||||
|
||||
// beyond limits? then build Speckle plane directly
|
||||
return new SOG.Plane
|
||||
{
|
||||
origin = _xyzToPointConverter.Convert(target.origin),
|
||||
xdir = _xyzToVectorConverter.Convert(target.xDir),
|
||||
ydir = _xyzToVectorConverter.Convert(target.yDir),
|
||||
normal = _xyzToVectorConverter.Convert(target.normal),
|
||||
units = _converterSettings.Current.SpeckleUnits
|
||||
};
|
||||
}
|
||||
}
|
||||
+21
-21
@@ -9,40 +9,40 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
|
||||
public class EllipseToSpeckleConverter : ITypedConverter<DB.Ellipse, SOG.Ellipse>
|
||||
{
|
||||
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
|
||||
private readonly ITypedConverter<DB.Plane, SOG.Plane> _planeConverter;
|
||||
private readonly ITypedConverter<
|
||||
(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal),
|
||||
SOG.Plane
|
||||
> _curveOriginToPlaneConverter;
|
||||
private readonly ScalingServiceToSpeckle _scalingService;
|
||||
|
||||
public EllipseToSpeckleConverter(
|
||||
IConverterSettingsStore<RevitConversionSettings> converterSettings,
|
||||
ITypedConverter<DB.Plane, SOG.Plane> planeConverter,
|
||||
ITypedConverter<(DB.XYZ origin, DB.XYZ xDir, DB.XYZ yDir, DB.XYZ normal), SOG.Plane> curveOriginToPlaneConverter,
|
||||
ScalingServiceToSpeckle scalingService
|
||||
)
|
||||
{
|
||||
_converterSettings = converterSettings;
|
||||
_planeConverter = planeConverter;
|
||||
_curveOriginToPlaneConverter = curveOriginToPlaneConverter;
|
||||
_scalingService = scalingService;
|
||||
}
|
||||
|
||||
public SOG.Ellipse Convert(DB.Ellipse target)
|
||||
{
|
||||
using (DB.Plane basePlane = DB.Plane.CreateByOriginAndBasis(target.Center, target.XDirection, target.YDirection))
|
||||
{
|
||||
var trim = target.IsBound
|
||||
? new Interval { start = target.GetEndParameter(0), end = target.GetEndParameter(1) }
|
||||
: null;
|
||||
var trim = target.IsBound
|
||||
? new Interval { start = target.GetEndParameter(0), end = target.GetEndParameter(1) }
|
||||
: null;
|
||||
|
||||
return new SOG.Ellipse()
|
||||
{
|
||||
plane = _planeConverter.Convert(basePlane),
|
||||
// POC: scale length correct? seems right?
|
||||
firstRadius = _scalingService.ScaleLength(target.RadiusX),
|
||||
secondRadius = _scalingService.ScaleLength(target.RadiusY),
|
||||
// POC: original EllipseToSpeckle() method was setting this twice
|
||||
domain = Interval.UnitInterval,
|
||||
trimDomain = trim,
|
||||
length = _scalingService.ScaleLength(target.Length),
|
||||
units = _converterSettings.Current.SpeckleUnits
|
||||
};
|
||||
}
|
||||
return new SOG.Ellipse()
|
||||
{
|
||||
plane = _curveOriginToPlaneConverter.Convert(
|
||||
(target.Center, target.XDirection, target.YDirection, target.Normal)
|
||||
),
|
||||
firstRadius = _scalingService.ScaleLength(target.RadiusX),
|
||||
secondRadius = _scalingService.ScaleLength(target.RadiusY),
|
||||
domain = Interval.UnitInterval,
|
||||
trimDomain = trim,
|
||||
length = _scalingService.ScaleLength(target.Length),
|
||||
units = _converterSettings.Current.SpeckleUnits
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ using Speckle.InterfaceGenerator;
|
||||
|
||||
namespace Speckle.Connectors.DUI.Bridge;
|
||||
|
||||
/// <remarks>
|
||||
/// This class was initially designed as an evolution
|
||||
/// of hostapp specific idle managers, since they followed a similar logic.
|
||||
/// However, has ended up a little over-engineered, so since then, for Revit connector
|
||||
/// we've started to prefer a simpler solution that fits only the needs of said host app.
|
||||
/// </remarks>
|
||||
//should be registered as singleton
|
||||
[GenerateAutoInterface]
|
||||
public sealed class IdleCallManager(ITopLevelExceptionHandler topLevelExceptionHandler) : IIdleCallManager
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Speckle.Connectors.DUI.Models;
|
||||
public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJsonSerializer serializer)
|
||||
: IDocumentModelStore
|
||||
{
|
||||
public event EventHandler<ModelCardsChangedEventArgs>? ModelCardsChanged;
|
||||
private readonly List<ModelCard> _models = new();
|
||||
|
||||
/// <summary>
|
||||
@@ -152,6 +153,7 @@ public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJs
|
||||
{
|
||||
var state = Serialize();
|
||||
HostAppSaveState(state);
|
||||
ModelCardsChanged?.Invoke(this, new ModelCardsChangedEventArgs(_models.ToList().AsReadOnly()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Speckle.Connectors.DUI.Models.Card;
|
||||
|
||||
namespace Speckle.Connectors.DUI.Models;
|
||||
|
||||
public sealed class ModelCardsChangedEventArgs(IReadOnlyList<ModelCard> modelCards) : EventArgs
|
||||
{
|
||||
public IReadOnlyList<ModelCard> ModelCards { get; } = modelCards;
|
||||
}
|
||||
@@ -14,7 +14,6 @@ namespace Speckle.Connectors.Common.Caching;
|
||||
public interface ISendConversionCache
|
||||
{
|
||||
void StoreSendResult(string projectId, IReadOnlyDictionary<Id, ObjectReference> convertedReferences);
|
||||
void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference);
|
||||
|
||||
/// <summary>
|
||||
/// <para>Call this method whenever you need to invalidate a set of objects that have changed in the host app.</para>
|
||||
|
||||
@@ -11,8 +11,6 @@ public class NullSendConversionCache : ISendConversionCache
|
||||
{
|
||||
public void StoreSendResult(string projectId, IReadOnlyDictionary<Id, ObjectReference> convertedReferences) { }
|
||||
|
||||
public void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference) { }
|
||||
|
||||
public void EvictObjects(IEnumerable<string> objectIds) { }
|
||||
|
||||
public void ClearCache() { }
|
||||
|
||||
@@ -17,11 +17,6 @@ public class SendConversionCache : ISendConversionCache
|
||||
}
|
||||
}
|
||||
|
||||
public void AppendSendResult(string projectId, string applicationId, ObjectReference convertedReference)
|
||||
{
|
||||
Cache[(applicationId, projectId)] = convertedReference;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void EvictObjects(IEnumerable<string> objectIds) =>
|
||||
Cache = Cache
|
||||
|
||||
Reference in New Issue
Block a user