diff --git a/freecad/bem/ifc_importer.py b/freecad/bem/ifc_importer.py index 4b0693c..c3a6043 100644 --- a/freecad/bem/ifc_importer.py +++ b/freecad/bem/ifc_importer.py @@ -1,521 +1,540 @@ # 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 """ from typing import NamedTuple, Generator import os import zipfile import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.element import ifcopenshell.util.unit import FreeCAD import Part from freecad.bem import materials from freecad.bem import utils from freecad.bem.bem_logging import logger from freecad.bem.entities import ( RelSpaceBoundary, Element, Container, Space, Project, ) def ios_settings(brep): """Create ifcopenshell.geom.settings for various cases""" settings = ifcopenshell.geom.settings() settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False) settings.set(settings.INCLUDE_CURVES, True) if brep: settings.set(settings.USE_BREP_DATA, True) return settings BREP_SETTINGS = ios_settings(brep=True) MESH_SETTINGS = ios_settings(brep=False) TOLERANCE = 0.001 """With IfcOpenShell 0.6.0a1 recreating face from wires seems to give more consistant results. Especially when inner boundaries touch outer boundary""" BREP = False def get_by_class(doc=FreeCAD.ActiveDocument, by_class=object): """Generator throught FreeCAD document element of specific python proxy class""" for element in doc.Objects: try: if isinstance(element.Proxy, by_class): yield element except AttributeError: continue def get_elements_by_ifctype( ifc_type: str, doc=FreeCAD.ActiveDocument ) -> Generator[Part.Feature, None, None]: """Generator throught FreeCAD document element of specific ifc_type""" for element in doc.Objects: try: if element.IfcType == ifc_type: yield element except (AttributeError, ReferenceError): continue +def get_materials(doc=FreeCAD.ActiveDocument): + """Generator throught FreeCAD document element of specific python proxy class""" + for element in doc.Objects: + try: + if element.IfcType in ( + "IfcMaterial", + "IfcMaterialList", + "IfcMaterialLayerSet", + "IfcMaterialLayerSetUsage", + "IfcMaterialConstituentSet", + "IfcMaterialConstituent", + ): + yield element + except AttributeError: + continue + + def get_unit_conversion_factor(ifc_file, unit_type, default=None): # TODO: Test with Imperial units units = [ u for u in ifc_file.by_type("IfcUnitAssignment")[0][0] if getattr(u, "UnitType", None) == unit_type ] if len(units) == 0: return default ifc_unit = units[0] unit_factor = 1.0 if ifc_unit.is_a("IfcConversionBasedUnit"): ifc_unit = ifc_unit.ConversionFactor unit_factor = ifc_unit.wrappedValue assert ifc_unit.is_a("IfcSIUnit") prefix_factor = ifcopenshell.util.unit.get_prefix_multiplier(ifc_unit.Prefix) return unit_factor * prefix_factor class IfcImporter: def __init__(self, ifc_path, doc=None): if not doc: doc = FreeCAD.newDocument() self.doc = doc self.ifc_file = self.open(ifc_path) self.ifc_scale = get_unit_conversion_factor(self.ifc_file, "LENGTHUNIT") self.fc_scale = FreeCAD.Units.Metre.Value self.material_creator = materials.MaterialCreator(self) self.xml: str = "" self.log: str = "" @staticmethod def open(ifc_path: str) -> ifcopenshell.file: ext = os.path.splitext(ifc_path)[1].lower() if ext == ".ifc": return ifcopenshell.open(ifc_path) if ext == ".ifcxml": # TODO: How to do this as ifcopenshell.ifcopenshell_wrapper has no parse_ifcxml ? raise NotImplementedError("No support for .ifcXML yet") if ext in (".ifczip", ".zip"): zip_path = zipfile.Path(ifc_path) for member in zip_path.iterdir(): zipped_ext = os.path.splitext(member.name)[1].lower() if zipped_ext == ".ifc": return ifcopenshell.file.from_string(member.read_text()) if zipped_ext == ".ifcxml": # TODO: How to do this as ifcopenshell.ifcopenshell_wrapper has no parse_ifcxml ? raise NotImplementedError("No support for .ifcXML yet") raise NotImplementedError( """Supported files : - unzipped : *.ifc | *.ifcXML - zipped : *.ifczip | *.zip containing un unzipped type""" ) def generate_rel_space_boundaries(self): """Display IfcRelSpaceBoundaries from selected IFC file into FreeCAD documennt""" ifc_file = self.ifc_file doc = self.doc # Generate elements (Door, Window, Wall, Slab etc…) without their geometry elements_group = get_or_create_group("Elements", doc) ifc_elements = ( e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries ) for ifc_entity in ifc_elements: elements_group.addObject(Element.create_from_ifc(ifc_entity, self)) - + materials_group = get_or_create_group("Materials", doc) + for material in get_materials(doc): + materials_group.addObject(material) # Generate projects structure and boundaries for ifc_project in ifc_file.by_type("IfcProject"): project = Project.create_from_ifc(ifc_project, self) self.generate_containers(ifc_project, project) # Associate CorrespondingBoundary associate_corresponding_boundaries(doc) # Associate Host / Hosted elements associate_host_element(ifc_file, elements_group) # Associate hosted elements an fill gaps for fc_space in get_elements_by_ifctype("IfcSpace", doc): fc_boundaries = fc_space.SecondLevel.Group # Minimal number of boundary is 5: 3 vertical faces, 2 horizontal faces # If there is less than 5 boundaries there is an issue or a new case to analyse if len(fc_boundaries) == 5: continue elif len(fc_boundaries) < 5: assert ValueError, f"{fc_space.Label} has less than 5 boundaries" # Associate hosted elements associate_inner_boundaries(fc_boundaries, doc) def guess_thickness(self, obj, ifc_entity): if obj.Material: thickness = getattr(obj.Material, "TotalThickness", 0) if thickness: return thickness if ifc_entity.is_a("IfcWall"): qto_lookup_name = "Qto_WallBaseQuantities" elif ifc_entity.is_a("IfcSlab"): qto_lookup_name = "Qto_SlabBaseQuantities" else: qto_lookup_name = "" if qto_lookup_name: for definition in ifc_entity.IsDefinedBy: if not definition.is_a("IfcRelDefinesByProperties"): continue if definition.RelatingPropertyDefinition.Name == qto_lookup_name: for quantity in definition.RelatingPropertyDefinition.Quantities: if quantity.Name == "Width": return quantity.LengthValue * self.fc_scale * self.ifc_scale if not ifc_entity.Representation: return 0 if ifc_entity.IsDecomposedBy: thicknesses = [] for aggregate in ifc_entity.IsDecomposedBy: thickness = 0 for related in aggregate.RelatedObjects: thickness += self.guess_thickness(obj, related) thicknesses.append(thickness) return max(thicknesses) for representation in ifc_entity.Representation.Representations: if ( representation.RepresentationIdentifier == "Box" and representation.RepresentationType == "BoundingBox" ): if self.is_wall_like(obj.IfcType): return representation.Items[0].YDim * self.fc_scale * self.ifc_scale elif self.is_slab_like(obj.IfcType): return representation.Items[0].ZDim * self.fc_scale * self.ifc_scale else: return 0 bbox = self.element_local_shape_by_brep(ifc_entity).BoundBox # Returning bbox thickness for windows or doors is not insteresting # as it does not return frame thickness. if self.is_wall_like(obj.IfcType): return min(bbox.YLength, bbox.XLength) elif self.is_slab_like(obj.IfcType): return bbox.ZLength return 0 @staticmethod def is_wall_like(ifc_type): return ifc_type in ("IfcWall", "IfcWallStandardCase", "IfcCurtainWall") @staticmethod def is_slab_like(ifc_type): return ifc_type in ("IfcSlab", "IfcSlabStandardCase", "IfcRoof") def generate_containers(self, ifc_parent, fc_parent): for rel_aggregates in ifc_parent.IsDecomposedBy: for element in rel_aggregates.RelatedObjects: if element.is_a("IfcSpace"): if element.BoundedBy: self.generate_space(element, fc_parent) else: if element.is_a("IfcSite"): self.workaround_site_coordinates(element) fc_container = Container.create_from_ifc(element, self) fc_parent.addObject(fc_container) self.generate_containers(element, fc_container) def workaround_site_coordinates(self, ifc_site): """Multiple softwares (eg. Revit) are storing World Coordinate system in IfcSite location instead of using IfcProject IfcGeometricRepresentationContext. This is a bad practice should be solved over time""" ifc_location = ifc_site.ObjectPlacement.RelativePlacement.Location fc_location = FreeCAD.Vector(ifc_location.Coordinates) fc_location.scale(*[self.ifc_scale * self.fc_scale] * 3) if not fc_location.Length > 1000000: # 1 km return for project in get_by_class(self.doc, Project): project.WorldCoordinateSystem += fc_location ifc_location.Coordinates = ( 0.0, 0.0, 0.0, ) def generate_space(self, ifc_space, parent): """Generate Space and RelSpaceBoundaries as defined in ifc_file. No post process.""" fc_space = Space.create_from_ifc(ifc_space, self) parent.addObject(fc_space) boundaries = fc_space.newObject("App::DocumentObjectGroup", "Boundaries") fc_space.Boundaries = boundaries second_levels = boundaries.newObject("App::DocumentObjectGroup", "SecondLevel") fc_space.SecondLevel = second_levels # All boundaries have their placement relative to space placement space_placement = self.get_placement(ifc_space) for ifc_boundary in (b for b in ifc_space.BoundedBy if b.Name == "2ndLevel"): try: fc_boundary = RelSpaceBoundary.create_from_ifc( ifc_entity=ifc_boundary, ifc_importer=self ) second_levels.addObject(fc_boundary) fc_boundary.Placement = space_placement except utils.ShapeCreationError: logger.warning( f"Failed to create fc_shape for RelSpaceBoundary <{ifc_boundary.id()}> even with fallback methode _part_by_mesh. IfcOpenShell bug ?" ) except utils.IsTooSmall: logger.warning( f"Boundary <{ifc_boundary.id()}> shape is too small and has been ignored" ) def get_placement(self, space): """Retrieve object placement""" space_geom = ifcopenshell.geom.create_shape(BREP_SETTINGS, space) # IfcOpenShell matrix values FreeCAD matrix values are transposed ios_matrix = space_geom.transformation.matrix.data m_l = list() for i in range(3): line = list(ios_matrix[i::3]) line[-1] *= self.fc_scale m_l.extend(line) return FreeCAD.Matrix(*m_l) def get_matrix(self, position): """Transform position to FreeCAD.Matrix""" total_scale = self.fc_scale * self.ifc_scale location = FreeCAD.Vector(position.Location.Coordinates) location.scale(*list(3 * [total_scale])) v_1 = FreeCAD.Vector(position.RefDirection.DirectionRatios) v_3 = FreeCAD.Vector(position.Axis.DirectionRatios) v_2 = v_3.cross(v_1) # fmt: off matrix = FreeCAD.Matrix( v_1.x, v_2.x, v_3.x, location.x, v_1.y, v_2.y, v_3.y, location.y, v_1.z, v_2.z, v_3.z, location.z, 0, 0, 0, 1, ) # fmt: on return matrix def create_fc_shape(self, ifc_boundary): """ Create Part shape from ifc geometry""" if BREP: try: return self._boundary_shape_by_brep( ifc_boundary.ConnectionGeometry.SurfaceOnRelatingElement ) except RuntimeError: print(f"Failed to generate brep from {ifc_boundary}") fallback = True if not BREP or fallback: try: return self.part_by_wires( ifc_boundary.ConnectionGeometry.SurfaceOnRelatingElement ) except RuntimeError: print(f"Failed to generate mesh from {ifc_boundary}") try: return self._part_by_mesh( ifc_boundary.ConnectionGeometry.SurfaceOnRelatingElement ) except RuntimeError: raise utils.ShapeCreationError def part_by_wires(self, ifc_entity): """ Create a Part Shape from ifc geometry""" inner_wires = list() outer_wire = self._polygon_by_mesh(ifc_entity.OuterBoundary) face = Part.Face(outer_wire) try: inner_boundaries = ifc_entity.InnerBoundaries or tuple() for inner_boundary in inner_boundaries: inner_wire = self._polygon_by_mesh(inner_boundary) face = face.cut(Part.Face(inner_wire)) inner_wires.append(inner_wire) except RuntimeError: pass fc_shape = Part.Compound([face, outer_wire, *inner_wires]) matrix = self.get_matrix(ifc_entity.BasisSurface.Position) fc_shape = fc_shape.transformGeometry(matrix) return fc_shape def _boundary_shape_by_brep(self, ifc_entity): """ Create a Part Shape from brep generated by ifcopenshell from ifc geometry""" ifc_shape = ifcopenshell.geom.create_shape(BREP_SETTINGS, ifc_entity) fc_shape = Part.Shape() fc_shape.importBrepFromString(ifc_shape.geometry.brep_data) fc_shape.scale(self.fc_scale) return fc_shape def element_local_shape_by_brep(self, ifc_entity) -> Part.Shape: - """ Create a Part Shape from brep generated by ifcopenshell from ifc geometry""" + """ Create a Element Shape from brep generated by ifcopenshell from ifc geometry""" settings = ifcopenshell.geom.settings() settings.set(settings.USE_BREP_DATA, True) settings.set(settings.USE_WORLD_COORDS, False) ifc_shape = ifcopenshell.geom.create_shape(settings, ifc_entity) fc_shape = Part.Shape() fc_shape.importBrepFromString(ifc_shape.geometry.brep_data) fc_shape.scale(self.fc_scale) return fc_shape def space_shape_by_brep(self, ifc_entity) -> Part.Shape: - """ Create a Part Shape from brep generated by ifcopenshell from ifc geometry""" + """ Create a Space Shape from brep generated by ifcopenshell from ifc geometry""" settings = ifcopenshell.geom.settings() settings.set(settings.USE_BREP_DATA, True) settings.set(settings.USE_WORLD_COORDS, True) ifc_shape = ifcopenshell.geom.create_shape(settings, ifc_entity) fc_shape = Part.Shape() fc_shape.importBrepFromString(ifc_shape.geometry.brep_data) fc_shape.scale(self.fc_scale) return fc_shape def _part_by_mesh(self, ifc_entity): """ Create a Part Shape from mesh generated by ifcopenshell from ifc geometry""" return Part.Face(self._polygon_by_mesh(ifc_entity)) def _polygon_by_mesh(self, ifc_entity): """Create a Polygon from a compatible ifc entity""" ifc_shape = ifcopenshell.geom.create_shape(MESH_SETTINGS, ifc_entity) ifc_verts = ifc_shape.verts fc_verts = [ FreeCAD.Vector(ifc_verts[i : i + 3]).scale(*[self.fc_scale] * 3) for i in range(0, len(ifc_verts), 3) ] utils.clean_vectors(fc_verts) utils.close_vectors(fc_verts) return Part.makePolygon(fc_verts) class CommonSegment(NamedTuple): index1: int index2: int opposite_dir: FreeCAD.Vector def associate_host_element(ifc_file, elements_group): # Associate Host / Hosted elements ifc_elements = (e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries) for ifc_entity in ifc_elements: if ifc_entity.FillsVoids: try: host = utils.get_element_by_guid( utils.get_host_guid(ifc_entity), elements_group ) except LookupError as err: logger.exception(err) continue hosted = utils.get_element_by_guid(ifc_entity.GlobalId, elements_group) utils.append(host, "HostedElements", hosted) hosted.HostElement = host.Id def associate_inner_boundaries(fc_boundaries, doc): """Associate hosted elements like a window or a door in a wall""" for fc_boundary in fc_boundaries: if not fc_boundary.IsHosted: continue candidates = set(fc_boundaries).intersection( utils.get_boundaries_by_element_id( fc_boundary.RelatedBuildingElement.HostElement, doc ) ) # If there is more than 1 candidate it doesn't really matter # as they share the same host element and space try: host_element = candidates.pop() except KeyError: logger.warning( f"RelSpaceBoundary Id<{fc_boundary.Id}> is hosted but host not found. Investigations required." ) continue fc_boundary.ParentBoundary = host_element.Id utils.append(host_element, "InnerBoundaries", fc_boundary) def associate_corresponding_boundaries(doc=FreeCAD.ActiveDocument): # Associate CorrespondingBoundary for fc_boundary in get_elements_by_ifctype("IfcRelSpaceBoundary", doc): associate_corresponding_boundary(fc_boundary, doc) def clean_corresponding_candidates(fc_boundary, doc): other_boundaries = utils.get_boundaries_by_element( fc_boundary.RelatedBuildingElement, doc ) other_boundaries.remove(fc_boundary) return [ b for b in other_boundaries if not b.CorrespondingBoundary or b.RelatingSpace != fc_boundary.RelatingSpace ] def associate_corresponding_boundary(fc_boundary, doc): """Associate corresponding boundaries according to IFC definition. Reference to the other space boundary of the pair of two space boundaries on either side of a space separating thermal boundary element. https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary2ndlevel.htm """ if ( fc_boundary.InternalOrExternalBoundary != "INTERNAL" or fc_boundary.CorrespondingBoundary ): return other_boundaries = clean_corresponding_candidates(fc_boundary, doc) if len(other_boundaries) == 1: corresponding_boundary = other_boundaries[0] else: center_of_mass = utils.get_outer_wire(fc_boundary).CenterOfMass min_lenght = 10000 # No element has 10 m thickness for boundary in other_boundaries: distance = center_of_mass.distanceToPoint( utils.get_outer_wire(boundary).CenterOfMass ) if distance < min_lenght: min_lenght = distance corresponding_boundary = boundary try: fc_boundary.CorrespondingBoundary = corresponding_boundary corresponding_boundary.CorrespondingBoundary = fc_boundary except NameError: # Considering test above. Assume that it has been missclassified but log the issue. fc_boundary.InternalOrExternalBoundary = "EXTERNAL" logger.warning(f"Boundary {fc_boundary.GlobalId} from space {fc_boundary}") return def get_or_create_group(name, doc=FreeCAD.ActiveDocument): """Get group by name or create one if not found""" group = doc.findObjects("App::DocumentObjectGroup", name) if group: return group[0] return doc.addObject("App::DocumentObjectGroup", name) if __name__ == "__main__": pass diff --git a/freecad/bem/materials.py b/freecad/bem/materials.py index dc9e482..af45f0e 100644 --- a/freecad/bem/materials.py +++ b/freecad/bem/materials.py @@ -1,326 +1,332 @@ # 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 FreeCAD 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.materials = {} self.material_layer_sets = {} self.material_constituent_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.parse_material(ifc_entity) def parse_material(self, ifc_entity): self.parse_associations(ifc_entity) 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) 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) 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) elif material_select.is_a("IfcMaterialConstituentSet"): self.obj.Material = self.create_constituent_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) 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] def create_constituent_set_from_material_list(self, material_list, ifc_element): constituent_set = ConstituentSet.create() 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 def get_type_name(self, ifc_element): 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 = "Layers" part_name = "Layer" 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: self.ifc_entity = ifc_entity 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": """Stantard FreeCAD FeaturePython Object creation method ifc_entity : Optionnally provide a base entity. """ obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Material") LayerSet(obj, ifc_entity) 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) 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 obj.Label = f"{obj.Id}_{obj.IfcName}" + obj.IfcType = ifc_entity.is_a() 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) 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