3b6623e51a
* 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
506 lines
18 KiB
C#
506 lines
18 KiB
C#
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();
|
||
}
|
||
}
|