diff --git a/freecad/bem/entities.py b/freecad/bem/entities.py index 8d31445..bd19626 100644 --- a/freecad/bem/entities.py +++ b/freecad/bem/entities.py @@ -1,518 +1,519 @@ # coding: utf8 """This module contains FreeCAD wrapper class for IfcEntities © All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, Laboratory CNPA, 2019-2020 See the LICENSE.TXT file for more details. Author : Cyril Waechter """ import typing from typing import Iterable import ifcopenshell import FreeCAD import Part from freecad.bem.bem_logging import logger from freecad.bem import utils if typing.TYPE_CHECKING: # https://www.python.org/dev/peps/pep-0484/#id36 from freecad.bem.ifc_importer import IfcImporter from freecad.bem.typing import ( # pylint: disable=import-error, no-name-in-module RootFeature, BEMBoundaryFeature, RelSpaceBoundaryFeature, ContainerFeature, SpaceFeature, ProjectFeature, ElementFeature, ) def get_color(ifc_boundary): """Return a color depending on IfcClass given""" product_colors = { "IfcWall": (0.7, 0.3, 0.0), "IfcWindow": (0.0, 0.7, 1.0), "IfcSlab": (0.7, 0.7, 0.5), "IfcRoof": (0.0, 0.3, 0.0), "IfcDoor": (1.0, 1.0, 1.0), } if ifc_boundary.PhysicalOrVirtualBoundary == "VIRTUAL": return (1.0, 0.0, 1.0) ifc_product = ifc_boundary.RelatedBuildingElement if not ifc_product: return (1.0, 0.0, 0.0) for product, color in product_colors.items(): # Not only test if IFC class is in dictionnary but it is a subclass if ifc_product.is_a(product): return color print(f"No color found for {ifc_product.is_a()}") return (0.0, 0.0, 0.0) def get_related_element(ifc_entity, doc=FreeCAD.ActiveDocument) -> Part.Feature: if not ifc_entity.RelatedBuildingElement: return guid = ifc_entity.RelatedBuildingElement.GlobalId for element in doc.Objects: try: if element.GlobalId == guid: return element except AttributeError: continue class Root: """Wrapping various IFC entity : https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcroot.htm """ def __init__(self, obj: "RootFeature") -> None: self.Type = self.__class__.__name__ # pylint: disable=invalid-name obj.Proxy = self # pylint: disable=invalid-name + # TODO: remove 2nd argument for FreeCAD ⩾0.19.1. See https://forum.freecadweb.org/viewtopic.php?t=57479 obj.addExtension("App::GroupExtensionPython", self) @classmethod def _init_properties(cls, obj: "RootFeature") -> None: ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyString", "IfcType", "IFC") obj.addProperty("App::PropertyInteger", "Id", ifc_attributes) obj.addProperty("App::PropertyString", "GlobalId", ifc_attributes) obj.addProperty("App::PropertyString", "IfcName", ifc_attributes) obj.addProperty("App::PropertyString", "Description", ifc_attributes) @classmethod def create(cls) -> "RootFeature": """Stantard FreeCAD FeaturePython Object creation method""" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", cls.__name__) cls(obj) cls._init_properties(obj) if FreeCAD.GuiUp: obj.ViewObject.Proxy = ViewProviderRoot(obj.ViewObject) return obj @classmethod def create_from_ifc(cls, ifc_entity, ifc_importer: "IfcImporter") -> "RootFeature": """As cls.create but providing an ifc source""" obj = cls.create() obj.Proxy.ifc_importer = ifc_importer cls.read_from_ifc(obj, ifc_entity) cls.set_label(obj) return obj @classmethod def read_from_ifc(cls, obj: "RootFeature", ifc_entity) -> None: obj.Id = ifc_entity.id() obj.GlobalId = ifc_entity.GlobalId obj.IfcType = ifc_entity.is_a() obj.IfcName = ifc_entity.Name or "" obj.Description = ifc_entity.Description or "" @staticmethod def set_label(obj: "RootFeature") -> None: """Allow specific method for specific elements""" obj.Label = f"{obj.Id}_{obj.IfcName or obj.IfcType}" @staticmethod def read_pset_from_ifc( obj: "RootFeature", ifc_entity, properties: Iterable[str] ) -> None: psets = ifcopenshell.util.element.get_psets(ifc_entity) for pset in psets.values(): for prop_name, prop in pset.items(): if prop_name in properties: setattr(obj, prop_name, getattr(prop, "wrappedValue", prop)) class ViewProviderRoot: def __init__(self, vobj): vobj.Proxy = self vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self) class RelSpaceBoundary(Root): """Wrapping IFC entity : https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary2ndlevel.htm""" def __init__(self, obj: "RelSpaceBoundaryFeature") -> None: super().__init__(obj) obj.Proxy = self @classmethod def _init_properties(cls, obj: "RelSpaceBoundaryFeature") -> None: super()._init_properties(obj) bem_category = "BEM" ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyLinkHidden", "RelatingSpace", ifc_attributes) obj.addProperty("App::PropertyLink", "RelatedBuildingElement", ifc_attributes) obj.addProperty( "App::PropertyEnumeration", "PhysicalOrVirtualBoundary", ifc_attributes ).PhysicalOrVirtualBoundary = ["PHYSICAL", "VIRTUAL", "NOTDEFINED"] obj.addProperty( "App::PropertyEnumeration", "InternalOrExternalBoundary", ifc_attributes ).InternalOrExternalBoundary = [ "INTERNAL", "EXTERNAL", "EXTERNAL_EARTH", "EXTERNAL_WATER", "EXTERNAL_FIRE", "NOTDEFINED", ] obj.addProperty( "App::PropertyLinkHidden", "CorrespondingBoundary", ifc_attributes ) obj.addProperty("App::PropertyLinkHidden", "ParentBoundary", ifc_attributes) obj.addProperty("App::PropertyLinkList", "InnerBoundaries", ifc_attributes) obj.addProperty("App::PropertyVector", "Normal", bem_category) obj.addProperty("App::PropertyVector", "TranslationToSpace", bem_category) obj.addProperty("App::PropertyLength", "UndergroundDepth", bem_category) obj.addProperty("App::PropertyIntegerList", "ClosestBoundaries", bem_category) obj.addProperty("App::PropertyIntegerList", "ClosestEdges", bem_category) obj.addProperty("App::PropertyIntegerList", "ClosestDistance", bem_category) obj.addProperty("App::PropertyBool", "IsHosted", bem_category) obj.addProperty( "App::PropertyInteger", "InternalToExternal", bem_category, """Define if material layers direction. 1 mean from inside of the space toward outside of the space. -1 is the opposite. 0 is undefined (bool not used as 3 state bool do no exist in FreeCAD yet).""", ) obj.addProperty("App::PropertyArea", "Area", bem_category) obj.addProperty("App::PropertyArea", "AreaWithHosted", bem_category) obj.addProperty("App::PropertyLink", "SIA_Interior", bem_category) obj.addProperty("App::PropertyLink", "SIA_Exterior", bem_category) obj.addProperty( "App::PropertyEnumeration", "LesoType", bem_category ).LesoType = [ "Ceiling", "Wall", "Flooring", "Window", "Door", "Opening", "Unknown", ] @classmethod def read_from_ifc(cls, obj: "RelSpaceBoundaryFeature", ifc_entity) -> None: super().read_from_ifc(obj, ifc_entity) ifc_importer = obj.Proxy.ifc_importer element = get_related_element(ifc_entity, ifc_importer.doc) if element: obj.RelatedBuildingElement = element utils.append(element, "ProvidesBoundaries", obj) obj.InternalOrExternalBoundary = ifc_entity.InternalOrExternalBoundary obj.PhysicalOrVirtualBoundary = ifc_entity.PhysicalOrVirtualBoundary try: obj.Shape = ifc_importer.create_fc_shape(ifc_entity) except utils.ShapeCreationError: ifc_importer.doc.removeObject(obj.Name) raise utils.ShapeCreationError obj.Area = obj.AreaWithHosted = obj.Shape.Area if obj.Area < utils.TOLERANCE: ifc_importer.doc.removeObject(obj.Name) raise utils.IsTooSmall try: obj.IsHosted = bool(ifc_entity.RelatedBuildingElement.FillsVoids) except AttributeError: obj.IsHosted = False obj.LesoType = "Unknown" obj.CorrespondingBoundary = getattr(ifc_entity, "CorrespondingBoundary", None) if FreeCAD.GuiUp: obj.ViewObject.Proxy = 0 obj.ViewObject.ShapeColor = get_color(ifc_entity) def onChanged( self, obj: "RelSpaceBoundaryFeature", prop ): # pylint: disable=invalid-name if prop == "InnerBoundaries": self.recompute_area_with_hosted(obj) @classmethod def recompute_areas(cls, obj: "RelSpaceBoundaryFeature") -> None: obj.Area = obj.Shape.Faces[0].Area cls.recompute_area_with_hosted(obj) @staticmethod def recompute_area_with_hosted(obj: "RelSpaceBoundaryFeature") -> None: """Recompute area including inner boundaries""" area = obj.Area for boundary in obj.InnerBoundaries: area = area + boundary.Area obj.AreaWithHosted = area @classmethod def set_label(cls, obj: "RelSpaceBoundaryFeature") -> None: try: obj.Label = f"{obj.Id}_{obj.RelatedBuildingElement.IfcName}" except AttributeError: obj.Label = f"{obj.Id} VIRTUAL" if obj.PhysicalOrVirtualBoundary != "VIRTUAL": logger.warning( f"{obj.Id} is not VIRTUAL and has no RelatedBuildingElement" ) @staticmethod def get_wires(obj): return utils.get_wires(obj) class Element(Root): """Wrapping various IFC entity : https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/schema/ifcproductextension/lexical/ifcelement.htm """ def __init__(self, obj: "ElementFeature") -> None: super().__init__(obj) self.Type = "IfcElement" obj.Proxy = self @classmethod def create_from_ifc( cls, ifc_entity, ifc_importer: "IfcImporter" ) -> "ElementFeature": """Stantard FreeCAD FeaturePython Object creation method""" obj = super().create_from_ifc(ifc_entity, ifc_importer) ifc_importer.create_element_type( obj, ifcopenshell.util.element.get_type(ifc_entity) ) ifc_importer.material_creator.create(obj, ifc_entity) obj.Thickness = ifc_importer.guess_thickness(obj, ifc_entity) if FreeCAD.GuiUp: obj.ViewObject.Proxy = 0 return obj @classmethod def _init_properties(cls, obj: "ElementFeature") -> None: super()._init_properties(obj) ifc_attributes = "IFC Attributes" bem_category = "BEM" obj.addProperty("App::PropertyLink", "Material", ifc_attributes) obj.addProperty( "App::PropertyLinkListHidden", "ProvidesBoundaries", ifc_attributes ) obj.addProperty("App::PropertyLinkHidden", "IsTypedBy", ifc_attributes) obj.addProperty("App::PropertyFloat", "ThermalTransmittance", ifc_attributes) obj.addProperty("App::PropertyLinkList", "HostedElements", bem_category) obj.addProperty("App::PropertyLinkListHidden", "HostElements", bem_category) obj.addProperty("App::PropertyLength", "Thickness", bem_category) @classmethod def read_from_ifc(cls, obj, ifc_entity): super().read_from_ifc(obj, ifc_entity) obj.Label = f"{obj.Id}_{obj.IfcType}" super().read_pset_from_ifc( obj, ifc_entity, [ "ThermalTransmittance", ], ) class ElementType(Root): """Wrapping various IFC entity : https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/schema/ifcproductextension/lexical/ifcelement.htm """ def __init__(self, obj: "ElementFeature") -> None: super().__init__(obj) self.Type = "IfcElementType" obj.Proxy = self @classmethod def create_from_ifc( cls, ifc_entity, ifc_importer: "IfcImporter" ) -> "ElementFeature": """Stantard FreeCAD FeaturePython Object creation method""" obj = super().create_from_ifc(ifc_entity, ifc_importer) ifc_importer.material_creator.create(obj, ifc_entity) obj.Thickness = ifc_importer.guess_thickness(obj, ifc_entity) if FreeCAD.GuiUp: obj.ViewObject.Proxy = 0 return obj @classmethod def _init_properties(cls, obj: "ElementFeature") -> None: super()._init_properties(obj) ifc_attributes = "IFC Attributes" bem_category = "BEM" obj.addProperty("App::PropertyLink", "Material", ifc_attributes) obj.addProperty("App::PropertyFloat", "ThermalTransmittance", ifc_attributes) obj.addProperty("App::PropertyLinkList", "ApplicableOccurrence", ifc_attributes) obj.addProperty("App::PropertyLength", "Thickness", bem_category) @classmethod def read_from_ifc(cls, obj, ifc_entity): super().read_from_ifc(obj, ifc_entity) obj.Label = f"{obj.Id}_{obj.IfcType}" super().read_pset_from_ifc( obj, ifc_entity, [ "ThermalTransmittance", ], ) class BEMBoundary: def __init__( self, obj: "BEMBoundaryFeature", boundary: "RelSpaceBoundaryFeature" ) -> None: self.Type = "BEMBoundary" # pylint: disable=invalid-name obj.Proxy = self category_name = "BEM" obj.addProperty("App::PropertyLinkHidden", "SourceBoundary", category_name) obj.SourceBoundary = boundary obj.addProperty("App::PropertyArea", "Area", category_name) obj.addProperty("App::PropertyArea", "AreaWithHosted", category_name) obj.Shape = boundary.Shape.copy() self.set_label(obj, boundary) obj.Area = boundary.Area obj.AreaWithHosted = boundary.AreaWithHosted @staticmethod def create(boundary: "RelSpaceBoundaryFeature", geo_type) -> "BEMBoundaryFeature": """Stantard FreeCAD FeaturePython Object creation method""" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "BEMBoundary") BEMBoundary(obj, boundary) setattr(boundary, geo_type, obj) if FreeCAD.GuiUp: # ViewProviderRelSpaceBoundary(obj.ViewObject) obj.ViewObject.Proxy = 0 obj.ViewObject.ShapeColor = boundary.ViewObject.ShapeColor return obj @staticmethod def set_label(obj: "RelSpaceBoundaryFeature", source_boundary) -> None: obj.Label = source_boundary.Label @staticmethod def get_wires(obj): return utils.get_wires(obj) class Container(Root): """Representation of an IfcProject: https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcproject.htm""" def __init__(self, obj): super().__init__(obj) obj.Proxy = self @classmethod def create_from_ifc(cls, ifc_entity, ifc_importer: "IfcImporter"): obj = super().create_from_ifc(ifc_entity, ifc_importer) cls.set_label(obj) if FreeCAD.GuiUp: obj.ViewObject.DisplayMode = "Wireframe" return obj class Project(Root): """Representation of an IfcProject: https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcproject.htm""" @classmethod def create(cls) -> "ProjectFeature": obj = super().create() if FreeCAD.GuiUp: obj.ViewObject.DisplayMode = "Wireframe" return obj @classmethod def _init_properties(cls, obj: "ProjectFeature") -> None: super()._init_properties(obj) ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyString", "LongName", ifc_attributes) obj.addProperty("App::PropertyVector", "TrueNorth", ifc_attributes) obj.addProperty("App::PropertyVector", "WorldCoordinateSystem", ifc_attributes) owning_application = "OwningApplication" obj.addProperty( "App::PropertyString", "ApplicationIdentifier", owning_application ) obj.addProperty("App::PropertyString", "ApplicationVersion", owning_application) obj.addProperty( "App::PropertyString", "ApplicationFullName", owning_application ) @classmethod def read_from_ifc(cls, obj: "ProjectFeature", ifc_entity) -> None: super().read_from_ifc(obj, ifc_entity) obj.LongName = ifc_entity.LongName or "" true_north = ifc_entity.RepresentationContexts[0].TrueNorth obj.TrueNorth = ( FreeCAD.Vector(*true_north.DirectionRatios) if true_north else FreeCAD.Vector(0, 1) ) obj.WorldCoordinateSystem = FreeCAD.Vector( ifc_entity.RepresentationContexts[ 0 ].WorldCoordinateSystem.Location.Coordinates ) owning_application = ifc_entity.OwnerHistory.OwningApplication obj.ApplicationIdentifier = owning_application.ApplicationIdentifier obj.ApplicationVersion = owning_application.Version obj.ApplicationFullName = owning_application.ApplicationFullName @classmethod def set_label(cls, obj): obj.Label = f"{obj.IfcName}_{obj.LongName}" class Space(Root): """Representation of an IfcProject: https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcproject.htm""" @classmethod def create(cls) -> "SpaceFeature": obj = super().create() if FreeCAD.GuiUp: obj.ViewObject.ShapeColor = (0.33, 1.0, 1.0) obj.ViewObject.Transparency = 90 return obj @classmethod def _init_properties(cls, obj: "SpaceFeature") -> None: super()._init_properties(obj) ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyString", "LongName", ifc_attributes) category_name = "Boundaries" obj.addProperty("App::PropertyLink", "Boundaries", category_name) obj.addProperty("App::PropertyLink", "SecondLevel", category_name) obj.addProperty("App::PropertyLink", "SIA", category_name) obj.addProperty("App::PropertyLink", "SIA_Interiors", category_name) obj.addProperty("App::PropertyLink", "SIA_Exteriors", category_name) bem_category = "BEM" obj.addProperty("App::PropertyArea", "Area", bem_category) obj.addProperty("App::PropertyArea", "AreaAE", bem_category) @classmethod def read_from_ifc(cls, obj: "SpaceFeature", ifc_entity) -> None: super().read_from_ifc(obj, ifc_entity) ifc_importer = obj.Proxy.ifc_importer obj.Shape = ifc_importer.space_shape_by_brep(ifc_entity) obj.LongName = ifc_entity.LongName or "" space_full_name = f"{ifc_entity.Name} {ifc_entity.LongName}" obj.Label = space_full_name obj.Description = ifc_entity.Description or "" @classmethod def set_label(cls, obj: "SpaceFeature") -> None: obj.Label = f"{obj.IfcName}_{obj.LongName}" diff --git a/freecad/bem/materials.py b/freecad/bem/materials.py index 8ed2cab..75c00fa 100644 --- a/freecad/bem/materials.py +++ b/freecad/bem/materials.py @@ -1,427 +1,464 @@ # coding: utf8 """This module reads IfcRelSpaceBoundary from an IFC file and display them in FreeCAD © All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, Laboratory CNPA, 2019-2020 See the LICENSE.TXT file for more details. Author : Cyril Waechter """ import typing import ifcopenshell import FreeCAD from freecad.bem import utils if typing.TYPE_CHECKING: from freecad.bem.typing import ( # pylint: disable=no-name-in-module, import-error MaterialFeature, LayerSetFeature, ConstituentSetFeature, ) class MaterialCreator: def __init__(self, ifc_importer=None): self.obj = None + self.ifc_entity = None self.materials = {} self.material_layer_sets = {} self.material_constituent_sets = {} self.material_profile_sets = {} self.ifc_scale = 1 self.fc_scale = 1 if ifc_importer: self.ifc_scale = ifc_importer.ifc_scale self.fc_scale = ifc_importer.fc_scale self.ifc_importer = ifc_importer def create(self, obj, ifc_entity): self.obj = obj + self.ifc_entity = ifc_entity self.parse_material(ifc_entity) def parse_material(self, ifc_entity): self.parse_associations(ifc_entity) if self.obj.Material: return entity_type = ifcopenshell.util.element.get_type(ifc_entity) if entity_type: self.parse_associations(entity_type) if self.obj.Material: return # When an element is composed of multiple elements if ifc_entity.IsDecomposedBy: # TODO: See in a real case how to handle every part and not only the first ifc_entity = ifc_entity.IsDecomposedBy[0].RelatedObjects[0] self.parse_associations(ifc_entity) # Set placement except for object type if hasattr(ifc_entity, "ObjectPlacement"): self.obj.Placement = self.ifc_importer.get_global_placement( ifc_entity.ObjectPlacement ) def parse_associations(self, ifc_entity): for association in ifc_entity.HasAssociations: if association.is_a("IfcRelAssociatesMaterial"): material_select = association.RelatingMaterial if self.is_material_definition(material_select): self.assign_material(material_select) elif material_select.is_a("IfcMaterialLayerSetUsage"): self.create_layer_set_usage(material_select) elif material_select.is_a("IfcMaterialList"): self.create_constituent_set_from_material_list( material_select, ifc_entity ) else: raise NotImplementedError( f"{material_select.is_a()} not handled yet" ) @staticmethod def is_material_definition(material_select): """Material Definition is new to IFC4. So doing material_select.is_a("MaterialDefinition") in IFC2x3 would return False event if it is in IFC4 schema.""" valid_class = ( "IfcMaterial", "IfcMaterialLayer", "IfcMaterialLayerSet", "IfcMaterialProfile", "IfcMaterialProfileSet", "IfcMaterialConstituent", "IfcMaterialConstituentSet", ) return material_select.is_a() in valid_class def create_layer_set_usage(self, usage): self.assign_material(usage.ForLayerSet) self.obj.Material.LayerSetDirection = usage.LayerSetDirection self.obj.Material.DirectionSense = usage.DirectionSense def assign_material(self, material_select): if material_select.is_a("IfcMaterial"): self.obj.Material = self.create_single(material_select) elif material_select.is_a("IfcMaterialLayerSet"): self.obj.Material = self.create_layer_set(material_select) utils.append(self.obj.Material, "AssociatedTo", self.obj) elif material_select.is_a("IfcMaterialConstituentSet"): self.obj.Material = self.create_constituent_set(material_select) elif material_select.is_a("IfcMaterialProfileSet"): self.obj.Material = self.create_profile_set(material_select) else: raise NotImplementedError(f"{material_select.is_a()} not handled yet") def create_single(self, material): if material.Name not in self.materials: return self.create_new_single(material) return self.materials[material.Name] def create_new_single(self, material): fc_material = Material.create(material) self.materials[material.Name] = fc_material return fc_material def create_layer_set(self, layer_set): if layer_set.LayerSetName not in self.material_layer_sets: - fc_layer_set = LayerSet.create(layer_set) + fc_layer_set = LayerSet.create(layer_set, building_element=self.ifc_entity) layers = [] layers_thickness = [] for layer in layer_set.MaterialLayers: layers.append(self.create_single(layer.Material)) layers_thickness.append(layer.LayerThickness * self.ifc_scale) fc_layer_set.MaterialLayers = layers fc_layer_set.Thicknesses = layers_thickness if not fc_layer_set.TotalThickness: fc_layer_set.TotalThickness = sum(layers_thickness) * self.fc_scale self.material_layer_sets[fc_layer_set.IfcName] = fc_layer_set return fc_layer_set return self.material_layer_sets[layer_set.LayerSetName] def create_constituent_set(self, constituent_set): - if constituent_set.Name not in self.material_constituent_sets: - fc_constituent_set = ConstituentSet.create(constituent_set) - constituents = [] - constituents_fraction = [] - constituents_categories = [] - for constituent in constituent_set.MaterialConstituents: - constituents.append(self.create_single(constituent.Material)) - constituents_fraction.append(constituent.Fraction or 0) - constituents_categories.append(constituent.Category or "") - fc_constituent_set.MaterialConstituents = constituents - fc_constituent_set.Fractions = constituents_fraction - fc_constituent_set.Categories = constituents_categories - self.material_constituent_sets[ - fc_constituent_set.IfcName - ] = fc_constituent_set - return fc_constituent_set - return self.material_constituent_sets[constituent_set.Name] + if constituent_set.Name in self.material_constituent_sets: + return self.material_constituent_sets[constituent_set.Name] + # In MVD IFC4RV IfcMaterialLayerSet do not exist. Layer thicknesses are stored in a + # quantity set. See: https://standards.buildingsmart.org/MVD/RELEASE/IFC4/ADD2_TC1/RV1_2/HTML/link/ifcmaterialconstituent.htm + # Also see bsi forum: https://forums.buildingsmart.org/t/why-are-material-layer-sets-excluded-from-ifc4-reference-view-mvd/3638 + layers = {} + for rel_definition in getattr(self.ifc_entity, "IsDefinedBy", ()): + definition = rel_definition.RelatingPropertyDefinition + if not definition.is_a("IfcElementQuantity"): + continue + # ArchiCAD stores it in standard Qto eg. Qto_WallBaseQuantities + if definition.Name.endswith("BaseQuantities"): + for quantity in definition.Quantities: + if not quantity.is_a("IfcPhysicalComplexQuantity"): + continue + layers[quantity.Name] = ( + quantity.HasQuantities[0].LengthValue * self.ifc_scale + ) + if layers: + fc_layer_set = LayerSet.create( + constituent_set, building_element=self.ifc_entity + ) + thicknesses = [] + materiallayers = [] + for layer in constituent_set.MaterialConstituents: + thicknesses.append(layers[layer.Name]) + materiallayers.append(self.create_single(layer.Material)) + fc_layer_set.Thicknesses = thicknesses + fc_layer_set.TotalThickness = sum(thicknesses) * self.fc_scale + fc_layer_set.MaterialLayers = materiallayers + self.material_constituent_sets[fc_layer_set.IfcName] = fc_layer_set + return fc_layer_set + + # Constituent set which cannot be converted to layer sets eg. windows, complex walls + fc_constituent_set = ConstituentSet.create(constituent_set) + constituents = [] + constituents_fraction = [] + constituents_categories = [] + for constituent in constituent_set.MaterialConstituents: + constituents.append(self.create_single(constituent.Material)) + constituents_fraction.append(constituent.Fraction or 0) + constituents_categories.append(constituent.Category or "") + fc_constituent_set.MaterialConstituents = constituents + fc_constituent_set.Fractions = constituents_fraction + fc_constituent_set.Categories = constituents_categories + self.material_constituent_sets[fc_constituent_set.IfcName] = fc_constituent_set + return fc_constituent_set def create_constituent_set_from_material_list(self, material_list, ifc_element): constituent_set = ConstituentSet.create() constituent_set.IfcType = material_list.is_a() constituent_set.IfcName = self.get_type_name(ifc_element) constituent_set.Id = material_list.id() constituent_set.Label = f"{constituent_set.Id}_{constituent_set.IfcName}" materials = material_list.Materials constituent_set.Fractions = [1 / len(materials)] * len(materials) constituent_set.Categories = ["MaterialList"] * len(materials) material_constituents = list() for material in materials: material_constituents.append(self.create_single(material)) constituent_set.MaterialConstituents = material_constituents self.obj.Material = constituent_set def create_profile_set(self, profile_set): if profile_set.Name not in self.material_profile_sets: fc_profile_set = ProfileSet.create(profile_set) profiles = [] profiles_categories = [] for profile in profile_set.MaterialProfiles: profiles.append(self.create_single(profile.Material)) profiles_categories.append(profile.Category or "") fc_profile_set.MaterialProfiles = profiles fc_profile_set.Categories = profiles_categories self.material_profile_sets[fc_profile_set.IfcName] = fc_profile_set return fc_profile_set return self.material_profile_sets[profile_set.Name] def get_type_name(self, ifc_element): if ifc_element.is_a("IfcTypeObject"): return ifc_element.Name if ifc_element.ObjectType: return ifc_element.ObjectType for definition in ifc_element.IsDefinedBy: if definition.is_a("IfcRelDefinesByType"): return definition.RelatingType.Name class ConstituentSet: attributes = () psets_dict = {} parts_name = "Constituents" part_name = "Constituent" part_props = ("MaterialConstituents", "Fractions", "Categories") part_attribs = ("Id", "Fraction", "Category") def __init__(self, obj, ifc_entity): self.ifc_entity = ifc_entity self._init_properties(obj, ifc_entity) self.Type = "MaterialConstituentSet" # pylint: disable=invalid-name obj.Proxy = self @classmethod def create(cls, ifc_entity=None) -> "ConstituentSetFeature": """Stantard FreeCAD FeaturePython Object creation method ifc_entity : Optionnally provide a base entity. """ obj = FreeCAD.ActiveDocument.addObject( "Part::FeaturePython", "MaterialConstituentSet" ) ConstituentSet(obj, ifc_entity) return obj def _init_properties(self, obj: "ConstituentSetFeature", ifc_entity) -> None: obj.addProperty("App::PropertyString", "IfcType", "IFC") obj.addProperty("App::PropertyFloatList", "Fractions", "BEM") obj.addProperty("App::PropertyStringList", "Categories", "BEM") ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyInteger", "Id", ifc_attributes) obj.addProperty("App::PropertyString", "IfcName", ifc_attributes) obj.addProperty("App::PropertyString", "Description", ifc_attributes) obj.addProperty("App::PropertyLinkList", "MaterialConstituents", ifc_attributes) if not ifc_entity: return obj.Id = ifc_entity.id() obj.IfcName = ifc_entity.Name or "" obj.Description = ifc_entity.Description or "" obj.Label = f"{obj.Id}_{obj.IfcName}" obj.IfcType = ifc_entity.is_a() class LayerSet: attributes = ("TotalThickness",) psets_dict = {} parts_name = "Layers" part_name = "Layer" part_props = ("MaterialLayers", "Thicknesses") part_attribs = ("Id", "Thickness") - def __init__(self, obj: "LayerSetFeature", ifc_entity) -> None: + def __init__(self, obj: "LayerSetFeature", ifc_entity, building_element) -> None: self.ifc_entity = ifc_entity + self.building_element = building_element self._init_properties(obj, ifc_entity) self.materials = {} self.Type = "MaterialLayerSet" # pylint: disable=invalid-name obj.Proxy = self @classmethod - def create(cls, ifc_entity=None) -> "ConstituentSetFeature": + def create(cls, ifc_entity=None, building_element=None) -> "ConstituentSetFeature": """Stantard FreeCAD FeaturePython Object creation method ifc_entity : Optionnally provide a base entity. """ obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Material") - LayerSet(obj, ifc_entity) + LayerSet(obj, ifc_entity, building_element) return obj def _init_properties(self, obj: "LayerSetFeature", ifc_entity) -> None: obj.addProperty("App::PropertyString", "IfcType", "IFC") obj.addProperty("App::PropertyFloatList", "Thicknesses", "BEM") ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyInteger", "Id", ifc_attributes) obj.addProperty("App::PropertyLinkList", "MaterialLayers", ifc_attributes) obj.addProperty("App::PropertyString", "IfcName", ifc_attributes) obj.addProperty("App::PropertyString", "Description", ifc_attributes) obj.addProperty("App::PropertyLength", "TotalThickness", ifc_attributes) obj.addProperty( "App::PropertyEnumeration", "LayerSetDirection", ifc_attributes ).LayerSetDirection = ["AXIS1", "AXIS2", "AXIS3"] obj.addProperty( "App::PropertyEnumeration", "DirectionSense", ifc_attributes ).DirectionSense = ["POSITIVE", "NEGATIVE"] obj.addProperty("App::PropertyLinkListHidden", "AssociatedTo", ifc_attributes) if not ifc_entity: return obj.Id = ifc_entity.id() - obj.IfcName = ifc_entity.LayerSetName or "" - # Description is new to IFC4 so IFC2x3 raise attribute error - try: - obj.Description = ifc_entity.Description or "" - except AttributeError: - pass + if ifc_entity.is_a("IfcMaterialLayerSet"): + obj.IfcName = ifc_entity.LayerSetName or "" + elif ifc_entity.is_a("IfcMaterialConstituentSet"): # for MVD IFC4RV + obj.IfcName = ifc_entity.Name + # Description is new to IFC4 so IFC2x3 + obj.Description = getattr(ifc_entity, "Description", None) or "" obj.Label = f"{obj.Id}_{obj.IfcName}" obj.IfcType = ifc_entity.is_a() - if ifc_entity.is_a("IfcWall"): + building_element = self.building_element + if building_element.is_a("IfcWall") or building_element.is_a("IfcWallType"): obj.LayerSetDirection = "AXIS2" - elif ifc_entity.is_a("IfcSlab") or ifc_entity.is_a("IfcRoof"): - obj.LayerSetDirection = "AXIS3" + else: + for ifc_class in ["IfcSlab", "IfcSlabType", "IfcRoof", "IfcRoofType"]: + if building_element.is_a(ifc_class): + obj.LayerSetDirection = "AXIS3" + break obj.DirectionSense = "POSITIVE" class Material: attributes = ("Category",) psets_dict = { "Pset_MaterialCommon": ("MassDensity", "Porosity"), "Pset_MaterialOptical": ( "VisibleTransmittance", "SolarTransmittance", "ThermalIrTransmittance", "ThermalIrEmissivityBack", "ThermalIrEmissivityFront", ), "Pset_MaterialThermal": ( "SpecificHeatCapacity", "ThermalConductivity", ), } parts_name = "" part_name = "" part_props = () part_attribs = () def __init__(self, obj, ifc_entity=None): self.ifc_entity = ifc_entity self._init_properties(obj) self.Type = "Material" # pylint: disable=invalid-name obj.Proxy = self @classmethod def create(cls, ifc_entity=None) -> "MaterialFeature": """Stantard FreeCAD FeaturePython Object creation method ifc_entity : Optionnally provide a base entity. """ obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Material") cls(obj, ifc_entity) return obj def _init_properties(self, obj) -> None: obj.addProperty("App::PropertyString", "IfcType", "IFC") ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyInteger", "Id", ifc_attributes) obj.addProperty("App::PropertyString", "IfcName", ifc_attributes) obj.addProperty("App::PropertyString", "Description", ifc_attributes) obj.addProperty("App::PropertyString", "Category", ifc_attributes) pset_name = "Pset_MaterialCommon" obj.addProperty("App::PropertyFloat", "MassDensity", pset_name) obj.addProperty("App::PropertyFloat", "Porosity", pset_name) pset_name = "Pset_MaterialOptical" obj.addProperty("App::PropertyFloat", "VisibleTransmittance", pset_name) obj.addProperty("App::PropertyFloat", "SolarTransmittance", pset_name) obj.addProperty("App::PropertyFloat", "ThermalIrTransmittance", pset_name) obj.addProperty("App::PropertyFloat", "ThermalIrEmissivityBack", pset_name) obj.addProperty("App::PropertyFloat", "ThermalIrEmissivityFront", pset_name) pset_name = "Pset_MaterialThermal" obj.addProperty("App::PropertyFloat", "SpecificHeatCapacity", pset_name) obj.addProperty("App::PropertyFloat", "ThermalConductivity", pset_name) ifc_entity = self.ifc_entity if not ifc_entity: return obj.Id = ifc_entity.id() obj.IfcName = ifc_entity.Name obj.Label = f"{obj.Id}_{obj.IfcName}" # Description is new to IFC4 so IFC2x3 raise attribute error obj.Description = getattr(ifc_entity, "Description", "") or "" obj.IfcType = ifc_entity.is_a() for pset in getattr(ifc_entity, "HasProperties", ()): if pset.Name in self.psets_dict.keys(): for prop in pset.Properties: if prop.Name in self.psets_dict[pset.Name]: setattr(obj, prop.Name, prop.NominalValue.wrappedValue) class ProfileSet: attributes = () psets_dict = {} parts_name = "Profiles" part_name = "Profile" part_props = ("MaterialProfiles",) part_attribs = ( "Id", "Category", ) def __init__(self, obj, ifc_entity): self.ifc_entity = ifc_entity self._init_properties(obj, ifc_entity) self.Type = "ProfileSet" # pylint: disable=invalid-name obj.Proxy = self @classmethod def create(cls, ifc_entity=None) -> "ProfileSetFeature": """Stantard FreeCAD FeaturePython Object creation method ifc_entity : Optionnally provide a base entity. """ obj = FreeCAD.ActiveDocument.addObject( "Part::FeaturePython", "MaterialProfileSet" ) ProfileSet(obj, ifc_entity) return obj def _init_properties(self, obj: "ProfileSetFeature", ifc_entity) -> None: obj.addProperty("App::PropertyString", "IfcType", "IFC") obj.addProperty("App::PropertyStringList", "Categories", "BEM") ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyInteger", "Id", ifc_attributes) obj.addProperty("App::PropertyString", "IfcName", ifc_attributes) obj.addProperty("App::PropertyString", "Description", ifc_attributes) obj.addProperty("App::PropertyLinkList", "MaterialProfiles", ifc_attributes) if not ifc_entity: return obj.Id = ifc_entity.id() obj.IfcName = ifc_entity.Name or "" obj.Description = ifc_entity.Description or "" obj.Label = f"{obj.Id}_{obj.IfcName}" obj.IfcType = ifc_entity.is_a() def get_type(ifc_entity): if ifc_entity.ObjectType: return ifc_entity.ObjectType for definition in ifc_entity.IsDefinedBy: if definition.is_a("IfcRelDefineByType"): return definition.RelatingType.Name