diff --git a/freecad/bem/entities.py b/freecad/bem/entities.py index 366c35a..991c5f5 100644 --- a/freecad/bem/entities.py +++ b/freecad/bem/entities.py @@ -1,511 +1,513 @@ # 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 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::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) + obj.Y_Axis = ifc_importer.get_global_y_axis(ifc_entity.ObjectPlacement) 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::PropertyLinkList", "FillsVoids", ifc_attributes) obj.addProperty("App::PropertyLinkList", "HasOpenings", 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::PropertyLinkHidden", "HostElement", bem_category) obj.addProperty("App::PropertyLength", "Thickness", bem_category) + obj.addProperty("App::PropertyVector", "Y_Axis", 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/ifc_importer.py b/freecad/bem/ifc_importer.py index f56a8be..8154e02 100644 --- a/freecad/bem/ifc_importer.py +++ b/freecad/bem/ifc_importer.py @@ -1,587 +1,602 @@ # 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.progress import Progress from freecad.bem.entities import ( RelSpaceBoundary, Element, ElementType, 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.element_types = dict() 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 Progress.set(1, "IfcImport_Elements", "") 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)) element_types_group = get_or_create_group("ElementTypes", doc) for element_type in utils.get_by_class(doc, ElementType): element_types_group.addObject(element_type) materials_group = get_or_create_group("Materials", doc) for material in get_materials(doc): materials_group.addObject(material) # Generate projects structure and boundaries Progress.set(5, "IfcImport_StructureAndBoundaries", "") for ifc_project in ifc_file.by_type("IfcProject"): project = Project.create_from_ifc(ifc_project, self) self.generate_containers(ifc_project, project) Progress.set(15, "IfcImporter_EnrichingDatas", "") # Associate CorrespondingBoundary associate_corresponding_boundaries(doc) # Associate Host / Hosted elements associate_host_element(ifc_file, elements_group) # Associate hosted elements for i, fc_space in enumerate(get_elements_by_ifctype("IfcSpace", doc), 1): Progress.set(15, "IfcImporter_EnrichingDatas", f"{i}") 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) Progress.len_spaces = i 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 getattr(ifc_entity, "Representation", None): 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 ) fc_boundary.RelatingSpace = fc_space 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_1 = FreeCAD.Vector( + getattr(position.RefDirection, "DirectionRatios", (1, 0, 0)) + ) + v_3 = FreeCAD.Vector(getattr(position.Axis, "DirectionRatios", (0, 0, 1))) 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 get_global_placement(self, obj_placement) -> FreeCAD.Matrix: + if not obj_placement: + return FreeCAD.Matrix() + if not obj_placement.PlacementRelTo: + parent = FreeCAD.Matrix() + else: + parent = self.get_global_placement(obj_placement.PlacementRelTo) + return parent.multiply(self.get_matrix(obj_placement.RelativePlacement)) + + def get_global_y_axis(self, ifc_entity): + global_placement = self.get_global_placement(ifc_entity) + return FreeCAD.Vector(global_placement.A[1:12:4]) + 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 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 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) def create_element_type(self, fc_element, ifc_entity_type): if not ifc_entity_type: return try: fc_element_type = self.element_types[ifc_entity_type.id()] except KeyError: fc_element_type = ElementType.create_from_ifc(ifc_entity_type, self) self.element_types[fc_element_type.Id] = fc_element_type fc_element.IsTypedBy = fc_element_type utils.append(fc_element_type, "ApplicableOccurrence", fc_element) 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 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( getattr( fc_boundary.RelatedBuildingElement.HostElement, "ProvidesBoundaries", () ) ) # 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: # Common issue with both ArchiCAD and Revit logger.info( f"RelSpaceBoundary Id<{fc_boundary.Id}> is hosted but host not found." ) continue fc_boundary.ParentBoundary = host_element 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 seems_too_smal(boundary) -> bool: """considered as too small if width or heigth < 100 mm""" try: uv_nodes = boundary.Shape.Faces[0].getUVNodes() return min(abs(n_2 - n_1) for n_1, n_2 in zip(uv_nodes[0], uv_nodes[2])) < 100 except RuntimeError: # TODO: further investigation to see why it happens if boundary.Shape.Faces[0].Area < 10000: # 0.01 m² return True else: return False def associate_corresponding_boundary(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 ( boundary.InternalOrExternalBoundary != "INTERNAL" or boundary.CorrespondingBoundary ): return corresponding_boundary = None other_boundaries = clean_corresponding_candidates(boundary, doc) if len(other_boundaries) == 1: corresponding_boundary = other_boundaries[0] else: center_of_mass = utils.get_outer_wire(boundary).CenterOfMass min_lenght = 10000 # No element has 10 m thickness for candidate in other_boundaries: distance = center_of_mass.distanceToPoint( utils.get_outer_wire(candidate).CenterOfMass ) if distance < min_lenght: min_lenght = distance corresponding_boundary = candidate if corresponding_boundary: boundary.CorrespondingBoundary = corresponding_boundary corresponding_boundary.CorrespondingBoundary = boundary elif boundary.PhysicalOrVirtualBoundary == "VIRTUAL" and seems_too_smal(boundary): logger.warning( f""" Boundary {boundary.Label} from space {boundary.RelatingSpace.Id} has been removed. It is VIRTUAL, INTERNAL, thin and has no corresponding boundary. It looks like a parasite.""" ) doc.removeObject(boundary.Name) else: # Considering test above. Assume that it has been missclassified but log the issue. boundary.InternalOrExternalBoundary = "EXTERNAL" logger.warning( f""" No corresponding boundary found for {boundary.Label} from space {boundary.RelatingSpace.Id}. Assigning to EXTERNAL assuming it was missclassified as INTERNAL""" ) 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