Files
speckle-sharp-connectors/Converters/Revit/Speckle.Converters.RevitShared/Helpers/DisplayValueExtractor.cs
T
Björn Steinhagen 3b6623e51a feat(revit): rebar displayValue options on send (#821)
* feat: add send rebars as solid toggle

* feat: rebar `displayValue` default of centrelines

* feat: sending rebars as solid poc

- need to refactor to avoid duplicate code
- is this the best way? what if user view isn't fine?

* refactor: extract common code and code comments

* refactor: reduce code duplication in DisplayValueExtractor with record

* refactor: wording volumetric not solid
2025-05-12 20:16:23 +02:00

506 lines
18 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<DB.ElementId, List<DB.Mesh>> target, DB.ElementId parentElementId, bool makeTransparent),
List<SOG.Mesh>
> _meshByMaterialConverter;
private readonly ITypedConverter<DB.Curve, ICurve> _curveConverter;
private readonly ITypedConverter<DB.PolyLine, SOG.Polyline> _polylineConverter;
private readonly ITypedConverter<DB.Point, SOG.Point> _pointConverter;
private readonly ITypedConverter<DB.PointCloudInstance, SOG.Pointcloud> _pointcloudConverter;
private readonly ILogger<DisplayValueExtractor> _logger;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
public DisplayValueExtractor(
ITypedConverter<
(Dictionary<DB.ElementId, List<DB.Mesh>> target, DB.ElementId parentElementId, bool makeTransparent),
List<SOG.Mesh>
> meshByMaterialConverter,
ITypedConverter<DB.Curve, ICurve> curveConverter,
ITypedConverter<DB.PolyLine, SOG.Polyline> polylineConverter,
ITypedConverter<DB.Point, SOG.Point> pointConverter,
ITypedConverter<DB.PointCloudInstance, SOG.Pointcloud> pointcloudConverter,
ILogger<DisplayValueExtractor> logger,
IConverterSettingsStore<RevitConversionSettings> converterSettings
)
{
_meshByMaterialConverter = meshByMaterialConverter;
_curveConverter = curveConverter;
_polylineConverter = polylineConverter;
_pointConverter = pointConverter;
_pointcloudConverter = pointcloudConverter;
_logger = logger;
_converterSettings = converterSettings;
}
public List<Base> 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<Base> areaDisplay = new();
using (var options = new DB.SpatialElementBoundaryOptions())
{
foreach (IList<DB.BoundarySegment> 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<Base> 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<Base> GetGeometryDisplayValue(DB.Element element, DB.Options? options = null)
{
var collections = GetSortedGeometryFromElement(element, options);
return ProcessGeometryCollections(element, collections);
}
/// <summary>
/// Extracts and sorts all geometry from an element into separate collections by type.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Essentially all the ensuing steps after the common get_Geometry element method
/// </remarks>
private List<Base> ProcessGeometryCollections(DB.Element element, GeometryCollections collections)
{
List<Base> displayValue = new();
// handle all solids and meshes by their material
var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids);
List<SOG.Mesh> 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<DB.ElementId, List<DB.Mesh>> GetMeshesByMaterial(
List<DB.Mesh> meshes,
List<DB.Solid> solids
)
{
var meshesByMaterial = new Dictionary<DB.ElementId, List<DB.Mesh>>();
foreach (var mesh in meshes)
{
var materialId = mesh.MaterialElementId;
if (!meshesByMaterial.TryGetValue(materialId, out List<DB.Mesh>? value))
{
value = new List<DB.Mesh>();
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<DB.Mesh>? value))
{
value = new List<DB.Mesh>();
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<DetailLevelType, DB.ViewDetailLevel> _detailLevelMap =
new()
{
{ DetailLevelType.Coarse, DB.ViewDetailLevel.Coarse },
{ DetailLevelType.Medium, DB.ViewDetailLevel.Medium },
{ DetailLevelType.Fine, DB.ViewDetailLevel.Fine }
};
/// <summary>
/// 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
/// </summary>
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;
}
}
}
/// <summary>
/// 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
/// </summary>
private readonly Dictionary<string, DB.GraphicsStyle> _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
}
/// <summary>
/// Overrides current view options to extract meaningful geometry for various elements. E.g., pipes, plumbing fixtures, steel elements
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Gets the solid representation of rebar elements.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private List<Base> 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<Base>();
}
/// <summary>
/// Gets the centerline representation of a rebar element.
/// </summary>
/// <remarks>
/// This method extracts the centerlines of rebar elements when a simplified representation is preferred.
/// </remarks>
private List<Base> GetRebarCenterlineDisplayValue(DB.Structure.Rebar rebar)
{
bool isSingleLayout = rebar.LayoutRule == DB.Structure.RebarLayoutRule.Single;
int numberOfBarPositions = rebar.NumberOfBarPositions;
List<DB.Curve> 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<Base> displayValue = new();
foreach (var curve in curves)
{
displayValue.Add(GetCurveDisplayValue(curve));
}
return displayValue;
}
/// <summary>
/// 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.
/// </summary>
private sealed record GeometryCollections
{
public List<DB.Solid> Solids { get; } = new();
public List<DB.Mesh> Meshes { get; } = new();
public List<DB.Curve> Curves { get; } = new();
public List<DB.PolyLine> Polylines { get; } = new();
public List<DB.Point> Points { get; } = new();
}
}