From 13b153e40119c722b6570ebf87358f2e3593af69 Mon Sep 17 00:00:00 2001 From: connorivy <43247197+connorivy@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:12:23 -0600 Subject: [PATCH] Fix(Revit) : add material quantites by mesh (#3030) * wip * finalize changes * account for most materials reporting quantity incorrectly * standardize materialQuant creation * adjust for not all mep materials being retrieved same way * rename Create method, more explicit types, if else -> switch * catch revit specific exceptions --------- Co-authored-by: Connor Ivy Co-authored-by: Kilian Speiser --- .../ConverterRevitShared/ConversionUtils.cs | 51 ++---- .../Extensions/ElementExtensions.cs | 26 +-- .../ConvertMaterialQuantities.cs | 173 +++++++++++++----- .../Partial Classes/ConvertMeshUtils.cs | 33 ++-- 4 files changed, 183 insertions(+), 100 deletions(-) diff --git a/Objects/Converters/ConverterRevit/ConverterRevitShared/ConversionUtils.cs b/Objects/Converters/ConverterRevit/ConverterRevitShared/ConversionUtils.cs index 39dd8da75f..7e4ee707d1 100644 --- a/Objects/Converters/ConverterRevit/ConverterRevitShared/ConversionUtils.cs +++ b/Objects/Converters/ConverterRevit/ConverterRevitShared/ConversionUtils.cs @@ -271,9 +271,17 @@ public void GetAllRevitParamsAndIds(Base speckleElement, DB.Element revitElement speckleElement["builtInCategory"] = builtInCategory.ToString(); //NOTE: adds the quantities of all materials to an element - var qs = MaterialQuantitiesToSpeckle(revitElement, speckleElement["units"] as string); - if (qs != null) - speckleElement["materialQuantities"] = qs; + try + { + speckleElement["materialQuantities"] = MaterialQuantitiesToSpeckle( + revitElement, + speckleElement["units"] as string)? + .ToList(); + } + catch (Autodesk.Revit.Exceptions.ApplicationException ex) + { + SpeckleLog.Logger.Error(ex, "An exception occurred in the Revit API while retrieving material quantities from element of type {elementType} and category {elementCategory}", revitElement.GetType(), revitElement.Category); + } } private void AddElementParamsToDict( @@ -1077,7 +1085,7 @@ public static RenderMaterial GetMEPSystemMaterial(Element e) /// /// Revit element to parse /// Revit material of the element, null if no material found - public static DB.Material GetMEPSystemRevitMaterial(Element e) + public static DB.Material? GetMEPSystemRevitMaterial(Element e) { ElementId idType = ElementId.InvalidElementId; @@ -1089,21 +1097,16 @@ public static DB.Material GetMEPSystemRevitMaterial(Element e) idType = system.GetTypeId(); } } - else if (IsSupportedMEPCategory(e)) + else if (e.GetConnectorManager() is ConnectorManager connectorManager) { - MEPModel m = ((DB.FamilyInstance)e).MEPModel; - - if (m != null && m.ConnectorManager != null) + //retrieve the first material from first connector. Could go wrong, but better than nothing ;-) + foreach (Connector item in connectorManager.Connectors) { - //retrieve the first material from first connector. Could go wrong, but better than nothing ;-) - foreach (Connector item in m.ConnectorManager.Connectors) + var system = item.MEPSystem; + if (system != null) { - var system = item.MEPSystem; - if (system != null) - { - idType = system.GetTypeId(); - break; - } + idType = system.GetTypeId(); + break; } } } @@ -1119,22 +1122,6 @@ public static DB.Material GetMEPSystemRevitMaterial(Element e) return null; } - private static bool IsSupportedMEPCategory(Element e) - { - var categories = e.Document.Settings.Categories; - - var supportedCategories = new[] - { - BuiltInCategory.OST_PipeFitting, - BuiltInCategory.OST_DuctFitting, - BuiltInCategory.OST_DuctAccessory, - BuiltInCategory.OST_PipeAccessory, - //BuiltInCategory.OST_MechanicalEquipment, - }; - - return supportedCategories.Any(cat => e.Category.Id == categories.get_Item(cat).Id); - } - #endregion diff --git a/Objects/Converters/ConverterRevit/ConverterRevitShared/Extensions/ElementExtensions.cs b/Objects/Converters/ConverterRevit/ConverterRevitShared/Extensions/ElementExtensions.cs index 45211b2db3..a16fe4297b 100644 --- a/Objects/Converters/ConverterRevit/ConverterRevitShared/Extensions/ElementExtensions.cs +++ b/Objects/Converters/ConverterRevit/ConverterRevitShared/Extensions/ElementExtensions.cs @@ -1,9 +1,7 @@ +#nullable enable using System.Collections.Generic; using System.Linq; using Autodesk.Revit.DB; -using Autodesk.Revit.DB.Electrical; -using Autodesk.Revit.DB.Mechanical; -using Autodesk.Revit.DB.Plumbing; namespace ConverterRevitShared.Extensions { @@ -12,18 +10,22 @@ public static class ElementExtensions public static IEnumerable GetConnectorSet(this Element element) { var empty = Enumerable.Empty(); + return element.GetConnectorManager()?.Connectors?.Cast() ?? empty; + } + + public static ConnectorManager? GetConnectorManager(this Element element) + { return element switch { - CableTray o => o.ConnectorManager?.Connectors?.Cast() ?? empty, - Conduit o => o.ConnectorManager?.Connectors?.Cast() ?? empty, - Duct o => o.ConnectorManager?.Connectors?.Cast() ?? empty, - FamilyInstance o => o.MEPModel?.ConnectorManager?.Connectors?.Cast() ?? empty, - FlexDuct o => o.ConnectorManager?.Connectors?.Cast() ?? empty, - FlexPipe o => o.ConnectorManager?.Connectors?.Cast() ?? empty, - Pipe o => o.ConnectorManager?.Connectors?.Cast() ?? empty, - Wire o => o.ConnectorManager?.Connectors?.Cast() ?? empty, - _ => Enumerable.Empty(), + MEPCurve o => o.ConnectorManager, + FamilyInstance o => o.MEPModel?.ConnectorManager, + _ => null, }; } + + public static bool IsMEPElement(this Element element) + { + return element.GetConnectorManager() != null; + } } } diff --git a/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMaterialQuantities.cs b/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMaterialQuantities.cs index 43bed2c31b..c2df744013 100644 --- a/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMaterialQuantities.cs +++ b/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMaterialQuantities.cs @@ -1,8 +1,7 @@ +#nullable enable using Autodesk.Revit.DB; -using Autodesk.Revit.DB.Structure; -using Objects.BuiltElements; -using Objects.BuiltElements.Revit; -using Speckle.Core.Models; +using ConverterRevitShared.Extensions; +using Objects.Other; using System.Collections.Generic; using System.Linq; using DB = Autodesk.Revit.DB; @@ -11,69 +10,153 @@ namespace Objects.Converter.Revit { public partial class ConverterRevit { - #region MaterialQuantity /// - /// Gets the quantitiy of a material in one element + /// Material Quantities in Revit are stored in different ways and therefore need to be retrieved + /// using different methods. According to this forum post https://forums.autodesk.com/t5/revit-api-forum/method-getmaterialarea-appears-to-use-different-formulas-for/td-p/11988215 + /// "Hosts" (whatever that means) will return the area of a single side of the object while other + /// objects will return the combined area of every side of the element. Certain MEP element materials + /// are attached to the MEP system that the element belongs to. /// /// - /// + /// /// - public Objects.Other.MaterialQuantity MaterialQuantityToSpeckle(DB.Element element, DB.Material material, string units) + public IEnumerable MaterialQuantitiesToSpeckle(DB.Element element, string units) { - if (material == null || element == null) + if (MaterialAreaAPICallWillReportSingleFace(element)) + { + return GetMaterialQuantitiesFromAPICall(element, units); + } + else if (MaterialIsAttachedToMEPSystem(element)) + { + MaterialQuantity quantity = GetMaterialQuantityForMEPElement(element, units); + return quantity == null ? Enumerable.Empty() : new List() { quantity }; + } + else + { + return GetMaterialQuantitiesFromSolids(element, units); + } + } + + private IEnumerable GetMaterialQuantitiesFromAPICall(DB.Element element, string units) + { + foreach (ElementId matId in element.GetMaterialIds(false)) + { + double volume = element.GetMaterialVolume(matId); + double area = element.GetMaterialArea(matId, false); + yield return CreateMaterialQuantity(element, matId, area, volume, units); + } + } + + private MaterialQuantity? GetMaterialQuantityForMEPElement(DB.Element element, string units) + { + DB.Material material = GetMEPSystemRevitMaterial(element); + if (material == null) + { return null; + } - // To-Do: These methods from the Revit API appear to have bugs. - double volume = element.GetMaterialVolume(material.Id); - double area = element.GetMaterialArea(material.Id, false); + DB.Options options = new() { DetailLevel = ViewDetailLevel.Fine }; + var (solids, _) = GetSolidsAndMeshesFromElement(element, options); - // Convert revit interal units to speckle commit units + (double area, double volume) = GetAreaAndVolumeFromSolids(solids); + return CreateMaterialQuantity(element, material.Id, area, volume, units); + } + + private IEnumerable GetMaterialQuantitiesFromSolids(DB.Element element, string units) + { + DB.Options options = new() { DetailLevel = ViewDetailLevel.Fine }; + var (solids, _) = GetSolidsAndMeshesFromElement(element, options); + + foreach (ElementId matId in GetMaterialsFromSolids(solids)) + { + (double area, double volume) = GetAreaAndVolumeFromSolids(solids, matId); + yield return CreateMaterialQuantity(element, matId, area, volume, units); + } + } + + private MaterialQuantity CreateMaterialQuantity( + Element element, + ElementId materialId, + double areaRevitInternalUnits, + double volumeRevitInternalUnits, + string units) + { + Other.Material speckleMaterial = ConvertAndCacheMaterial(materialId, element.Document); double factor = ScaleToSpeckle(1); - volume *= factor * factor * factor; - area *= factor * factor; + double area = factor * factor * areaRevitInternalUnits; + double volume = factor * factor * factor * volumeRevitInternalUnits; + MaterialQuantity materialQuantity = new(speckleMaterial, volume, area, units); + + switch (element) + { + case DB.Architecture.Railing railing: + materialQuantity["length"] = railing.GetPath().Sum(e => e.Length) * factor; + break; + + case DB.Architecture.ContinuousRail continuousRail: + materialQuantity["length"] = continuousRail.GetPath().Sum(e => e.Length) * factor; + break; - var speckleMaterial = ConvertAndCacheMaterial(material.Id, material.Document); - var materialQuantity = new Objects.Other.MaterialQuantity(speckleMaterial, volume, area, units); + default: + if (LocationToSpeckle(element) is ICurve curve) + { + materialQuantity["length"] = curve.length; + } + break; + }; - if (LocationToSpeckle(element) is ICurve curve) - materialQuantity["length"] = curve.length; return materialQuantity; } - #endregion - - #region MaterialQuantities - public IEnumerable MaterialQuantitiesToSpeckle(DB.Element element, string units) + private (double, double) GetAreaAndVolumeFromSolids(List solids, ElementId? materialId = null) { - - var matIDs = element?.GetMaterialIds(false); - // Does not return the correct materials for some categories - // Need to take different approach for MEP-Elements - if (matIDs == null || !matIDs.Any() && element is MEPCurve) + if (materialId != null) { - DB.Material mepMaterial = ConverterRevit.GetMEPSystemRevitMaterial(element); - if (mepMaterial != null) matIDs.Add(mepMaterial.Id); + solids = solids + .Where( + solid => solid.Volume > 0 + && !solid.Faces.IsEmpty + && solid.Faces.get_Item(0).MaterialElementId == materialId) + .ToList(); } - if (matIDs == null || !matIDs.Any()) - return null; + double volume = solids.Sum(solid => solid.Volume); + IEnumerable areaOfLargestFaceInEachSolid = solids + .Select(solid => solid.Faces.Cast().Select(face => face.Area) + .Max()); + double area = areaOfLargestFaceInEachSolid.Sum(); + return (area, volume); + } - var materials = matIDs.Select(material => element.Document.GetElement(material) as DB.Material); - return MaterialQuantitiesToSpeckle(element, materials, units); + private IEnumerable GetMaterialsFromSolids(List solids) + { + return solids + .Where(solid => solid.Volume > 0 && !solid.Faces.IsEmpty) + .Select(m => m.Faces.get_Item(0).MaterialElementId) + .Distinct(); } - public IEnumerable MaterialQuantitiesToSpeckle(DB.Element element, IEnumerable materials, string units) + + private bool MaterialAreaAPICallWillReportSingleFace(Element element) { - if (materials == null || !materials.Any()) return null; - List quantities = new List(); - - foreach (var material in materials) - quantities.Add(MaterialQuantityToSpeckle(element, material, units)); - - return quantities; + return element switch + { + DB.CeilingAndFloor + or DB.Wall + or DB.RoofBase => true, + _ => false + }; } - - #endregion + private bool MaterialIsAttachedToMEPSystem(Element element) + { + return element switch + { + DB.Mechanical.Duct + or DB.Mechanical.FlexDuct + or DB.Plumbing.Pipe + or DB.Plumbing.FlexPipe => true, + _ => false + }; + } } - } diff --git a/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMeshUtils.cs b/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMeshUtils.cs index 7e828d50d8..7e1c6a40a3 100644 --- a/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMeshUtils.cs +++ b/Objects/Converters/ConverterRevit/ConverterRevitShared/Partial Classes/ConvertMeshUtils.cs @@ -49,9 +49,24 @@ public List GetElementDisplayValue( return displayMeshes; } + var (solids, meshes) = GetSolidsAndMeshesFromElement(element, options, transform); + + // convert meshes and solids + displayMeshes.AddRange(ConvertMeshesByRenderMaterial(meshes, element.Document, isConvertedAsInstance)); + displayMeshes.AddRange(ConvertSolidsByRenderMaterial(solids, element.Document, isConvertedAsInstance)); + + return displayMeshes; + } + + public (List, List) GetSolidsAndMeshesFromElement( + Element element, + Options options, + Transform? transform = null + ) + { options = ViewSpecificOptions ?? options ?? new Options() { DetailLevel = DetailLevelSetting }; - GeometryElement geom = null; + GeometryElement geom; try { geom = element.get_Geometry(options); @@ -62,20 +77,16 @@ public List GetElementDisplayValue( geom = element.get_Geometry(options); } - if (geom == null) - return displayMeshes; - - // retrieves all meshes and solids from a geometry element var solids = new List(); var meshes = new List(); - SortGeometry(element, solids, meshes, geom, transform?.Inverse); - - // convert meshes and solids - displayMeshes.AddRange(ConvertMeshesByRenderMaterial(meshes, element.Document, isConvertedAsInstance)); - displayMeshes.AddRange(ConvertSolidsByRenderMaterial(solids, element.Document, isConvertedAsInstance)); + if (geom != null) + { + // retrieves all meshes and solids from a geometry element + SortGeometry(element, solids, meshes, geom, transform?.Inverse); + } - return displayMeshes; + return (solids, meshes); } private static void LogInstanceMeshRetrievalWarnings(