Skip to content

Commit

Permalink
Feat(revit): view filter (#322)
Browse files Browse the repository at this point in the history
* POC view filter

* Use only view dropdown

* Init selection filters as default

* 2nd option for views

* Do not use WhereElementIsNotElementType

* Refresh send filters if elements modified

* Remove experimental view selection send filter

* chore: fixes expiration changes for view filters

* Remove everything filter

* Drop note for not using DI on deserialization

* Note about CheckFilterExpiration

* Fix cathastrophic failure on debugger

* Idle subscriptions on another event to fix main thread problems

* Implement APIContext

* APIContext in revit views filter

* Call GetObjectIds as async

* Remove CheckExpiry from everywhere

* Format

* Add ids to IdMap for newly added elements

* Await Commands.RefreshSendFilters

---------

Co-authored-by: Claire Kuang <[email protected]>
Co-authored-by: Dimitrie Stefanescu <[email protected]>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 0debe8f commit 9d25e61
Show file tree
Hide file tree
Showing 23 changed files with 422 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ private async Task RunExpirationChecks(bool idsDeleted)
{
var objIds = sender.SendFilter.NotNull().GetObjectIds();
var intersection = objIds.Intersect(objectIdsList).ToList();
bool isExpired = sender.SendFilter.NotNull().CheckExpiry(objectIdsList);
bool isExpired = intersection.Count != 0;
if (isExpired)
{
expiredSenderIds.Add(sender.ModelCardId.NotNull());
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ namespace Speckle.Connectors.ArcGIS.Filters;

public class ArcGISSelectionFilter : DirectSelectionSendFilter
{
public override List<string> GetObjectIds() => SelectedObjectIds;
public ArcGISSelectionFilter()
{
IsDefault = true;
}

public override bool CheckExpiry(string[] changedObjectIds) => SelectedObjectIds.Intersect(changedObjectIds).Any();
public override List<string> GetObjectIds() => SelectedObjectIds;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ namespace Speckle.Connectors.Autocad.Filters;

public class AutocadSelectionFilter : DirectSelectionSendFilter
{
public override List<string> GetObjectIds() => SelectedObjectIds;
public AutocadSelectionFilter()
{
IsDefault = true;
}

public override bool CheckExpiry(string[] changedObjectIds) => SelectedObjectIds.Intersect(changedObjectIds).Any();
public override List<string> GetObjectIds() => SelectedObjectIds;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"profiles": {
"ConnectorRevit2025": {
"commandName": "Executable",
"executablePath": "C:\\Program Files\\Autodesk\\Revit 2025\\Revit.exe"
"executablePath": "C:\\Program Files\\Autodesk\\Revit 2025\\Revit.exe",
"runtime": "net8.0-windows"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.DUI.Models.Card;
using Speckle.Connectors.Revit.HostApp;
using Speckle.Connectors.RevitShared;
using Speckle.Connectors.RevitShared.Operations.Send.Filters;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Sdk;
using Speckle.Sdk.Common;
Expand All @@ -19,12 +21,14 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding

public BasicConnectorBindingCommands Commands { get; }

private readonly APIContext _apiContext;
private readonly DocumentModelStore _store;
private readonly RevitContext _revitContext;
private readonly ISpeckleApplication _speckleApplication;
private readonly ILogger<BasicConnectorBindingRevit> _logger;

public BasicConnectorBindingRevit(
APIContext apiContext,
DocumentModelStore store,
IBrowserBridge parent,
RevitContext revitContext,
Expand All @@ -34,6 +38,7 @@ ILogger<BasicConnectorBindingRevit> logger
{
Name = "baseBinding";
Parent = parent;
_apiContext = apiContext;
_store = store;
_revitContext = revitContext;
_speckleApplication = speckleApplication;
Expand Down Expand Up @@ -101,9 +106,27 @@ public async Task HighlightModel(string modelCardId)

if (model is SenderModelCard senderModelCard)
{
elementIds = senderModelCard
.SendFilter.NotNull()
.GetObjectIds()
if (senderModelCard.SendFilter is RevitViewsFilter revitViewsFilter)
{
revitViewsFilter.SetContext(_revitContext, _apiContext);
await _apiContext
.Run(() =>
{
var view = revitViewsFilter.GetView();
if (view is not null)
{
_revitContext.UIApplication.ActiveUIDocument.ActiveView = view;
}
})
.ConfigureAwait(false);
return;
}

var selectedObjects = await _apiContext
.Run(_ => senderModelCard.SendFilter.NotNull().GetObjectIds())
.ConfigureAwait(false);

elementIds = selectedObjects
.Select(uid => ElementIdHelper.GetElementIdFromUniqueId(activeUIDoc.Document, uid))
.Where(el => el is not null)
.Cast<ElementId>()
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Speckle.Connectors.Revit.HostApp;
using Speckle.Connectors.Revit.Operations.Send.Settings;
using Speckle.Connectors.Revit.Plugin;
using Speckle.Connectors.RevitShared.Operations.Send.Filters;
using Speckle.Converters.Common;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Settings;
Expand All @@ -27,6 +28,7 @@ namespace Speckle.Connectors.Revit.Bindings;
internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
{
private readonly IRevitIdleManager _idleManager;
private readonly APIContext _apiContext;
private readonly CancellationManager _cancellationManager;
private readonly IServiceProvider _serviceProvider;
private readonly ISendConversionCache _sendConversionCache;
Expand All @@ -43,7 +45,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
/// As to why a concurrent dictionary, it's because it's the cheapest/easiest way to do so.
/// https://stackoverflow.com/questions/18922985/concurrent-hashsett-in-net-framework
/// </summary>
private ConcurrentDictionary<string, byte> ChangedObjectIds { get; set; } = new();
private ConcurrentDictionary<ElementId, byte> ChangedObjectIds { get; set; } = new();

/// <summary>
/// We need it to get UniqueId whenever it is not available i.e. GetDeletedElementIds returns ElementId and cannot find its Element to get UniqueId. We store them both just before send to remember later.
Expand All @@ -53,6 +55,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
public RevitSendBinding(
IRevitIdleManager idleManager,
RevitContext revitContext,
APIContext apiContext,
DocumentModelStore store,
CancellationManager cancellationManager,
IBrowserBridge bridge,
Expand All @@ -68,6 +71,7 @@ ISpeckleApplication speckleApplication
: base("sendBinding", store, bridge, revitContext)
{
_idleManager = idleManager;
_apiContext = apiContext;
_cancellationManager = cancellationManager;
_serviceProvider = serviceProvider;
_sendConversionCache = sendConversionCache;
Expand All @@ -91,10 +95,8 @@ ISpeckleApplication speckleApplication
topLevelExceptionHandler.FireAndForget(async () => await OnDocumentChanged().ConfigureAwait(false));
}

public List<ISendFilter> GetSendFilters()
{
return new List<ISendFilter> { new RevitSelectionFilter() { IsDefault = true } };
}
public List<ISendFilter> GetSendFilters() =>
[new RevitSelectionFilter() { IsDefault = true }, new RevitViewsFilter(RevitContext, _apiContext)];

public List<ICardSetting> GetSendSettings() =>
[
Expand All @@ -107,7 +109,9 @@ public List<ICardSetting> GetSendSettings() =>

public SendBindingUICommands Commands { get; }

#pragma warning disable CA1506
public async Task Send(string modelCardId)
#pragma warning restore CA1506
{
// Note: removed top level handling thing as it was confusing me
try
Expand All @@ -125,19 +129,26 @@ public async Task Send(string modelCardId)
.ServiceProvider.GetRequiredService<IConverterSettingsStore<RevitConversionSettings>>()
.Initialize(
_revitConversionSettingsFactory.Create(
_toSpeckleSettingsManager.GetDetailLevelSetting(modelCard),
_toSpeckleSettingsManager.GetReferencePointSetting(modelCard),
_toSpeckleSettingsManager.GetSendParameterNullOrEmptyStringsSetting(modelCard)
await _toSpeckleSettingsManager.GetDetailLevelSetting(modelCard).ConfigureAwait(false),
await _toSpeckleSettingsManager.GetReferencePointSetting(modelCard).ConfigureAwait(false),
await _toSpeckleSettingsManager.GetSendParameterNullOrEmptyStringsSetting(modelCard).ConfigureAwait(false)
)
);

var activeUIDoc =
RevitContext.UIApplication?.ActiveUIDocument
?? throw new SpeckleException("Unable to retrieve active UI document");

List<Element> elements = modelCard
.SendFilter.NotNull()
.GetObjectIds()
if (modelCard.SendFilter is RevitViewsFilter viewFilter)
{
viewFilter.SetContext(RevitContext, _apiContext);
}

var selectedObjects = await _apiContext
.Run(_ => modelCard.SendFilter.NotNull().GetObjectIds())
.ConfigureAwait(false);

List<Element> elements = selectedObjects
.Select(uid => activeUIDoc.Document.GetElement(uid))
.Where(el => el is not null)
.ToList();
Expand Down Expand Up @@ -187,34 +198,47 @@ await Commands
/// a filter refresh (e.g., views being added).
/// </summary>
/// <param name="e"></param>
private void DocChangeHandler(Autodesk.Revit.DB.Events.DocumentChangedEventArgs e)
private async Task DocChangeHandler(Autodesk.Revit.DB.Events.DocumentChangedEventArgs e)
{
ICollection<ElementId> addedElementIds = e.GetAddedElementIds();
ICollection<ElementId> deletedElementIds = e.GetDeletedElementIds();
ICollection<ElementId> modifiedElementIds = e.GetModifiedElementIds();

foreach (ElementId elementId in addedElementIds)
{
ChangedObjectIds[elementId.ToString()] = 1;
ChangedObjectIds[elementId] = 1;
}

foreach (ElementId elementId in deletedElementIds)
{
ChangedObjectIds[elementId.ToString()] = 1;
ChangedObjectIds[elementId] = 1;
}

foreach (ElementId elementId in modifiedElementIds)
{
ChangedObjectIds[elementId.ToString()] = 1;
ChangedObjectIds[elementId] = 1;
}

if (HaveUnitsChanged(e.GetDocument()))
{
var objectIds = Store.GetSenders().SelectMany(s => s.SendFilter != null ? s.SendFilter.GetObjectIds() : []);
var objectIds = new List<string>();
foreach (var sender in Store.GetSenders())
{
if (sender.SendFilter is null)
{
continue;
}
var selectedObjects = await _apiContext
.Run(_ => sender.SendFilter.NotNull().GetObjectIds())
.ConfigureAwait(false);
objectIds.AddRange(selectedObjects);
}
var unpackedObjectIds = _elementUnpacker.GetUnpackedElementIds(objectIds.ToList());
_sendConversionCache.EvictObjects(unpackedObjectIds);
}
_idleManager.SubscribeToIdle(nameof(RevitSendBinding), RunExpirationChecks);

_idleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
_idleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
}

// Keeps track of doc and current units
Expand Down Expand Up @@ -251,6 +275,26 @@ private bool HaveUnitsChanged(Document doc)
return false;
}

/// <summary>
/// Notifies ui if any filters need refreshing. Currently, this only applies for view filters.
/// </summary>
private async Task CheckFilterExpiration()
{
// NOTE: below code seems like more make sense in terms of performance but it causes unmanaged exception on Revit
// using var viewCollector = new FilteredElementCollector(RevitContext.UIApplication?.ActiveUIDocument.Document);
// var views = viewCollector.OfClass(typeof(View)).Cast<View>().Select(v => v.Id).ToList();
// var intersection = ChangedObjectIds.Keys.Intersect(views).ToList();
// if (intersection.Count != 0)
// {
// await Commands.RefreshSendFilters().ConfigureAwait(false);
// }

if (ChangedObjectIds.Keys.Any(e => RevitContext.UIApplication?.ActiveUIDocument.Document.GetElement(e) is View))
{
await Commands.RefreshSendFilters().ConfigureAwait(false);
}
}

private async Task RunExpirationChecks()
{
var senders = Store.GetSenders();
Expand All @@ -263,12 +307,18 @@ private async Task RunExpirationChecks()
}

var objUniqueIds = new List<string>();
foreach (string changedElementId in ChangedObjectIds.Keys.ToArray())
foreach (var changedElementId in ChangedObjectIds.Keys.ToArray())
{
if (IdMap.TryGetValue(changedElementId, out var uniqueId))
if (IdMap.TryGetValue(changedElementId.ToString(), out var uniqueId))
{
objUniqueIds.Add(uniqueId);
}
else
{
var uniqId = doc.GetElement(changedElementId).UniqueId;
objUniqueIds.Add(uniqId);
IdMap[changedElementId.ToString()] = uniqId;
}
}

var unpackedObjectIds = _elementUnpacker.GetUnpackedElementIds(objUniqueIds);
Expand All @@ -278,7 +328,14 @@ private async Task RunExpirationChecks()
List<string> expiredSenderIds = new();
foreach (SenderModelCard modelCard in senders)
{
var intersection = modelCard.SendFilter.NotNull().GetObjectIds().Intersect(objUniqueIds).ToList();
if (modelCard.SendFilter is RevitViewsFilter viewFilter)
{
viewFilter.SetContext(RevitContext, _apiContext);
}
var selectedObjects = await _apiContext
.Run(_ => modelCard.SendFilter.NotNull().GetObjectIds())
.ConfigureAwait(false);
var intersection = selectedObjects.Intersect(objUniqueIds).ToList();
bool isExpired = intersection.Count != 0;
if (isExpired)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ public static void AddRevit(this IServiceCollection serviceCollection)

// operation progress manager
serviceCollection.AddSingleton<IOperationProgressManager, OperationProgressManager>();

// API context helps us to run functions on Revit UI Thread (main)
serviceCollection.AddSingleton<APIContext>();
}

public static void RegisterUiDependencies(IServiceCollection serviceCollection)
Expand Down
Loading

0 comments on commit 9d25e61

Please sign in to comment.