using Microsoft.Extensions.Logging; using Speckle.Converters.Common; using Speckle.Converters.Common.Objects; using Speckle.Converters.RevitShared.Extensions; using Speckle.Converters.RevitShared.Settings; using Speckle.Objects; using Speckle.Sdk; using Speckle.Sdk.Common; using Speckle.Sdk.Models; namespace Speckle.Converters.RevitShared.Helpers; // POC: needs breaking down https://spockle.atlassian.net/browse/CNX-9354 public sealed class DisplayValueExtractor { private readonly ITypedConverter< (Dictionary> target, DB.ElementId parentElementId, bool makeTransparent), List > _meshByMaterialConverter; private readonly ITypedConverter _curveConverter; private readonly ITypedConverter _polylineConverter; private readonly ITypedConverter _pointConverter; private readonly ITypedConverter _pointcloudConverter; private readonly ILogger _logger; private readonly IConverterSettingsStore _converterSettings; public DisplayValueExtractor( ITypedConverter< (Dictionary> target, DB.ElementId parentElementId, bool makeTransparent), List > meshByMaterialConverter, ITypedConverter curveConverter, ITypedConverter polylineConverter, ITypedConverter pointConverter, ITypedConverter pointcloudConverter, ILogger logger, IConverterSettingsStore converterSettings ) { _meshByMaterialConverter = meshByMaterialConverter; _curveConverter = curveConverter; _polylineConverter = polylineConverter; _pointConverter = pointConverter; _pointcloudConverter = pointcloudConverter; _logger = logger; _converterSettings = converterSettings; } public List GetDisplayValue(DB.Element element) { switch (element) { // get custom (anything not using element.get_geometry) display values case DB.PointCloudInstance pointcloud: return new() { _pointcloudConverter.Convert(pointcloud) }; case DB.ModelCurve modelCurve: return new() { GetCurveDisplayValue(modelCurve.GeometryCurve) }; case DB.Grid grid: return new() { GetCurveDisplayValue(grid.Curve) }; case DB.Area area: List areaDisplay = new(); using (var options = new DB.SpatialElementBoundaryOptions()) { foreach (IList boundarySegmentGroup in area.GetBoundarySegments(options)) { foreach (DB.BoundarySegment boundarySegment in boundarySegmentGroup) { areaDisplay.Add(GetCurveDisplayValue(boundarySegment.GetCurve())); } } } return areaDisplay; // NOTE: this is only for Rebar and not AreaReinforcement, RebarInSystem // AreaReinforcement and RebarInSystem pass through GetGeometryDisplayValue which get DisplayValues as per hostApp // Rebar elements need special handling as get_Geometry() doesn't work properly // We either represent them as centerlines or as solids based on settings case DB.Structure.Rebar rebar: return _converterSettings.Current.SendRebarsAsVolumetric ? GetRebarVolumetricDisplayValue(rebar) : GetRebarCenterlineDisplayValue(rebar); // handle specific types of objects with multiple parts or children // curtain and stacked walls should have their display values in their children case DB.Wall wall: return wall.CurtainGrid is not null || wall.IsStackedWall ? new() : GetGeometryDisplayValue(element); // railings should also include toprail which need to be retrieved separately case DBA.Railing railing: List railingDisplay = GetGeometryDisplayValue(railing); if (railing.TopRail != DB.ElementId.InvalidElementId) { var topRail = _converterSettings.Current.Document.GetElement(railing.TopRail); railingDisplay.AddRange(GetGeometryDisplayValue(topRail)); } return railingDisplay; // POC: footprint roofs can have curtain walls in them. Need to check if they can also have non-curtain wall parts, bc currently not skipping anything. // case DB.FootPrintRoof footPrintRoof: default: return GetGeometryDisplayValue(element); } } private Base GetCurveDisplayValue(DB.Curve curve) => (Base)_curveConverter.Convert(curve); private List GetGeometryDisplayValue(DB.Element element, DB.Options? options = null) { var collections = GetSortedGeometryFromElement(element, options); return ProcessGeometryCollections(element, collections); } /// /// Extracts and sorts all geometry from an element into separate collections by type. /// /// /// Extraction of geometry from any element using get_Geometry(). /// Note: Some special element types (like Rebar) cannot use this method as their /// get_Geometry() returns null, requiring specialized extraction methods. /// private GeometryCollections GetSortedGeometryFromElement(DB.Element element, DB.Options? options) { //options = ViewSpecificOptions ?? options ?? new Options() { DetailLevel = DetailLevelSetting }; options ??= new DB.Options { DetailLevel = _detailLevelMap[_converterSettings.Current.DetailLevel] }; options = OverrideViewOptions(element, options); DB.GeometryElement geom; try { geom = element.get_Geometry(options); } // POC: should we be trying to continue? catch (Autodesk.Revit.Exceptions.ArgumentException) { options.ComputeReferences = false; geom = element.get_Geometry(options); } var collections = new GeometryCollections(); if (geom != null && geom.Any()) { // retrieves all meshes and solids from a geometry element SortGeometry(element, collections, geom); } return collections; } /// /// Processes collections of different geometry types and converts them to display values. /// Extracted as a common method to reduce code duplication between regular geometry processing and special cases like rebar. /// /// /// Essentially all the ensuing steps after the common get_Geometry element method /// private List ProcessGeometryCollections(DB.Element element, GeometryCollections collections) { List displayValue = new(); // handle all solids and meshes by their material var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids); List displayMeshes = _meshByMaterialConverter.Convert( (meshesByMaterial, element.Id, ShouldSetElementDisplayToTransparent(element)) ); displayValue.AddRange(displayMeshes); // add rest of geometry foreach (var curve in collections.Curves) { displayValue.Add(GetCurveDisplayValue(curve)); } foreach (var polyline in collections.Polylines) { displayValue.Add(_polylineConverter.Convert(polyline)); } foreach (var point in collections.Points) { displayValue.Add(_pointConverter.Convert(point)); } return displayValue; } private static Dictionary> GetMeshesByMaterial( List meshes, List solids ) { var meshesByMaterial = new Dictionary>(); foreach (var mesh in meshes) { var materialId = mesh.MaterialElementId; if (!meshesByMaterial.TryGetValue(materialId, out List? value)) { value = new List(); meshesByMaterial[materialId] = value; } value.Add(mesh); } foreach (var solid in solids) { foreach (DB.Face face in solid.Faces) { var materialId = face.MaterialElementId; if (!meshesByMaterial.TryGetValue(materialId, out List? value)) { value = new List(); meshesByMaterial[materialId] = value; } var mesh = face.Triangulate(); //Revit API can return null here if (mesh is null) { continue; } value.Add(mesh); } } return meshesByMaterial; } // We do not handle DetailLevelType.Undefined behavior, so we don't use 'DB.ViewDetailLevel' enum directly as option in UI. private readonly Dictionary _detailLevelMap = new() { { DetailLevelType.Coarse, DB.ViewDetailLevel.Coarse }, { DetailLevelType.Medium, DB.ViewDetailLevel.Medium }, { DetailLevelType.Fine, DB.ViewDetailLevel.Fine } }; /// /// According to the remarks on the GeometryInstance class in the RevitAPIDocs, /// https://www.revitapidocs.com/2024/fe25b14f-5866-ca0f-a660-c157484c3a56.htm, /// a family instance geometryElement should have a top-level geometry instance when the symbol /// does not have modified geometry (the docs say that modified geometry will not have a geom instance, /// however in my experience, all family instances have a top-level geom instance, but if the family instance /// is modified, then the geom instance won't contain any geometry.) /// /// This remark also leads me to think that a family instance will not have top-level solids and geom instances. /// We are logging cases where this is not true. /// /// Note: this is basically a geometry unpacker for all types of geometry /// private void SortGeometry(DB.Element element, GeometryCollections collections, DB.GeometryElement geom) { foreach (DB.GeometryObject geomObj in geom) { if (SkipGeometry(geomObj, element)) { continue; } switch (geomObj) { case DB.Solid solid: // skip invalid solid if (solid.Faces.Size == 0) { continue; } collections.Solids.Add(solid); break; case DB.Mesh mesh: collections.Meshes.Add(mesh); break; case DB.Curve curve: collections.Curves.Add(curve); break; case DB.PolyLine polyline: collections.Polylines.Add(polyline); break; case DB.Point point: collections.Points.Add(point); break; case DB.GeometryInstance instance: // element transforms should not be carried down into nested geometryInstances. // Nested geomInstances should have their geom retreived with GetInstanceGeom, not GetSymbolGeom SortGeometry(element, collections, instance.GetInstanceGeometry()); break; case DB.GeometryElement geometryElement: SortGeometry(element, collections, geometryElement); break; } } } /// /// We're caching a dictionary of graphic styles and their ids as it can be a costly operation doing Document.GetElement(solid.GraphicsStyleId) for every solid /// private readonly Dictionary _graphicStyleCache = new(); private bool SkipGeometry(DB.GeometryObject geomObj, DB.Element element) { if (geomObj.GraphicsStyleId == DB.ElementId.InvalidElementId) { return false; // exit fast on a potential hot path } DB.GraphicsStyle? bjk = null; // ask ogu why this variable is named like this if (!_graphicStyleCache.ContainsKey(geomObj.GraphicsStyleId.ToString().NotNull())) { bjk = (DB.GraphicsStyle)element.Document.GetElement(geomObj.GraphicsStyleId); _graphicStyleCache[geomObj.GraphicsStyleId.ToString().NotNull()] = bjk; } else { bjk = _graphicStyleCache[geomObj.GraphicsStyleId.ToString().NotNull()]; } #if REVIT2023_OR_GREATER if (bjk?.GraphicsStyleCategory.BuiltInCategory == DB.BuiltInCategory.OST_LightingFixtureSource) { return true; } #else if (bjk?.GraphicsStyleCategory.Id.IntegerValue == (int)DB.BuiltInCategory.OST_LightingFixtureSource) { return true; } #endif return false; } // Determines if an element should be sent with invisible display values private bool ShouldSetElementDisplayToTransparent(DB.Element element) { #if REVIT2023_OR_GREATER switch (element.Category?.BuiltInCategory) { case DB.BuiltInCategory.OST_Rooms: return true; default: return false; } #else return false; #endif } /// /// Overrides current view options to extract meaningful geometry for various elements. E.g., pipes, plumbing fixtures, steel elements /// /// /// private DB.Options OverrideViewOptions(DB.Element element, DB.Options currentOptions) { // there is no point to progress if element category already null if (element.Category is null) { return currentOptions; } var elementBuiltInCategory = element.Category.GetBuiltInCategory(); // Note: some elements do not get display values (you get invalid solids) unless we force the view detail level to be fine. This is annoying, but it's bad ux: people think the // elements are not there (they are, just invisible). if ( elementBuiltInCategory == DB.BuiltInCategory.OST_PipeFitting || elementBuiltInCategory == DB.BuiltInCategory.OST_PipeAccessory || elementBuiltInCategory == DB.BuiltInCategory.OST_PlumbingFixtures #if REVIT2024_OR_GREATER || element is DB.Toposolid // note, brought back from 2.x.x. #endif ) { currentOptions.DetailLevel = DB.ViewDetailLevel.Fine; // Force detail level to be fine return currentOptions; } // NOTE: On steel elements. This is an incomplete solution. // If steel element proxies will be sucked in via category selection, and they are not visible in the current view, they will not be extracted out. // I'm inclined to go with this as a semi-permanent limitation. See: // https://speckle.community/t/revit-2025-2-missing-elements-and-colors/14073 // and https://forums.autodesk.com/t5/revit-api-forum/how-to-get-steelproxyelement-geometry/td-p/10347898 if ( elementBuiltInCategory is DB.BuiltInCategory.OST_StructConnections or DB.BuiltInCategory.OST_StructConnectionPlates or DB.BuiltInCategory.OST_StructuralFraming or DB.BuiltInCategory.OST_StructuralColumns or DB.BuiltInCategory.OST_StructConnectionBolts or DB.BuiltInCategory.OST_StructConnectionWelds or DB.BuiltInCategory.OST_StructConnectionShearStuds ) { // try-catch is not pretty. we need to understand this better. try { // try to create options with the active view - this will work for the main document and will fail with the linked models. Well, we can safely swallow the exception since we do not care DB.Options for linked models. return new DB.Options() { View = _converterSettings.Current.Document.NotNull().ActiveView }; } catch (Exception ex) when (!ex.IsFatal()) { // if that fails (which will happen for linked documents), use the current options return currentOptions; } } return currentOptions; } /// /// Gets the solid representation of rebar elements. /// /// /// Rebars require special handling since the standard get_Geometry() method returns null. /// Instead, we use GetFullGeometryForView() to obtain the geometry and then process it /// using the standard geometry sorting and conversion. /// private List GetRebarVolumetricDisplayValue(DB.Structure.Rebar rebar) { var collections = new GeometryCollections(); // Regular get_Geometry() returns null for rebar, so we need to use GetFullGeometryForView // ❗NOTE: ️view detail level needs to be fine in order for this to work // Same behaviour as sending structural frame though - consistent and therefore okay. DB.GeometryElement geometryElements = rebar.GetFullGeometryForView(_converterSettings.Current.Document.ActiveView); SortGeometry(rebar, collections, geometryElements); if (geometryElements != null) { SortGeometry(rebar, collections, geometryElements); return ProcessGeometryCollections(rebar, collections); } // Return empty list if no geometry is found - imo not critical return new List(); } /// /// Gets the centerline representation of a rebar element. /// /// /// This method extracts the centerlines of rebar elements when a simplified representation is preferred. /// private List GetRebarCenterlineDisplayValue(DB.Structure.Rebar rebar) { bool isSingleLayout = rebar.LayoutRule == DB.Structure.RebarLayoutRule.Single; int numberOfBarPositions = rebar.NumberOfBarPositions; List curves = new(); for (int barPositionIndex = 0; barPositionIndex < numberOfBarPositions; barPositionIndex++) { if (!isSingleLayout) { if ( !rebar.IncludeFirstBar && barPositionIndex == 0 || !rebar.IncludeLastBar && barPositionIndex == rebar.NumberOfBarPositions - 1 ) { continue; } } curves.AddRange( rebar.GetTransformedCenterlineCurves( false, false, false, DB.Structure.MultiplanarOption.IncludeAllMultiplanarCurves, barPositionIndex ) ); } List displayValue = new(); foreach (var curve in curves) { displayValue.Add(GetCurveDisplayValue(curve)); } return displayValue; } /// /// Represents sorted collections of different geometry types extracted from an element. /// Used to pass multiple geometry collections as a single parameter to improve code readability /// and reduce the risk of parameter ordering errors. /// private sealed record GeometryCollections { public List Solids { get; } = new(); public List Meshes { get; } = new(); public List Curves { get; } = new(); public List Polylines { get; } = new(); public List Points { get; } = new(); } }