diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 87e527dc72..264a8fbf6b 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -24,6 +24,7 @@ import bonsai.tool as tool import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.element +import ifc5d.qto from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree from shapely.geometry import Polygon @@ -565,7 +566,7 @@ def has_openings(obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]: element = tool.Ifc.get_entity(obj) if not element: return [] - return [o for o in tool.Geometry.get_openings(element)] + return [o for o in ifcopenshell.util.element.get_openings(element)] def get_obj_decompositions(obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: @@ -576,20 +577,42 @@ def get_obj_decompositions(obj: bpy.types.Object) -> set[ifcopenshell.entity_ins def get_gross_weight(obj: bpy.types.Object) -> Union[float, None]: - """Get gross weight of the object (based on gross volume and Pset_MaterialCommon.MassDensity)""" + """Get gross weight of the object. + + Based on gross volume and Pset_MaterialCommon.MassDensity + or Pset_ProfileMechanical.MassPerLength and extrusion depth if it's profile based. + """ + + weight = get_profile_obj_weight(obj) + if weight is not None: + return weight + obj_mass_density = get_obj_mass_density(obj) if not obj_mass_density: return + gross_volume = get_gross_volume(obj) gross_weight = obj_mass_density * gross_volume return gross_weight def get_net_weight(obj: bpy.types.Object) -> Union[float, None]: - """Get net weight of the object (based on net volume and Pset_MaterialCommon.MassDensity)""" + """Get net weight of the object. + + Based on Pset_ProfileMechanical.MassPerLength and extrusion depth + (for profile based objects, though objects with openings are not supported) + or object's net volume and Pset_MaterialCommon.MassDensity. + """ + + if not has_openings(obj): + weight = get_profile_obj_weight(obj) + if weight is not None: + return weight + obj_mass_density = get_obj_mass_density(obj) if not obj_mass_density: return + net_volume = get_net_volume(obj) net_weight = obj_mass_density * net_volume return net_weight @@ -602,6 +625,13 @@ def get_obj_mass_density(obj: bpy.types.Object) -> Union[float, None]: return ifcopenshell.util.element.get_element_mass_density(entity) +def get_profile_obj_weight(obj: bpy.types.Object) -> Union[float, None]: + element = tool.Ifc.get_entity(obj) + assert element + weight = ifc5d.qto.IfcOpenShell.get_weight_profile_based(element) + return weight + + def get_opening_type(opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]: """_summary_: Returns the opening type - OPENING / RECESS diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 9f3a5fee30..2975bbdbcb 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1660,15 +1660,13 @@ class Geometry(bonsai.core.tool.Geometry): Use `.RelatedOpeningElement` to get the opening element. """ - for element_rel in getattr(element, "HasOpenings", ()): - yield element_rel - - if aggregate := ifcopenshell.util.element.get_aggregate(element): - yield from cls.get_openings(aggregate) + # TODO: replace everywhere with util method. + return ifcopenshell.util.element.get_openings(element) @classmethod def has_openings(cls, element: ifcopenshell.entity_instance) -> bool: - return bool(next(cls.get_openings(element), False)) + # TODO: replace everywhere with util method. + return ifcopenshell.util.element.has_openings(element) @classmethod def get_elements_by_representation( diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index a71aa32942..6856d78fba 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -238,7 +238,9 @@ class IfcOpenShell(QtoCalculator): "get_weight": Function( "IfcMassMeasure", "Weight", - "The weight of the object based on it's volume and material density (from Pset_MaterialCommon.MassDensity).", + "The weight of the object based on it's length and Pset_ProfileMechanical.MassPerLength " + "(for profile based objects, though objects with openings are not supported for net calculations)" + "or it's volume and material density (from Pset_MaterialCommon.MassDensity).", ), } @@ -308,7 +310,8 @@ class IfcOpenShell(QtoCalculator): if value is None: continue elif formula == "get_weight": - value = cls.get_weight(element, geometry) + calculation_type = "GROSS" if iterator.settings is cls.gross_settings else "NET" + value = cls.get_weight(element, geometry, calculation_type) if value is None: continue else: @@ -367,7 +370,10 @@ class IfcOpenShell(QtoCalculator): @classmethod def get_weight( - cls, element: ifcopenshell.entity_instance, geometry: ifcopenshell.geom.ShapeType + cls, + element: ifcopenshell.entity_instance, + geometry: ifcopenshell.geom.ShapeType, + calculation_type: Literal["GROSS", "NET"], ) -> Union[float, None]: """Get element's weight. @@ -376,12 +382,44 @@ class IfcOpenShell(QtoCalculator): or ``None`` if mass density calculation for this element is not supported. """ + if calculation_type == "gross" or not ifcopenshell.util.element.has_openings(element): + weight = cls.get_weight_profile_based(element) + if weight is not None: + return weight + density = ifcopenshell.util.element.get_element_mass_density(element) if density is None: return volume = ifcopenshell.util.shape.get_volume(geometry) return volume * density + @classmethod + def get_weight_profile_based(cls, element: ifcopenshell.entity_instance) -> Union[float, None]: + """Get weight of the profile based element. + + :return: A float weight value if calculation was successful + or ``None`` if it's either not profile based object + or it's not supported. + """ + representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if not representation: + return None + items = representation.Items + if not all(item.is_a("IfcExtrudedAreaSolid") for item in items): + return None + mass = 0.0 + for item in items: + profile = item.SweptArea + # TODO: there are also bunch of other similar props we will need to consider in the future. + # Examples: + # - Pset_CableSegmentTypeBusBarSegment.MassPerLength + # - Pset_CableCarrierSegmentTypeCatenaryWire.MassPerLength + mass_per_length = ifcopenshell.util.element.get_pset(profile, "Pset_ProfileMechanical", "MassPerLength") + if not isinstance(mass_per_length, float): + return None + mass += mass_per_length * item.Depth + return mass + class Blender(QtoCalculator): """Calculates geometry based on currently loaded Blender objects.""" diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 85cc192ceb..4f21aa8a98 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.representation -from typing import Any, Callable, Optional, Union, Literal, overload, Sequence +from typing import Any, Callable, Optional, Union, Literal, overload, Sequence, Generator from collections import namedtuple @@ -1720,3 +1720,27 @@ def has_property(product: ifcopenshell.entity_instance, property_name: str) -> b return True qtos = get_psets(product, qtos_only=True) return any(property_name in quantities.keys() for quantities in qtos.values()) + + +def get_openings(element: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance, None, None]: + """Get element openings as IfcRelVoidsElements. + + Use `.RelatedOpeningElement` to get the opening element. + + :param element: IfcElement. + :return: Generator of IfcRelVoidsElements. + """ + for element_rel in getattr(element, "HasOpenings", ()): + yield element_rel + + if aggregate := get_aggregate(element): + yield from get_openings(aggregate) + + +def has_openings(element: ifcopenshell.entity_instance) -> bool: + """Check if the element has openings. + + :param element: IfcElement. + :return: True if element has openings. + """ + return bool(next(get_openings(element), False))