Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix(Revit) : add material quantites by mesh #3030

Merged
merged 11 commits into from
Nov 9, 2023
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);
}
}
connorivy marked this conversation as resolved.
Show resolved Hide resolved

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 ;-)
BovineOx marked this conversation as resolved.
Show resolved Hide resolved
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>
connorivy marked this conversation as resolved.
Show resolved Hide resolved
/// <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);
}
BovineOx marked this conversation as resolved.
Show resolved Hide resolved
}

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;
connorivy marked this conversation as resolved.
Show resolved Hide resolved
}

#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)
BovineOx marked this conversation as resolved.
Show resolved Hide resolved
.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 partial class ConverterRevit
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 partial class ConverterRevit
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