diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs index 47534dca8..aef43aa2a 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs @@ -61,6 +61,7 @@ public static class ServiceRegistration serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc()); diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitViewManager.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitViewManager.cs new file mode 100644 index 000000000..f44bc2cae --- /dev/null +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitViewManager.cs @@ -0,0 +1,32 @@ +using Autodesk.Revit.DB; + +namespace Speckle.Connectors.Revit.HostApp; + +/// +/// Handles Revit Views per Send/Receive, e.g. determines whether the View is supported for specific operation. +/// +public class RevitViewManager +{ + /// + /// 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 + /// + 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; +} diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs index ae7e839c5..288ac8b8c 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs @@ -33,6 +33,7 @@ public sealed class RevitHostObjectBuilder( RevitGroupBaker groupManager, RevitMaterialBaker materialBaker, RootObjectUnpacker rootObjectUnpacker, + RevitViewManager viewManager, ILogger 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 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."); diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems b/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems index 9a11ccdd9..5500e8bb2 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems @@ -25,6 +25,7 @@ + diff --git a/Converters/Revit/Speckle.Converters.RevitShared/RevitRootToHostConverter.cs b/Converters/Revit/Speckle.Converters.RevitShared/RevitRootToHostConverter.cs index 96c5e9093..9ca6b0c16 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/RevitRootToHostConverter.cs +++ b/Converters/Revit/Speckle.Converters.RevitShared/RevitRootToHostConverter.cs @@ -14,19 +14,31 @@ public class RevitRootToHostConverter : IRootToHostConverter { private readonly IConverterSettingsStore _converterSettings; private readonly ITypedConverter> _baseToGeometryConverter; + private readonly ITypedConverter> _planViewToGeometryConverter; public RevitRootToHostConverter( + ITypedConverter> planViewToGeometryConverter, ITypedConverter> baseToGeometryConverter, IConverterSettingsStore converterSettings ) { + _planViewToGeometryConverter = planViewToGeometryConverter; _baseToGeometryConverter = baseToGeometryConverter; _converterSettings = converterSettings; } public object Convert(Base target) { - List 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 geometryObjects = _baseToGeometryConverter.Convert(target); if (geometryObjects.Count == 0) { diff --git a/Converters/Revit/Speckle.Converters.RevitShared/Speckle.Converters.RevitShared.projitems b/Converters/Revit/Speckle.Converters.RevitShared/Speckle.Converters.RevitShared.projitems index 1a6ad540a..eef05a2ea 100644 --- a/Converters/Revit/Speckle.Converters.RevitShared/Speckle.Converters.RevitShared.projitems +++ b/Converters/Revit/Speckle.Converters.RevitShared/Speckle.Converters.RevitShared.projitems @@ -31,6 +31,7 @@ + @@ -42,6 +43,7 @@ + diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/Geometry/RegionConverterToHost.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/Geometry/RegionConverterToHost.cs new file mode 100644 index 000000000..da2098c05 --- /dev/null +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/Geometry/RegionConverterToHost.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 +{ + private readonly IConverterSettingsStore _converterSettings; + private readonly ITypedConverter _curveConverter; + + public RegionConverterToHost( + IConverterSettingsStore converterSettings, + ITypedConverter curveConverter + ) + { + _converterSettings = converterSettings; + _curveConverter = curveConverter; + } + + public string Convert(SOG.Region target) + { + List resultList = new(); + List profileLoops = new(); + + // convert boundary loop and add to profileLoops list + CurveLoop boundaryLoop = new(); + List outerLoop = _curveConverter.Convert(target.boundary).Cast().ToList(); + outerLoop.ForEach(x => boundaryLoop.Append(x)); + profileLoops.Add(boundaryLoop); + + // convert inner loops and add to profileLoops list + List> innerLoops = target + .innerLoops.Select(x => _curveConverter.Convert(x).Cast().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; + } +} diff --git a/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/PlanViewToHostGeometryObjectConverter.cs b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/PlanViewToHostGeometryObjectConverter.cs new file mode 100644 index 000000000..79e15dfdc --- /dev/null +++ b/Converters/Revit/Speckle.Converters.RevitShared/ToHost/Raw/PlanViewToHostGeometryObjectConverter.cs @@ -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> +{ + private readonly ITypedConverter _regionToFilledRegionConverter; + + public PlanViewToHostGeometryObjectConverter(ITypedConverter regionToFilledRegionConverter) + { + _regionToFilledRegionConverter = regionToFilledRegionConverter; + } + + public List Convert(Base target) + { + switch (target) + { + case SOG.Region region: + return new List() { _regionToFilledRegionConverter.Convert(region) }; + + case DataObject dataObj: + List results = new(); + + var displayValue = target.TryGetDisplayValue(); + 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."); + } + } +}