Merge branch 'dev' into bilal/cnx-3123-consistently-handle-view-direction-basis
This commit is contained in:
@@ -76,6 +76,6 @@ jobs:
|
||||
display-workflow-run-url-interval: 10s
|
||||
|
||||
# Allows us to inspect the artifacts of failed builds, since this below step will be skipped if the above step fails
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
- uses: geekyeggo/delete-artifact@v6
|
||||
with:
|
||||
name: output-*
|
||||
|
||||
+9
-1
@@ -2,6 +2,8 @@ using Autodesk.Revit.DB;
|
||||
using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Connectors.DUI.Models;
|
||||
using Speckle.Connectors.DUI.Models.Card;
|
||||
using Speckle.Connectors.DUI.Utils;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Connectors.Revit.Plugin;
|
||||
using Speckle.Connectors.RevitShared;
|
||||
using Speckle.Connectors.RevitShared.Operations.Send.Filters;
|
||||
@@ -24,6 +26,8 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
|
||||
private readonly ISpeckleApplication _speckleApplication;
|
||||
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
|
||||
private readonly IRevitTask _revitTask;
|
||||
private readonly ParameterUpdater _parameterUpdater;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
public BasicConnectorBindingRevit(
|
||||
DocumentModelStore store,
|
||||
@@ -31,7 +35,9 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
|
||||
RevitContext revitContext,
|
||||
ISpeckleApplication speckleApplication,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
IRevitTask revitTask
|
||||
IRevitTask revitTask,
|
||||
ParameterUpdater parameterUpdater,
|
||||
IJsonSerializer jsonSerializer
|
||||
)
|
||||
{
|
||||
Name = "baseBinding";
|
||||
@@ -41,6 +47,8 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
|
||||
_speckleApplication = speckleApplication;
|
||||
_topLevelExceptionHandler = topLevelExceptionHandler;
|
||||
_revitTask = revitTask;
|
||||
_parameterUpdater = parameterUpdater;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
Commands = new BasicConnectorBindingCommands(parent);
|
||||
|
||||
_store.DocumentChanged += (_, _) =>
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
using Autodesk.Revit.DB;
|
||||
using Speckle.Connectors.DUI.Bindings;
|
||||
using Speckle.Connectors.DUI.Bridge;
|
||||
using Speckle.Connectors.DUI.Utils;
|
||||
using Speckle.Connectors.Revit.HostApp;
|
||||
using Speckle.Connectors.Revit.Operations.Receive;
|
||||
using Speckle.Connectors.Revit.Plugin;
|
||||
using Speckle.Connectors.RevitShared;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Connectors.Revit.Bindings;
|
||||
|
||||
public static class ParameterScopes
|
||||
{
|
||||
public const string INSTANCE = "Instance Parameters";
|
||||
public const string TYPE = "Type Parameters";
|
||||
public const string SYSTEM_TYPE = "System Type Parameters";
|
||||
}
|
||||
|
||||
public record ParsedParameterPath(string Scope, string Category, string Name)
|
||||
{
|
||||
public string[] ToArray() => [Scope, Category, Name];
|
||||
}
|
||||
|
||||
internal sealed class RevitParametersBinding : IParametersBinding
|
||||
{
|
||||
public string Name => "parametersBinding";
|
||||
public IBrowserBridge Parent { get; }
|
||||
|
||||
private readonly RevitContext _revitContext;
|
||||
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
|
||||
private readonly IRevitTask _revitTask;
|
||||
private readonly ParameterUpdater _parameterUpdater;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IBasicConnectorBinding _baseBinding;
|
||||
|
||||
public RevitParametersBinding(
|
||||
IBrowserBridge parent,
|
||||
RevitContext revitContext,
|
||||
ITopLevelExceptionHandler topLevelExceptionHandler,
|
||||
IRevitTask revitTask,
|
||||
ParameterUpdater parameterUpdater,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IBasicConnectorBinding baseBinding
|
||||
)
|
||||
{
|
||||
Parent = parent;
|
||||
_revitContext = revitContext;
|
||||
_topLevelExceptionHandler = topLevelExceptionHandler;
|
||||
_revitTask = revitTask;
|
||||
_parameterUpdater = parameterUpdater;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_baseBinding = baseBinding;
|
||||
}
|
||||
|
||||
public async Task Update(string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wrapper = _jsonSerializer.Deserialize<ParameterChangesWrapper>(payload);
|
||||
var requests = wrapper?.Changes;
|
||||
|
||||
if (requests == null || requests.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var activeUIDoc =
|
||||
_revitContext.UIApplication?.ActiveUIDocument
|
||||
?? throw new SpeckleException("Unable to retrieve active UI document");
|
||||
var doc = activeUIDoc.Document;
|
||||
|
||||
int successCount = 0;
|
||||
List<string> errors = [];
|
||||
|
||||
await _revitTask
|
||||
.RunAsync(() =>
|
||||
{
|
||||
using var t = new Transaction(doc, "Speckle: Apply Parameter Changes");
|
||||
|
||||
// silence pop-ups like "duplicate mark values" etc. which blocks our param updates
|
||||
var failureOptions = t.GetFailureHandlingOptions();
|
||||
failureOptions.SetFailuresPreprocessor(new HideWarningsFailuresPreprocessor());
|
||||
t.SetFailureHandlingOptions(failureOptions);
|
||||
|
||||
t.Start();
|
||||
|
||||
foreach (var request in requests)
|
||||
{
|
||||
if (!TryValidateAndParseRequest(doc, request, out var element, out var parsedPath, out var errorMessage))
|
||||
{
|
||||
errors.Add(errorMessage!);
|
||||
continue;
|
||||
}
|
||||
|
||||
object? rawValue = request.To;
|
||||
if (rawValue is Newtonsoft.Json.Linq.JValue jValue)
|
||||
{
|
||||
rawValue = jValue.Value;
|
||||
}
|
||||
|
||||
var result = _parameterUpdater.Update(
|
||||
element!,
|
||||
parsedPath!.ToArray(),
|
||||
rawValue,
|
||||
request.InternalDefinitionName
|
||||
);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(result.ErrorMessage ?? "Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
t.Commit();
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
var groupedErrors = errors.GroupBy(e => e).Select(g => g.Count() > 1 ? $"{g.Count()} x {g.Key}" : g.Key);
|
||||
await _baseBinding.Commands.SetGlobalNotification(
|
||||
ToastNotificationType.WARNING,
|
||||
"Update Completed with Issues",
|
||||
$"Applied {successCount} updates. Encountered {errors.Count} errors: {string.Join(" | ", groupedErrors)}",
|
||||
autoClose: false
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _baseBinding.Commands.SetGlobalNotification(
|
||||
ToastNotificationType.SUCCESS,
|
||||
"Parameters Updated",
|
||||
$"Successfully applied {successCount} updates."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_topLevelExceptionHandler.CatchUnhandled(
|
||||
() => throw new SpeckleException("Failed to apply parameter updates", ex)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryValidateAndParseRequest(
|
||||
Document doc,
|
||||
ParameterChangeRequest request,
|
||||
out Element? element,
|
||||
out ParsedParameterPath? parsedPath,
|
||||
out string? errorMessage
|
||||
)
|
||||
{
|
||||
element = null;
|
||||
parsedPath = null;
|
||||
errorMessage = null;
|
||||
|
||||
if (string.IsNullOrEmpty(request.ApplicationId))
|
||||
{
|
||||
errorMessage = "Missing ApplicationId.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var elementId = ElementIdHelper.GetElementIdFromUniqueId(doc, request.ApplicationId);
|
||||
if (elementId == null)
|
||||
{
|
||||
errorMessage = $"Element UniqueId not found: {request.ApplicationId}";
|
||||
return false;
|
||||
}
|
||||
|
||||
element = doc.GetElement(elementId);
|
||||
if (element == null)
|
||||
{
|
||||
errorMessage = $"Element is null for Id: {elementId}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var rawPath = request.Path;
|
||||
if (string.IsNullOrEmpty(rawPath))
|
||||
{
|
||||
errorMessage = "Path is missing.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rawPath.StartsWith("properties.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
rawPath = rawPath[11..];
|
||||
}
|
||||
|
||||
if (rawPath.StartsWith("parameters.", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
rawPath = rawPath[11..];
|
||||
}
|
||||
|
||||
var pathParts = rawPath.Split(['.'], 3);
|
||||
if (pathParts.Length != 3)
|
||||
{
|
||||
errorMessage = $"Path must have 3 parts. Got: '{rawPath}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
parsedPath = new ParsedParameterPath(pathParts[0], pathParts[1], pathParts[2]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ParameterChangesWrapper
|
||||
{
|
||||
public List<ParameterChangeRequest>? Changes { get; set; }
|
||||
}
|
||||
@@ -39,6 +39,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
private readonly LinkedModelHandler _linkedModelHandler;
|
||||
private readonly IThreadContext _threadContext;
|
||||
private readonly ISendOperationManagerFactory _sendOperationManagerFactory;
|
||||
private readonly ParameterUpdater _parameterUpdater;
|
||||
private bool _isDocChangedSubscribed;
|
||||
private EventHandler<Autodesk.Revit.DB.Events.DocumentChangedEventArgs>? _documentChangedHandler;
|
||||
private readonly ConnectorConfig _config;
|
||||
@@ -67,6 +68,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
IThreadContext threadContext,
|
||||
IRevitTask revitTask,
|
||||
ISendOperationManagerFactory sendOperationManagerFactory,
|
||||
ParameterUpdater parameterUpdater,
|
||||
IConfigStore configStore
|
||||
)
|
||||
: base("sendBinding", bridge)
|
||||
@@ -84,6 +86,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
_linkedModelHandler = linkedModelHandler;
|
||||
_threadContext = threadContext;
|
||||
_sendOperationManagerFactory = sendOperationManagerFactory;
|
||||
_parameterUpdater = parameterUpdater;
|
||||
_config = configStore.GetConnectorConfig();
|
||||
|
||||
Commands = new SendBindingUICommands(bridge);
|
||||
@@ -198,6 +201,44 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
|
||||
);
|
||||
}
|
||||
|
||||
public async Task UpdateParameters(List<ParameterChangeRequest> changes)
|
||||
{
|
||||
var document = _revitContext.UIApplication?.ActiveUIDocument?.Document;
|
||||
if (document == null)
|
||||
{
|
||||
throw new SpeckleException("No document is active.");
|
||||
}
|
||||
|
||||
await _threadContext.RunOnMainAsync(() =>
|
||||
{
|
||||
using var transaction = new Transaction(document, "Speckle Parameter Updates");
|
||||
transaction.Start();
|
||||
|
||||
foreach (var change in changes)
|
||||
{
|
||||
var element = document.GetElement(change.ApplicationId);
|
||||
if (element == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = ParsePath(change.Path);
|
||||
var result = _parameterUpdater.Update(element, path, change.To);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
return Task.FromResult(true);
|
||||
});
|
||||
}
|
||||
|
||||
private string[] ParsePath(string concatenatedPath)
|
||||
{
|
||||
// "properties.Parameters.Type Parameters.Other.Family Name"
|
||||
// → ["Type Parameters", "Other", "Family Name"]
|
||||
var segments = concatenatedPath.Split('.');
|
||||
return segments.Skip(2).ToArray();
|
||||
}
|
||||
|
||||
private static (string? fileName, long? fileBytes) GetFileInfo(Document document)
|
||||
{
|
||||
string fullPath = document.PathName;
|
||||
|
||||
+4
@@ -53,6 +53,9 @@ public static class ServiceRegistration
|
||||
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IBasicConnectorBinding>());
|
||||
serviceCollection.AddSingleton<IBasicConnectorBinding, BasicConnectorBindingRevit>();
|
||||
|
||||
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IParametersBinding>());
|
||||
serviceCollection.AddSingleton<IParametersBinding, RevitParametersBinding>();
|
||||
|
||||
// serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
|
||||
|
||||
// send operation and dependencies
|
||||
@@ -66,6 +69,7 @@ public static class ServiceRegistration
|
||||
serviceCollection.AddSingleton<ToSpeckleSettingsManager>();
|
||||
serviceCollection.AddSingleton<ToHostSettingsManager>();
|
||||
serviceCollection.AddSingleton<LinkedModelHandler>();
|
||||
serviceCollection.AddSingleton<ParameterUpdater>();
|
||||
|
||||
// receive operation and dependencies
|
||||
serviceCollection.AddScoped<IHostObjectBuilder, RevitHostObjectBuilder>();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
public class ParameterChangeRequest
|
||||
{
|
||||
public required string ApplicationId { get; init; }
|
||||
public required string Path { get; init; }
|
||||
public object? To { get; init; }
|
||||
public string? InternalDefinitionName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Converters.RevitShared.Helpers;
|
||||
using Speckle.Converters.RevitShared.Services;
|
||||
using Speckle.Sdk;
|
||||
using DB = Autodesk.Revit.DB;
|
||||
|
||||
namespace Speckle.Connectors.Revit.HostApp;
|
||||
|
||||
/// <summary>
|
||||
/// Updates parameter values on Revit elements. Mirrors the structure from ParameterExtractor.
|
||||
/// Path format: ["Instance Parameters" | "Type Parameters" | "System Type Parameters", "GroupName", "ParameterName"]
|
||||
/// </summary>
|
||||
public class ParameterUpdater
|
||||
{
|
||||
private readonly RevitContext _revitContext;
|
||||
private readonly ScalingServiceToHost _scalingServiceToHost;
|
||||
private readonly ILogger<ParameterUpdater> _logger;
|
||||
|
||||
public ParameterUpdater(
|
||||
RevitContext revitContext,
|
||||
ScalingServiceToHost scalingServiceToHost,
|
||||
ILogger<ParameterUpdater> logger
|
||||
)
|
||||
{
|
||||
_revitContext = revitContext;
|
||||
_scalingServiceToHost = scalingServiceToHost;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public UpdateResult Update(DB.Element element, string[] path, object? newValue, string? internalDefinitionName = null)
|
||||
{
|
||||
// path = ["Instance Parameters", "Identity Data", "Mark"]
|
||||
if (path.Length != 3)
|
||||
{
|
||||
return UpdateResult.Fail(
|
||||
$"Path must have exactly 3 segments: [scope, group, parameter]. Got: {string.Join(" → ", path)}"
|
||||
);
|
||||
}
|
||||
|
||||
var parameterScope = path[0]; // "Instance Parameters" | "Type Parameters" | "System Type Parameters"
|
||||
var groupName = path[1]; // "Identity Data", "Dimensions", etc.
|
||||
var parameterKey = path[2]; // human-readable name (or internalDefinitionName if collision)
|
||||
|
||||
// get target element based on scope
|
||||
var targetElement = GetTargetElement(element, parameterScope);
|
||||
if (targetElement == null)
|
||||
{
|
||||
return UpdateResult.Fail($"Could not resolve target for scope: {parameterScope}");
|
||||
}
|
||||
|
||||
// find the parameter (now using the robust lookup)
|
||||
var parameter = FindParameter(targetElement, groupName, parameterKey, internalDefinitionName);
|
||||
if (parameter == null)
|
||||
{
|
||||
return UpdateResult.Fail($"Parameter not found: {parameterKey} in group {groupName}");
|
||||
}
|
||||
|
||||
if (parameter.IsReadOnly)
|
||||
{
|
||||
return UpdateResult.Fail($"Parameter '{parameterKey}' is readonly in Revit");
|
||||
}
|
||||
|
||||
return SetParameterValue(parameter, newValue);
|
||||
}
|
||||
|
||||
private DB.Element? GetTargetElement(DB.Element element, string scope) =>
|
||||
scope switch
|
||||
{
|
||||
"Instance Parameters" => element,
|
||||
"Type Parameters" => GetTypeElement(element),
|
||||
"System Type Parameters" => GetSystemTypeElement(element),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private DB.Element? GetTypeElement(DB.Element element)
|
||||
{
|
||||
var typeId = element.GetTypeId();
|
||||
if (typeId == DB.ElementId.InvalidElementId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _revitContext.UIApplication?.ActiveUIDocument.Document.GetElement(typeId);
|
||||
}
|
||||
|
||||
private DB.Element? GetSystemTypeElement(DB.Element element)
|
||||
{
|
||||
var system = GetMEPSystem(element);
|
||||
if (system == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _revitContext.UIApplication?.ActiveUIDocument.Document.GetElement(system.GetTypeId());
|
||||
}
|
||||
|
||||
private DB.MEPSystem? GetMEPSystem(DB.Element element)
|
||||
{
|
||||
if (element is DB.MEPCurve curve)
|
||||
{
|
||||
return curve.MEPSystem;
|
||||
}
|
||||
|
||||
if (element is DB.FamilyInstance fi)
|
||||
{
|
||||
var cm = fi.MEPModel?.ConnectorManager;
|
||||
if (cm != null)
|
||||
{
|
||||
foreach (DB.Connector conn in cm.Connectors)
|
||||
{
|
||||
if (conn.ConnectorType == DB.ConnectorType.Physical && conn.IsConnected && conn.MEPSystem != null)
|
||||
{
|
||||
return conn.MEPSystem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DB.Parameter? FindParameter(
|
||||
DB.Element element,
|
||||
string groupName,
|
||||
string parameterKey,
|
||||
string? internalDefinitionName
|
||||
)
|
||||
{
|
||||
// fast path: direct lookup using the internal definition name
|
||||
if (!string.IsNullOrEmpty(internalDefinitionName))
|
||||
{
|
||||
// try as BuiltInParameter enum
|
||||
if (Enum.TryParse(internalDefinitionName, out DB.BuiltInParameter bip) && bip != DB.BuiltInParameter.INVALID)
|
||||
{
|
||||
var param = element.get_Parameter(bip);
|
||||
if (param != null)
|
||||
{
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
// try as shared parameter Guid
|
||||
if (Guid.TryParse(internalDefinitionName, out Guid guid))
|
||||
{
|
||||
var param = element.get_Parameter(guid);
|
||||
if (param != null)
|
||||
{
|
||||
return param;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: iteration for project parameters or missing internal names
|
||||
DB.Parameter? fallbackParameter = null;
|
||||
|
||||
foreach (DB.Parameter parameter in element.Parameters)
|
||||
{
|
||||
var definition = parameter.Definition;
|
||||
if (definition == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentInternalName = GetInternalDefinitionName(parameter);
|
||||
var humanName = definition.Name;
|
||||
|
||||
// exact internal name match (covers project params that aren't BuiltIn/Shared)
|
||||
if (!string.IsNullOrEmpty(internalDefinitionName) && currentInternalName == internalDefinitionName)
|
||||
{
|
||||
return parameter;
|
||||
}
|
||||
|
||||
// fallback human-readable name matching
|
||||
if (humanName == parameterKey || currentInternalName == parameterKey)
|
||||
{
|
||||
var paramGroup = definition.GetGroupTypeId();
|
||||
var groupLabel = DB.LabelUtils.GetLabelForGroup(paramGroup);
|
||||
|
||||
if (groupLabel == groupName)
|
||||
{
|
||||
return parameter;
|
||||
}
|
||||
fallbackParameter ??= parameter;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackParameter;
|
||||
}
|
||||
|
||||
private string GetInternalDefinitionName(DB.Parameter parameter)
|
||||
{
|
||||
if (parameter.Definition is DB.InternalDefinition internalDef)
|
||||
{
|
||||
var bip = internalDef.BuiltInParameter;
|
||||
if (bip != DB.BuiltInParameter.INVALID)
|
||||
{
|
||||
return bip.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return parameter.Definition.Name;
|
||||
}
|
||||
|
||||
private UpdateResult SetParameterValue(DB.Parameter parameter, object? newValue)
|
||||
{
|
||||
var paramName = parameter.Definition.Name;
|
||||
if (newValue == null)
|
||||
{
|
||||
if (parameter.StorageType == DB.StorageType.String)
|
||||
{
|
||||
return parameter.Set(string.Empty)
|
||||
? UpdateResult.Success()
|
||||
: UpdateResult.Fail("Failed to clear string parameter");
|
||||
}
|
||||
return UpdateResult.Fail("Cannot set non-string parameter to null");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var success = parameter.StorageType switch
|
||||
{
|
||||
DB.StorageType.String => parameter.Set(newValue.ToString()),
|
||||
DB.StorageType.Integer => SetIntegerValue(parameter, newValue),
|
||||
DB.StorageType.Double => SetDoubleValue(parameter, newValue),
|
||||
DB.StorageType.ElementId => SetElementIdValue(parameter, newValue),
|
||||
_ => false
|
||||
};
|
||||
|
||||
return success ? UpdateResult.Success() : UpdateResult.Fail($"Failed to set parameter value to: {newValue}");
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to set parameter value");
|
||||
return UpdateResult.Fail($"Exception for '{paramName}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool SetIntegerValue(DB.Parameter parameter, object newValue)
|
||||
{
|
||||
if (newValue is int i)
|
||||
{
|
||||
return parameter.Set(i);
|
||||
}
|
||||
|
||||
if (newValue is bool b)
|
||||
{
|
||||
return parameter.Set(b ? 1 : 0);
|
||||
}
|
||||
|
||||
if (int.TryParse(newValue.ToString(), out var parsed))
|
||||
{
|
||||
return parameter.Set(parsed);
|
||||
}
|
||||
|
||||
var strValue = newValue.ToString();
|
||||
if (strValue == "Yes")
|
||||
{
|
||||
return parameter.Set(1);
|
||||
}
|
||||
if (strValue == "No")
|
||||
{
|
||||
return parameter.Set(0);
|
||||
}
|
||||
|
||||
return parameter.SetValueString(strValue);
|
||||
}
|
||||
|
||||
private bool SetDoubleValue(DB.Parameter parameter, object newValue)
|
||||
{
|
||||
double doubleValue;
|
||||
|
||||
if (newValue is double d)
|
||||
{
|
||||
doubleValue = d;
|
||||
}
|
||||
else if (newValue is int intVal)
|
||||
{
|
||||
doubleValue = intVal;
|
||||
}
|
||||
else if (double.TryParse(newValue.ToString(), out var parsed))
|
||||
{
|
||||
doubleValue = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var internalValue = _scalingServiceToHost.ScaleToNative(doubleValue, parameter.GetUnitTypeId());
|
||||
return parameter.Set(internalValue);
|
||||
}
|
||||
|
||||
private bool SetElementIdValue(DB.Parameter parameter, object newValue)
|
||||
{
|
||||
if (newValue is DB.ElementId eid)
|
||||
{
|
||||
return parameter.Set(eid);
|
||||
}
|
||||
|
||||
// TODO: check this fckr later
|
||||
|
||||
// if (newValue is long idInt)
|
||||
// {
|
||||
// #if REVIT2024_OR_GREATER
|
||||
// return parameter.Set(new DB.ElementId(idInt));
|
||||
// #else
|
||||
// return parameter.Set(new DB.ElementId((long)idInt));
|
||||
// #endif
|
||||
// }
|
||||
//
|
||||
// if (long.TryParse(newValue.ToString(), out var parsedId))
|
||||
// {
|
||||
// #if REVIT2024_OR_GREATER
|
||||
// return parameter.Set(new DB.ElementId(parsedId));
|
||||
// #else
|
||||
// return parameter.Set(new DB.ElementId((long)parsedId));
|
||||
// #endif
|
||||
// }
|
||||
|
||||
var elementName = newValue.ToString();
|
||||
if (elementName != null)
|
||||
{
|
||||
var foundElement = FindElementByName(elementName);
|
||||
if (foundElement != null)
|
||||
{
|
||||
return parameter.Set(foundElement.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private DB.Element? FindElementByName(string name)
|
||||
{
|
||||
var doc = _revitContext.UIApplication?.ActiveUIDocument.Document;
|
||||
|
||||
using var materialCollector = new DB.FilteredElementCollector(doc);
|
||||
var material = materialCollector.OfClass(typeof(DB.Material)).FirstOrDefault(e => e.Name == name);
|
||||
if (material != null)
|
||||
{
|
||||
return material;
|
||||
}
|
||||
|
||||
using var levelCollector = new DB.FilteredElementCollector(doc);
|
||||
var level = levelCollector.OfClass(typeof(DB.Level)).FirstOrDefault(e => e.Name == name);
|
||||
if (level != null)
|
||||
{
|
||||
return level;
|
||||
}
|
||||
using var phaseCollector = new DB.FilteredElementCollector(doc);
|
||||
var phase = phaseCollector.OfClass(typeof(DB.Phase)).FirstOrDefault(e => e.Name == name);
|
||||
if (phase != null)
|
||||
{
|
||||
return phase;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we will see, extract this guy out
|
||||
public readonly struct UpdateResult
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
private UpdateResult(bool success, string? error)
|
||||
{
|
||||
IsSuccess = success;
|
||||
ErrorMessage = error;
|
||||
}
|
||||
|
||||
public static UpdateResult Success() => new(true, null);
|
||||
|
||||
public static UpdateResult Fail(string message) => new(false, message);
|
||||
}
|
||||
@@ -45,6 +45,8 @@ public static class SupportedCategoriesUtils
|
||||
#else
|
||||
category.Name == "OST_Grids";
|
||||
#endif
|
||||
case CategoryType.AnalyticalModel:
|
||||
return true;
|
||||
|
||||
case CategoryType.Model:
|
||||
return
|
||||
|
||||
+3
@@ -15,6 +15,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\BasicConnectorBindingRevit.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitBaseBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitParametersBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitReceiveBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\SelectionBinding.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Bindings\RevitSendBinding.cs" />
|
||||
@@ -27,6 +28,8 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\FamilyTransformUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LevelUnpacker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LinkedModelHandler.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ParameterChangeRequest.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\ParameterUpdater.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitFamilyBaker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitMaterialBaker.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitViewBaker.cs" />
|
||||
|
||||
+58
-19
@@ -26,8 +26,7 @@ public class ExpandCollection : GH_Component, IGH_VariableParameterComponent
|
||||
public override Guid ComponentGuid => GetType().GUID;
|
||||
protected override Bitmap Icon => Resources.speckle_collections_expand;
|
||||
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager)
|
||||
{
|
||||
protected override void RegisterInputParams(GH_InputParamManager pManager) =>
|
||||
pManager.AddParameter(
|
||||
new SpeckleCollectionParam(GH_ParamAccess.item),
|
||||
"Collection",
|
||||
@@ -35,7 +34,6 @@ public class ExpandCollection : GH_Component, IGH_VariableParameterComponent
|
||||
"The Collection you want to expand",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager) { }
|
||||
|
||||
@@ -180,27 +178,68 @@ public class ExpandCollection : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
private void CreateOutputs(List<OutputParamWrapper> outputParams)
|
||||
{
|
||||
// TODO: better, nicer handling of creation/removal
|
||||
while (Params.Output.Count > 0)
|
||||
{
|
||||
Params.UnregisterOutputParameter(Params.Output[^1]);
|
||||
}
|
||||
bool needsMaintenance = false;
|
||||
|
||||
foreach (var newParam in outputParams)
|
||||
// remove old parameters that are no longer present
|
||||
for (int i = Params.Output.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var param = new SpeckleOutputParam
|
||||
var existingParam = Params.Output[i];
|
||||
if (outputParams.All(p => p.Param.Name != existingParam.Name))
|
||||
{
|
||||
Name = newParam.Param.Name,
|
||||
NickName = newParam.Param.NickName,
|
||||
MutableNickName = false,
|
||||
Access = newParam.Param.Access
|
||||
};
|
||||
Params.RegisterOutputParam(param);
|
||||
Params.UnregisterOutputParameter(existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
// add new parameters and update existing ones in place
|
||||
for (int i = 0; i < outputParams.Count; i++)
|
||||
{
|
||||
var targetParam = outputParams[i].Param;
|
||||
var existingParam = Params.Output.FirstOrDefault(p => p.Name == targetParam.Name);
|
||||
|
||||
if (existingParam != null)
|
||||
{
|
||||
if (existingParam.Access != targetParam.Access)
|
||||
{
|
||||
existingParam.Access = targetParam.Access;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
if (existingParam.NickName != targetParam.NickName)
|
||||
{
|
||||
existingParam.NickName = targetParam.NickName;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
int currentIndex = Params.Output.IndexOf(existingParam);
|
||||
if (currentIndex != i)
|
||||
{
|
||||
Params.Output.RemoveAt(currentIndex);
|
||||
Params.Output.Insert(i, existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newParam = new SpeckleOutputParam
|
||||
{
|
||||
Name = targetParam.Name,
|
||||
NickName = targetParam.NickName,
|
||||
MutableNickName = false,
|
||||
Access = targetParam.Access,
|
||||
Description = targetParam.Description
|
||||
};
|
||||
Params.RegisterOutputParam(newParam, i);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMaintenance)
|
||||
{
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void VariableParameterMaintenance() { }
|
||||
|
||||
+4
-5
@@ -45,13 +45,13 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
{
|
||||
var properties = new Dictionary<string, ISpecklePropertyGoo>();
|
||||
var groupGoo = new SpecklePropertyGroupGoo();
|
||||
|
||||
// Validate for duplicate names
|
||||
var paramNames = Params.Input.Select(p => p.NickName).ToList();
|
||||
var duplicates = paramNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key);
|
||||
var duplicates = paramNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
|
||||
if (duplicates.Any())
|
||||
if (duplicates.Count != 0)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
@@ -77,11 +77,10 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
|
||||
|
||||
if (propertyValue != null)
|
||||
{
|
||||
properties[paramName] = propertyValue;
|
||||
groupGoo.SetValueByPath(paramName, propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
var groupGoo = new SpecklePropertyGroupGoo(properties);
|
||||
da.SetData(0, groupGoo);
|
||||
}
|
||||
|
||||
|
||||
+132
-59
@@ -40,57 +40,91 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
{
|
||||
// ALWAYS run port generation on the first iteration, BEFORE validating the current item
|
||||
// ensure that a null at index 0 doesn't prevent ports from being created.
|
||||
if (da.Iteration == 0)
|
||||
{
|
||||
// gather all property groups from the input (skipNulls = true)
|
||||
var allData = Params.Input[0].VolatileData.AllData(true).OfType<SpecklePropertyGroupGoo>().ToList();
|
||||
|
||||
// guard against empty data on file load / async operations to prevent stale ports from dropping (CNX-3245)
|
||||
if (allData.Count > 0)
|
||||
{
|
||||
var outputParamsDict = new Dictionary<string, OutputParamWrapper>();
|
||||
|
||||
foreach (var propGroup in allData)
|
||||
{
|
||||
if (propGroup?.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var key in propGroup.Value.Keys)
|
||||
{
|
||||
ISpecklePropertyGoo value = propGroup.Value[key];
|
||||
object? outputValue = value switch
|
||||
{
|
||||
SpecklePropertyGoo prop => prop.Value,
|
||||
SpecklePropertyGroupGoo pg => pg,
|
||||
_ => value
|
||||
};
|
||||
|
||||
if (!outputParamsDict.TryGetValue(key, out var existingWrapper))
|
||||
{
|
||||
var param = new SpeckleOutputParam
|
||||
{
|
||||
Name = key,
|
||||
NickName = key,
|
||||
Access = outputValue is IList ? GH_ParamAccess.list : GH_ParamAccess.item
|
||||
};
|
||||
outputParamsDict[key] = new OutputParamWrapper(param, outputValue);
|
||||
}
|
||||
else if (existingWrapper.Param.Access == GH_ParamAccess.item && outputValue is IList)
|
||||
{
|
||||
existingWrapper.Param.Access = GH_ParamAccess.list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var outputParams = outputParamsDict.Values.ToList();
|
||||
|
||||
Name = $"Properties ({outputParams.Count})";
|
||||
NickName = Name;
|
||||
|
||||
if (OutputMismatch(outputParams))
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(outputParams));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpecklePropertyGroupGoo? properties = null;
|
||||
if (!da.GetData(0, ref properties) || properties?.Value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Name = $"Properties ({properties.Value.Count})";
|
||||
NickName = Name;
|
||||
|
||||
var outputParams = new List<OutputParamWrapper>();
|
||||
|
||||
foreach (var key in properties.Value.Keys)
|
||||
for (int i = 0; i < Params.Output.Count; i++)
|
||||
{
|
||||
ISpecklePropertyGoo value = properties.Value[key];
|
||||
object? outputValue = value switch
|
||||
{
|
||||
SpecklePropertyGoo prop => prop.Value,
|
||||
SpecklePropertyGroupGoo propGroup => propGroup,
|
||||
_ => value
|
||||
};
|
||||
var outParam = Params.Output[i];
|
||||
|
||||
var param = new SpeckleOutputParam
|
||||
if (properties.Value.TryGetValue(outParam.Name, out ISpecklePropertyGoo? value))
|
||||
{
|
||||
Name = key,
|
||||
NickName = key,
|
||||
Access = outputValue is IList ? GH_ParamAccess.list : GH_ParamAccess.item
|
||||
};
|
||||
|
||||
outputParams.Add(new OutputParamWrapper(param, outputValue));
|
||||
}
|
||||
|
||||
// handle parameter creation/update (only on first iteration)
|
||||
if (da.Iteration == 0 && OutputMismatch(outputParams))
|
||||
{
|
||||
OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(outputParams));
|
||||
return; // exit early
|
||||
}
|
||||
// only set data if we have the correct parameter structure
|
||||
if (Params.Output.Count == outputParams.Count)
|
||||
{
|
||||
for (int i = 0; i < outputParams.Count; i++)
|
||||
{
|
||||
var outputParam = outputParams[i];
|
||||
switch (outputParam.Param.Access)
|
||||
object? outputValue = value switch
|
||||
{
|
||||
case GH_ParamAccess.item:
|
||||
da.SetData(i, outputParam.Value);
|
||||
break;
|
||||
case GH_ParamAccess.list:
|
||||
da.SetDataList(i, outputParam.Value as IList ?? new List<object?>());
|
||||
break;
|
||||
SpecklePropertyGoo prop => prop.Value,
|
||||
SpecklePropertyGroupGoo propGroup => propGroup,
|
||||
_ => value
|
||||
};
|
||||
|
||||
if (outParam.Access == GH_ParamAccess.item)
|
||||
{
|
||||
da.SetData(i, outputValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
da.SetDataList(i, outputValue as IList ?? new List<object?>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,29 +135,68 @@ public class ExpandSpeckleProperties : GH_Component, IGH_VariableParameterCompon
|
||||
/// </summary>
|
||||
private void CreateOutputs(List<OutputParamWrapper> outputParams)
|
||||
{
|
||||
// remove all existing output parameters
|
||||
while (Params.Output.Count > 0)
|
||||
{
|
||||
Params.UnregisterOutputParameter(Params.Output[^1]);
|
||||
}
|
||||
bool needsMaintenance = false;
|
||||
|
||||
// add new output parameters
|
||||
foreach (var newParam in outputParams)
|
||||
// remove old parameters that are no longer present
|
||||
for (int i = Params.Output.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var param = new SpeckleOutputParam
|
||||
var existingParam = Params.Output[i];
|
||||
if (outputParams.All(p => p.Param.Name != existingParam.Name))
|
||||
{
|
||||
Name = newParam.Param.Name,
|
||||
NickName = newParam.Param.NickName,
|
||||
MutableNickName = false,
|
||||
Access = newParam.Param.Access
|
||||
};
|
||||
Params.RegisterOutputParam(param);
|
||||
Params.UnregisterOutputParameter(existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
// notify gh of parameter changes
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
// add new parameters and update existing ones in place to preserve wires
|
||||
for (int i = 0; i < outputParams.Count; i++)
|
||||
{
|
||||
var targetParam = outputParams[i].Param;
|
||||
var existingParam = Params.Output.FirstOrDefault(p => p.Name == targetParam.Name);
|
||||
|
||||
if (existingParam != null)
|
||||
{
|
||||
if (existingParam.Access != targetParam.Access)
|
||||
{
|
||||
existingParam.Access = targetParam.Access;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
if (existingParam.NickName != targetParam.NickName)
|
||||
{
|
||||
existingParam.NickName = targetParam.NickName;
|
||||
needsMaintenance = true;
|
||||
}
|
||||
|
||||
int currentIndex = Params.Output.IndexOf(existingParam);
|
||||
if (currentIndex != i)
|
||||
{
|
||||
Params.Output.RemoveAt(currentIndex);
|
||||
Params.Output.Insert(i, existingParam);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newParam = new SpeckleOutputParam
|
||||
{
|
||||
Name = targetParam.Name,
|
||||
NickName = targetParam.NickName,
|
||||
MutableNickName = false,
|
||||
Access = targetParam.Access
|
||||
};
|
||||
Params.RegisterOutputParam(newParam, i);
|
||||
needsMaintenance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMaintenance)
|
||||
{
|
||||
// notify gh of parameter changes
|
||||
Params.OnParametersChanged();
|
||||
VariableParameterMaintenance();
|
||||
ExpireSolution(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+68
-20
@@ -37,19 +37,27 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
pManager.AddTextParameter(
|
||||
"Property Key",
|
||||
"P",
|
||||
"K",
|
||||
"Find objects with a property that has a matching key",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[2].Optional = true;
|
||||
|
||||
pManager.AddTextParameter(
|
||||
"Property Value",
|
||||
"V",
|
||||
"Find objects with a property that has a matching value",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[3].Optional = true;
|
||||
|
||||
pManager.AddTextParameter(
|
||||
"Material Name",
|
||||
"M",
|
||||
"Find objects with a render material that has a matching name",
|
||||
GH_ParamAccess.item
|
||||
);
|
||||
Params.Input[3].Optional = true;
|
||||
Params.Input[4].Optional = true;
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
|
||||
@@ -89,10 +97,12 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
string name = "";
|
||||
dataAccess.GetData(1, ref name);
|
||||
string property = "";
|
||||
dataAccess.GetData(2, ref property);
|
||||
string propertyKey = "";
|
||||
dataAccess.GetData(2, ref propertyKey);
|
||||
string propertyValue = "";
|
||||
dataAccess.GetData(3, ref propertyValue);
|
||||
string material = "";
|
||||
dataAccess.GetData(3, ref material);
|
||||
dataAccess.GetData(4, ref material);
|
||||
|
||||
// optional parameters - only read if they've been added via ⊕
|
||||
string appId = "";
|
||||
@@ -118,7 +128,19 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
|
||||
foreach (SpeckleWrapper wrapper in objects.Cast<SpeckleWrapper>())
|
||||
{
|
||||
if (MatchesAllFilters(wrapper, name, property, material, appId, filterByAppId, speckleId, filterBySpeckleId))
|
||||
if (
|
||||
MatchesAllFilters(
|
||||
wrapper,
|
||||
name,
|
||||
propertyKey,
|
||||
propertyValue,
|
||||
material,
|
||||
appId,
|
||||
filterByAppId,
|
||||
speckleId,
|
||||
filterBySpeckleId
|
||||
)
|
||||
)
|
||||
{
|
||||
matchedObjects.Add(wrapper);
|
||||
}
|
||||
@@ -149,7 +171,8 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
private bool MatchesAllFilters(
|
||||
SpeckleWrapper wrapper,
|
||||
string name,
|
||||
string property,
|
||||
string propertyKey,
|
||||
string propertyValue,
|
||||
string material,
|
||||
string appId,
|
||||
bool filterByAppId,
|
||||
@@ -164,7 +187,7 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
}
|
||||
|
||||
// filter by property
|
||||
if (!MatchesPropertyFilter(wrapper, property))
|
||||
if (!MatchesPropertyFilter(wrapper, propertyKey, propertyValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -190,9 +213,12 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MatchesPropertyFilter(SpeckleWrapper wrapper, string property)
|
||||
private bool MatchesPropertyFilter(SpeckleWrapper wrapper, string propertyKey, string propertyValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(property))
|
||||
bool hasKeyFilter = !string.IsNullOrEmpty(propertyKey);
|
||||
bool hasValueFilter = !string.IsNullOrEmpty(propertyValue);
|
||||
|
||||
if (!hasKeyFilter && !hasValueFilter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -208,8 +234,30 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return false;
|
||||
}
|
||||
|
||||
// use flattened properties to search ALL nested property keys
|
||||
return properties.Flatten().Keys.Any(key => MatchesSearchPattern(property, key));
|
||||
var flattenedProps = properties.Flatten();
|
||||
|
||||
// Check both property key and value simultaneously
|
||||
if (hasKeyFilter && hasValueFilter)
|
||||
{
|
||||
return flattenedProps.Any(kvp =>
|
||||
MatchesSearchPattern(propertyKey, kvp.Key)
|
||||
&& MatchesSearchPattern(propertyValue, kvp.Value.Value?.ToString() ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
// Check just property key
|
||||
if (hasKeyFilter)
|
||||
{
|
||||
return flattenedProps.Keys.Any(key => MatchesSearchPattern(propertyKey, key));
|
||||
}
|
||||
|
||||
// Check just property value
|
||||
if (hasValueFilter)
|
||||
{
|
||||
return flattenedProps.Values.Any(val => MatchesSearchPattern(propertyValue, val.Value?.ToString() ?? ""));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool MatchesMaterialFilter(SpeckleWrapper wrapper, string material)
|
||||
@@ -258,23 +306,23 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return false;
|
||||
}
|
||||
|
||||
// only allow inserting after the fixed parameters (index 4+)
|
||||
if (index < 4)
|
||||
// only allow inserting after the fixed parameters (index 5+)
|
||||
if (index < 5)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// check how many optional params are already added (total inputs - 4 fixed)
|
||||
int addedOptionalCount = Params.Input.Count - 4;
|
||||
// check how many optional params are already added (total inputs - 5 fixed)
|
||||
int addedOptionalCount = Params.Input.Count - 5;
|
||||
|
||||
// we have 2 optional parameters available
|
||||
return addedOptionalCount < 2;
|
||||
}
|
||||
|
||||
public bool CanRemoveParameter(GH_ParameterSide side, int index) =>
|
||||
// only allow removing optional input parameters (index 4+)
|
||||
// only allow removing optional input parameters (index 5+)
|
||||
side == GH_ParameterSide.Input
|
||||
&& index >= 4;
|
||||
&& index >= 5;
|
||||
|
||||
/// <remarks>
|
||||
/// The ternary operator for NickName is needed due to a Grasshopper quirk where
|
||||
@@ -316,12 +364,12 @@ public class FilterSpeckleObjects : GH_Component, IGH_VariableParameterComponent
|
||||
return new Param_String();
|
||||
}
|
||||
|
||||
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input && index >= 4;
|
||||
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input && index >= 5;
|
||||
|
||||
public void VariableParameterMaintenance()
|
||||
{
|
||||
// ensure all optional parameters stay marked as optional
|
||||
for (int i = 4; i < Params.Input.Count; i++)
|
||||
for (int i = 5; i < Params.Input.Count; i++)
|
||||
{
|
||||
Params.Input[i].Optional = true;
|
||||
}
|
||||
|
||||
+6
-6
@@ -24,14 +24,14 @@ public class PropertyGroupPathsSelector : ValueSet<IGH_Goo>
|
||||
|
||||
protected override void LoadVolatileData()
|
||||
{
|
||||
List<SpecklePropertyGroupGoo> propertyGroups = VolatileData
|
||||
.AllData(true)
|
||||
.OfType<SpecklePropertyGroupGoo>()
|
||||
.ToList();
|
||||
var allData = VolatileData.AllData(true).ToList();
|
||||
|
||||
if (VolatileDataCount > propertyGroups.Count)
|
||||
List<SpecklePropertyGroupGoo> propertyGroups = allData.OfType<SpecklePropertyGroupGoo>().ToList();
|
||||
|
||||
// compare against allData.Count to safely ignore nulls (CNX-3176)
|
||||
if (allData.Count > propertyGroups.Count)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Only Speckle Properties are accepted as inputs.");
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Only Speckle Properties are accepted as inputs.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+2
-31
@@ -35,10 +35,8 @@ public class QueryProperties : GH_Component
|
||||
pManager.AddTextParameter("Keys", "K", "Property keys to filter by", GH_ParamAccess.list);
|
||||
}
|
||||
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
|
||||
{
|
||||
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
|
||||
pManager.AddGenericParameter("Values", "V", "The values of the specified keys", GH_ParamAccess.list);
|
||||
}
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
{
|
||||
@@ -64,7 +62,7 @@ public class QueryProperties : GH_Component
|
||||
List<object?> values = [];
|
||||
foreach (string key in keys)
|
||||
{
|
||||
var value = GetValueByPath(properties, key);
|
||||
var value = properties.GetValueByPath(key);
|
||||
var extractedValue = (value as SpecklePropertyGoo)?.Value ?? value;
|
||||
|
||||
// NOTE: if property is a list, flatten into individual items for native gh list access
|
||||
@@ -80,31 +78,4 @@ public class QueryProperties : GH_Component
|
||||
|
||||
da.SetDataList(0, values);
|
||||
}
|
||||
|
||||
public static ISpecklePropertyGoo? GetValueByPath(SpecklePropertyGroupGoo data, string path)
|
||||
{
|
||||
string[] keys = path.Split('.');
|
||||
ISpecklePropertyGoo? current = data;
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (current is SpecklePropertyGroupGoo dict)
|
||||
{
|
||||
if (dict.Value.TryGetValue(key, out ISpecklePropertyGoo? next))
|
||||
{
|
||||
current = next;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return null; // Current is not a dictionary, path is invalid
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
+39
-32
@@ -30,17 +30,30 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
|
||||
private enum PropertyMode
|
||||
{
|
||||
Merge, // this should be default mode
|
||||
Merge,
|
||||
Replace,
|
||||
Remove
|
||||
Remove,
|
||||
Update, // pre rewording (cnx-3177), keeping for scripts with settings saved
|
||||
Overwrite // pre rewording (cnx-3177), keeping for scripts with settings saved
|
||||
}
|
||||
|
||||
private PropertyMode _mode = PropertyMode.Merge;
|
||||
private PropertyMode _mode = PropertyMode.Update;
|
||||
private PropertyMode Mode
|
||||
{
|
||||
get => _mode;
|
||||
set
|
||||
{
|
||||
// auto-migrate legacy modes to new modes
|
||||
if (value == PropertyMode.Update)
|
||||
{
|
||||
value = PropertyMode.Merge;
|
||||
}
|
||||
|
||||
if (value == PropertyMode.Overwrite)
|
||||
{
|
||||
value = PropertyMode.Replace;
|
||||
}
|
||||
|
||||
if (_mode != value)
|
||||
{
|
||||
_mode = value;
|
||||
@@ -96,7 +109,7 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
}
|
||||
|
||||
// validate that keys and values are of valid length
|
||||
if ((Mode == PropertyMode.Merge || Mode == PropertyMode.Replace) && inputKeys.Count != inputValues.Count)
|
||||
if ((Mode == PropertyMode.Update || Mode == PropertyMode.Overwrite) && inputKeys.Count != inputValues.Count)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Keys and values are mismatched in length");
|
||||
return;
|
||||
@@ -108,18 +121,16 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
return;
|
||||
}
|
||||
|
||||
// process the properties
|
||||
Dictionary<string, ISpecklePropertyGoo> result =
|
||||
inputProperties is null || Mode == PropertyMode.Replace
|
||||
? new()
|
||||
: inputProperties.Value.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||
// deep clone to prevent mutating upstream grasshopper data
|
||||
SpecklePropertyGroupGoo resultGoo =
|
||||
inputProperties is null || Mode == PropertyMode.Replace ? new SpecklePropertyGroupGoo() : inputProperties.Clone();
|
||||
|
||||
// process keys and values
|
||||
if (hasKeys)
|
||||
{
|
||||
// check for duplicate keys
|
||||
var duplicates = inputKeys.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key);
|
||||
if (duplicates.Any())
|
||||
var duplicates = inputKeys.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
if (duplicates.Count != 0)
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
@@ -128,12 +139,12 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
return;
|
||||
}
|
||||
|
||||
// set keyvalue pairs
|
||||
// set key-value pairs
|
||||
for (int i = 0; i < inputKeys.Count; i++)
|
||||
{
|
||||
string key = inputKeys[i];
|
||||
object? value = Mode == PropertyMode.Remove ? null : inputValues[i];
|
||||
ISpecklePropertyGoo? convertedValue = null;
|
||||
ISpecklePropertyGoo? convertedValue;
|
||||
switch (value)
|
||||
{
|
||||
case SpecklePropertyGroupGoo propGoo:
|
||||
@@ -148,7 +159,7 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
{
|
||||
AddRuntimeMessage(
|
||||
GH_RuntimeMessageLevel.Error,
|
||||
$"Values contain an invalid data type. Only strings, numbers, booleans, planes, vectors, intervals, and other Speckle properties are supported."
|
||||
"Values contain an invalid data type. Only strings, numbers, booleans, planes, vectors, intervals, and other Speckle properties are supported."
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -160,29 +171,19 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
switch (Mode)
|
||||
{
|
||||
case PropertyMode.Merge:
|
||||
if (result.ContainsKey(key))
|
||||
{
|
||||
result[key] = convertedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(key, convertedValue);
|
||||
}
|
||||
break;
|
||||
case PropertyMode.Replace:
|
||||
result.Add(key, convertedValue);
|
||||
resultGoo.SetValueByPath(key, convertedValue);
|
||||
break;
|
||||
case PropertyMode.Remove:
|
||||
result.Remove(key);
|
||||
resultGoo.RemoveValueByPath(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groupGoo = new SpecklePropertyGroupGoo(result);
|
||||
da.SetData(0, groupGoo);
|
||||
da.SetDataList(1, result.Keys);
|
||||
da.SetDataList(2, result.Values);
|
||||
da.SetData(0, resultGoo);
|
||||
da.SetDataList(1, resultGoo.Value.Keys);
|
||||
da.SetDataList(2, resultGoo.Value.Values);
|
||||
}
|
||||
|
||||
public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
|
||||
@@ -192,18 +193,24 @@ public class SpecklePropertiesPassthrough : SpeckleSolveInstance
|
||||
Menu_AppendSeparator(menu); // modes section
|
||||
foreach (PropertyMode mode in Enum.GetValues(typeof(PropertyMode)))
|
||||
{
|
||||
// hide "legacy modes" (before cnx-3177 rewording) from the dropdown
|
||||
if (mode is PropertyMode.Update or PropertyMode.Overwrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var modeItem = Menu_AppendItem(menu, mode.ToString(), (_, _) => Mode = mode, true, mode == Mode);
|
||||
switch (mode)
|
||||
{
|
||||
case PropertyMode.Merge:
|
||||
modeItem.ToolTipText =
|
||||
"Input keyvalue pairs will be merged with existing properties. Any existing keys will be updated with new values.";
|
||||
@"Input key-value pairs will be merged with existing properties. Any existing keys will be updated with new values.";
|
||||
break;
|
||||
case PropertyMode.Replace:
|
||||
modeItem.ToolTipText = "Existing properties will be cleared and replaced by input keyvalue pairs.";
|
||||
modeItem.ToolTipText = @"Existing properties will be cleared and replaced by input key-value pairs.";
|
||||
break;
|
||||
case PropertyMode.Remove:
|
||||
modeItem.ToolTipText = "Existing keyvalue pairs that match the input keys will be removed from properties.";
|
||||
modeItem.ToolTipText = @"Existing key-value pairs that match the input keys will be removed from properties.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-8
@@ -26,7 +26,8 @@ namespace Speckle.Connectors.GrasshopperShared.Operations.Receive;
|
||||
/// </remarks>
|
||||
internal sealed class LocalToGlobalMapHandler
|
||||
{
|
||||
public Dictionary<string, SpeckleGeometryWrapper> ConvertedObjectsMap { get; } = new();
|
||||
public Dictionary<string, SpeckleGeometryWrapper> ConvertedObjectsMap { get; } = [];
|
||||
private readonly HashSet<string> _processedDataObjects = [];
|
||||
|
||||
// injected via constructor (DI-managed)
|
||||
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
|
||||
@@ -113,7 +114,7 @@ internal sealed class LocalToGlobalMapHandler
|
||||
var obj = atomicContext.Current;
|
||||
var objId = obj.applicationId ?? obj.id;
|
||||
|
||||
if (objId is null || ConvertedObjectsMap.ContainsKey(objId))
|
||||
if (objId is null || ConvertedObjectsMap.ContainsKey(objId) || _processedDataObjects.Contains(objId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -132,6 +133,15 @@ internal sealed class LocalToGlobalMapHandler
|
||||
{
|
||||
List<(object, Base)> converted = SpeckleConversionContext.Current.ConvertToHost(obj);
|
||||
|
||||
// geometry-less data objects (cnx-2522)
|
||||
bool isMetadataOnly = obj is DataObject { displayValue.Count: 0 };
|
||||
|
||||
// bypass the early return if this genuinely is a metadata-only DataObject (cnx-3237)
|
||||
if (converted.Count == 0 && !isMetadataOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// get path and collection
|
||||
var path = _traversalContextUnpacker.GetCollectionPath(atomicContext).ToList();
|
||||
var objectCollection = CollectionRebuilder.GetOrCreateSpeckleCollectionFromPath(
|
||||
@@ -143,6 +153,8 @@ internal sealed class LocalToGlobalMapHandler
|
||||
// handle all DataObjects
|
||||
if (obj is DataObject normalDataObject)
|
||||
{
|
||||
_processedDataObjects.Add(objId);
|
||||
|
||||
var geometries = ConvertToGeometryWrappers(converted);
|
||||
var dataObjectWrapper = CreateDataObjectWrapper(normalDataObject, geometries, path, objectCollection);
|
||||
|
||||
@@ -150,12 +162,6 @@ internal sealed class LocalToGlobalMapHandler
|
||||
return;
|
||||
}
|
||||
|
||||
// nothing converted - nothing to do (for non-DataObjects)
|
||||
if (converted.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// handle normal geometry (not DataObject)
|
||||
SpecklePropertyGroupGoo propertyGroup = new();
|
||||
if (obj[Constants.PROPERTIES_PROP] is Dictionary<string, object?> props)
|
||||
@@ -214,6 +220,12 @@ internal sealed class LocalToGlobalMapHandler
|
||||
return;
|
||||
}
|
||||
|
||||
// ensures we don't process the same registered DataObject multiple times due to multiple traversal encounters.
|
||||
if (!_processedDataObjects.Add(dataObjectId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = _traversalContextUnpacker.GetCollectionPath(atomicContext).ToList();
|
||||
|
||||
+96
-1
@@ -10,7 +10,7 @@ namespace Speckle.Connectors.GrasshopperShared.Parameters;
|
||||
/// </summary>
|
||||
public partial class SpecklePropertyGroupGoo : GH_Goo<Dictionary<string, ISpecklePropertyGoo>>, ISpecklePropertyGoo
|
||||
{
|
||||
public override IGH_Goo Duplicate() => throw new NotImplementedException();
|
||||
public override IGH_Goo Duplicate() => Clone();
|
||||
|
||||
public override string ToString() => $"Speckle Properties : ({Value.Count})";
|
||||
|
||||
@@ -213,5 +213,100 @@ public partial class SpecklePropertyGroupGoo : GH_Goo<Dictionary<string, ISpeckl
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a deep clone of the property group to prevent mutating upstream Grasshopper data.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A new SpecklePropertyGroupGoo instance with cloned nested properties.
|
||||
/// Needed since adding support for dot notation [CNX-3179]
|
||||
/// </returns>
|
||||
public SpecklePropertyGroupGoo Clone()
|
||||
{
|
||||
var newDict = new Dictionary<string, ISpecklePropertyGoo>();
|
||||
foreach (var kvp in Value)
|
||||
{
|
||||
newDict[kvp.Key] = kvp.Value is SpecklePropertyGroupGoo group ? group.Clone() : kvp.Value;
|
||||
}
|
||||
return new SpecklePropertyGroupGoo(newDict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property value using dot-notation path traversal. Creates nested groups if they do not exist.
|
||||
/// </summary>
|
||||
/// <param name="path">The dot-notation property path.</param>
|
||||
/// <param name="value">The Speckle property to set.</param>
|
||||
public void SetValueByPath(string path, ISpecklePropertyGoo value)
|
||||
{
|
||||
string[] parts = path.Split('.');
|
||||
var current = Value;
|
||||
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
string part = parts[i];
|
||||
if (!current.TryGetValue(part, out var existing) || existing is not SpecklePropertyGroupGoo group)
|
||||
{
|
||||
group = new SpecklePropertyGroupGoo();
|
||||
current[part] = group;
|
||||
}
|
||||
current = group.Value;
|
||||
}
|
||||
|
||||
current[parts[^1]] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a property value using dot-notation path traversal.
|
||||
/// </summary>
|
||||
/// <param name="path">The dot-notation property path.</param>
|
||||
public void RemoveValueByPath(string path)
|
||||
{
|
||||
string[] parts = path.Split('.');
|
||||
var current = Value;
|
||||
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
string part = parts[i];
|
||||
if (!current.TryGetValue(part, out var existing) || existing is not SpecklePropertyGroupGoo group)
|
||||
{
|
||||
return; // path does not exist
|
||||
}
|
||||
|
||||
current = group.Value;
|
||||
}
|
||||
|
||||
current.Remove(parts[^1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a property value using dot-notation path traversal. Attempts exact match first.
|
||||
/// </summary>
|
||||
/// <param name="path">The dot-notation property path.</param>
|
||||
/// <returns>The matching property goo if found, otherwise null.</returns>
|
||||
public ISpecklePropertyGoo? GetValueByPath(string path)
|
||||
{
|
||||
// attempt exact match first for literal dots in native keys
|
||||
if (Value.TryGetValue(path, out var exactMatch))
|
||||
{
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
string[] parts = path.Split('.');
|
||||
ISpecklePropertyGoo? current = this;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (current is SpecklePropertyGroupGoo group && group.Value.TryGetValue(part, out var next))
|
||||
{
|
||||
current = next;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null; // current is not a dictionary, or path is invalid
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => base.GetHashCode();
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public sealed class DisplayValueExtractor
|
||||
case DB.Architecture.Room room:
|
||||
// api still returns geometry for unplaced rooms.
|
||||
// return empty list so room object is sent but with null display value
|
||||
if (room.Volume == 0)
|
||||
if (room.UpperLimit == null)
|
||||
{
|
||||
return new List<DisplayValueResult>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Speckle.Connectors.DUI.Bindings;
|
||||
|
||||
public interface IParametersBinding : IBinding
|
||||
{
|
||||
public Task Update(string payload);
|
||||
}
|
||||
Reference in New Issue
Block a user