Skip to content

Commit

Permalink
Fix(Revit) : add material quantites by mesh (#3030)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Kilian Speiser <[email protected]>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent aeca91d commit 13b153e
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -1077,7 +1085,7 @@ public static RenderMaterial GetMEPSystemMaterial(Element e)
/// </summary>
/// <param name="e">Revit element to parse</param>
/// <returns>Revit material of the element, null if no material found</returns>
public static DB.Material GetMEPSystemRevitMaterial(Element e)
public static DB.Material? GetMEPSystemRevitMaterial(Element e)
{
ElementId idType = ElementId.InvalidElementId;

Expand All @@ -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;
}
}
}
Expand All @@ -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


Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -12,18 +10,22 @@ public static class ElementExtensions
public static IEnumerable<Connector> GetConnectorSet(this Element element)
{
var empty = Enumerable.Empty<Connector>();
return element.GetConnectorManager()?.Connectors?.Cast<Connector>() ?? empty;
}

public static ConnectorManager? GetConnectorManager(this Element element)
{
return element switch
{
CableTray o => o.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
Conduit o => o.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
Duct o => o.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
FamilyInstance o => o.MEPModel?.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
FlexDuct o => o.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
FlexPipe o => o.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
Pipe o => o.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
Wire o => o.ConnectorManager?.Connectors?.Cast<Connector>() ?? empty,
_ => Enumerable.Empty<Connector>(),
MEPCurve o => o.ConnectorManager,
FamilyInstance o => o.MEPModel?.ConnectorManager,
_ => null,
};
}

public static bool IsMEPElement(this Element element)
{
return element.GetConnectorManager() != null;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,69 +10,153 @@ namespace Objects.Converter.Revit
{
public partial class ConverterRevit
{
#region MaterialQuantity
/// <summary>
/// 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.
/// </summary>
/// <param name="element"></param>
/// <param name="material"></param>
/// <param name="units"></param>
/// <returns></returns>
public Objects.Other.MaterialQuantity MaterialQuantityToSpeckle(DB.Element element, DB.Material material, string units)
public IEnumerable<Other.MaterialQuantity> 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<MaterialQuantity>() : new List<MaterialQuantity>() { quantity };
}
else
{
return GetMaterialQuantitiesFromSolids(element, units);
}
}

private IEnumerable<MaterialQuantity> 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<MaterialQuantity> 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<Objects.Other.MaterialQuantity> MaterialQuantitiesToSpeckle(DB.Element element, string units)
private (double, double) GetAreaAndVolumeFromSolids(List<Solid> 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<double> areaOfLargestFaceInEachSolid = solids
.Select(solid => solid.Faces.Cast<Face>().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<DB.ElementId> GetMaterialsFromSolids(List<Solid> solids)
{
return solids
.Where(solid => solid.Volume > 0 && !solid.Faces.IsEmpty)
.Select(m => m.Faces.get_Item(0).MaterialElementId)
.Distinct();
}
public IEnumerable<Objects.Other.MaterialQuantity> MaterialQuantitiesToSpeckle(DB.Element element, IEnumerable<DB.Material> materials, string units)

private bool MaterialAreaAPICallWillReportSingleFace(Element element)
{
if (materials == null || !materials.Any()) return null;
List<Objects.Other.MaterialQuantity> quantities = new List<Objects.Other.MaterialQuantity>();

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
};
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,24 @@ public List<Mesh> 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<Solid>, List<DB.Mesh>) 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);
Expand All @@ -62,20 +77,16 @@ public List<Mesh> GetElementDisplayValue(
geom = element.get_Geometry(options);
}

if (geom == null)
return displayMeshes;

// retrieves all meshes and solids from a geometry element
var solids = new List<Solid>();
var meshes = new List<DB.Mesh>();

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(
Expand Down

0 comments on commit 13b153e

Please sign in to comment.