feat (revit): receive Region as native FilledRegion (#696)

* regions with failed viewId

* render stuff in the first found suitable view

* use native or fallback conversion depending on the view

* better comments

* implement conditional conversion

* remove comment

* comment

* unload Root Host converter

* fix highlighting the model

* inject PlanView converter

* specify views in which receive is supported

* throw unsupported views in advance

* remove redundant check

* ViewManager added; View check is moved to the beginning of receive operation (to throw once and not for every object)

* simplify and remove unused

---------

Co-authored-by: Claire Kuang <kuang.claire@gmail.com>
This commit is contained in:
KatKatKateryna
2025-04-06 21:58:15 +01:00
committed by GitHub
parent 33e4008e4b
commit cf570342d2
8 changed files with 177 additions and 1 deletions
@@ -61,6 +61,7 @@ public static class ServiceRegistration
serviceCollection.AddScoped<ITransactionManager, TransactionManager>();
serviceCollection.AddScoped<RevitGroupBaker>();
serviceCollection.AddScoped<RevitMaterialBaker>();
serviceCollection.AddScoped<RevitViewManager>();
serviceCollection.AddSingleton<RevitUtils>();
serviceCollection.AddSingleton<IFailuresPreprocessor, HideWarningsFailuresPreprocessor>();
serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc());
@@ -0,0 +1,32 @@
using Autodesk.Revit.DB;
namespace Speckle.Connectors.Revit.HostApp;
/// <summary>
/// Handles Revit Views per Send/Receive, e.g. determines whether the View is supported for specific operation.
/// </summary>
public class RevitViewManager
{
/// <summary>
/// Determine if the View is supported for Receive operation. Currently only 3d view or horizontal 2d views are supported.
/// Views like Section, Elevation, ViewSheet etc. are not supported
/// </summary>
public bool IsSupportedReceiveView(View activeView)
{
switch (activeView.ViewType)
{
case ViewType.ThreeD:
case ViewType.FloorPlan:
case ViewType.AreaPlan:
case ViewType.CeilingPlan:
return true;
case ViewType.Detail:
return IsHorizontalView(activeView);
default:
return false;
}
}
private bool IsHorizontalView(View activeView) => Math.Abs(activeView.ViewDirection.Z - 1) < 0.00001;
}
@@ -33,6 +33,7 @@ public sealed class RevitHostObjectBuilder(
RevitGroupBaker groupManager,
RevitMaterialBaker materialBaker,
RootObjectUnpacker rootObjectUnpacker,
RevitViewManager viewManager,
ILogger<RevitHostObjectBuilder> logger,
IThreadContext threadContext,
RevitToHostCacheSingleton revitToHostCacheSingleton,
@@ -61,6 +62,13 @@ public sealed class RevitHostObjectBuilder(
CancellationToken cancellationToken
)
{
// ignore Receive in any other views (e.g. Section, Elevation, ViewSheet etc.)
View activeView = converterSettings.Current.Document.ActiveView;
if (!viewManager.IsSupportedReceiveView(activeView))
{
throw new ConversionException($"Receive in '{activeView.ViewType}' View is not supported");
}
var baseGroupName = $"Project {projectName}: Model {modelName}"; // TODO: unify this across connectors!
onOperationProgressed.Report(new("Converting", null));
@@ -201,6 +209,16 @@ public sealed class RevitHostObjectBuilder(
new(Status.SUCCESS, localToGlobalMap.AtomicObject, directShapes.UniqueId, "Direct Shape")
);
}
else if (result is List<string> elementsIds)
{
// This is the case when conversion returns not a GeometryObject, but Documentation elements (Annotations, Details etc.)
// If Regions were a part of DataObject, it can return more than 1 native shape
foreach (var elementId in elementsIds)
{
conversionResults.Add(new(Status.SUCCESS, localToGlobalMap.AtomicObject, elementId, "Filled Region"));
bakedObjectIds.Add(elementId);
}
}
else
{
throw new ConversionException($"Failed to cast {result.GetType()} to direct shape definition wrapper.");
@@ -25,6 +25,7 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\LinkedModelHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitMaterialBaker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\SupportedCategoriesUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\RevitViewManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\HideWarningsFailuresPreprocessor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\IdStorageSchema.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\IStorageSchema.cs" />
@@ -14,19 +14,31 @@ public class RevitRootToHostConverter : IRootToHostConverter
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<Base, List<DB.GeometryObject>> _baseToGeometryConverter;
private readonly ITypedConverter<Base, List<string>> _planViewToGeometryConverter;
public RevitRootToHostConverter(
ITypedConverter<Base, List<string>> planViewToGeometryConverter,
ITypedConverter<Base, List<DB.GeometryObject>> baseToGeometryConverter,
IConverterSettingsStore<RevitConversionSettings> converterSettings
)
{
_planViewToGeometryConverter = planViewToGeometryConverter;
_baseToGeometryConverter = baseToGeometryConverter;
_converterSettings = converterSettings;
}
public object Convert(Base target)
{
List<GeometryObject> geometryObjects = _baseToGeometryConverter.Convert(target);
// If ActiveView is a 2d view, use PlanView converter (will ignore DirectShapes)
// Unsupported views already filtered out in HostObjectBuilder
View activeView = _converterSettings.Current.Document.ActiveView;
if (activeView.ViewType != ViewType.ThreeD)
{
return _planViewToGeometryConverter.Convert(target);
}
// Use default behavior and covert everything to DirectShapes
List<DB.GeometryObject> geometryObjects = _baseToGeometryConverter.Convert(target);
if (geometryObjects.Count == 0)
{
@@ -31,6 +31,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Settings\ReferencePointType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Settings\RevitConversionSettingsFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\BaseToHostGeometryObjectConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\PlanViewToHostGeometryObjectConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\ArcConverterToHost.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\CircleConverterToHost.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\CurveConverterToHost.cs" />
@@ -42,6 +43,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\PlaneConverterToHost.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\PointConverterToHost.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\PolylineConverterToHost.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\RegionConverterToHost.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\Geometry\TransformConverterToHost.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\LocalToGlobalToDirectShapeConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToHost\Raw\RenderMaterialToHostConverter.cs" />
@@ -0,0 +1,63 @@
using Autodesk.Revit.DB;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Objects;
namespace Speckle.Converters.RevitShared.ToHost.TopLevel;
public class RegionConverterToHost : ITypedConverter<SOG.Region, string>
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly ITypedConverter<ICurve, DB.CurveArray> _curveConverter;
public RegionConverterToHost(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
ITypedConverter<ICurve, DB.CurveArray> curveConverter
)
{
_converterSettings = converterSettings;
_curveConverter = curveConverter;
}
public string Convert(SOG.Region target)
{
List<DB.GeometryObject> resultList = new();
List<CurveLoop> profileLoops = new();
// convert boundary loop and add to profileLoops list
CurveLoop boundaryLoop = new();
List<DB.Curve> outerLoop = _curveConverter.Convert(target.boundary).Cast<DB.Curve>().ToList();
outerLoop.ForEach(x => boundaryLoop.Append(x));
profileLoops.Add(boundaryLoop);
// convert inner loops and add to profileLoops list
List<List<DB.Curve>> innerLoops = target
.innerLoops.Select(x => _curveConverter.Convert(x).Cast<DB.Curve>().ToList())
.ToList();
foreach (var innerLoop in innerLoops)
{
CurveLoop voidLoop = new();
innerLoop.ForEach(x => voidLoop.Append(x));
profileLoops.Add(voidLoop);
}
// get FilledRegionType from the document to create a new FilledRegion element
using var filledRegionCollector = new FilteredElementCollector(_converterSettings.Current.Document);
Element filledRegionElementType = filledRegionCollector.OfClass(typeof(DB.FilledRegionType)).FirstElement();
// follow the pattern of the native CAD import: try to draw native FilledRegion in the Active view,
// or draw a linked CAD document, if imported into unsupported View (in our case: don't catch the error, so the converter will default to fallback)
View activeView = _converterSettings.Current.Document.ActiveView;
// Autodesk.Revit.Exceptions.ArgumentException will be thrown if ActiveView invalid
using FilledRegion filledRegion = FilledRegion.Create(
_converterSettings.Current.Document,
filledRegionElementType.Id,
activeView.Id,
profileLoops
);
return filledRegion.UniqueId;
}
}
@@ -0,0 +1,47 @@
using System.Collections;
using Speckle.Converters.Common.Objects;
using Speckle.Objects.Data;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Extensions;
namespace Speckle.Converters.RevitShared.ToSpeckle;
public class PlanViewToHostGeometryObjectConverter : ITypedConverter<Base, List<string>>
{
private readonly ITypedConverter<SOG.Region, string> _regionToFilledRegionConverter;
public PlanViewToHostGeometryObjectConverter(ITypedConverter<SOG.Region, string> regionToFilledRegionConverter)
{
_regionToFilledRegionConverter = regionToFilledRegionConverter;
}
public List<string> Convert(Base target)
{
switch (target)
{
case SOG.Region region:
return new List<string>() { _regionToFilledRegionConverter.Convert(region) };
case DataObject dataObj:
List<string> results = new();
var displayValue = target.TryGetDisplayValue<Base>();
if ((displayValue is IList && !displayValue.Any()) || displayValue is null)
{
throw new ValidationException($"No display value found for {target.speckle_type}");
}
dataObj.displayValue.ForEach(x => results.AddRange(Convert(x)));
if (results.Count == 0)
{
throw new ConversionException($"No objects could be converted for {target.speckle_type}.");
}
return results;
default:
throw new ConversionException($"Objects of type {target.speckle_type} cannot be converted in 2d view.");
}
}
}