diff --git a/IfcPython/read_boundaries.py b/IfcPython/read_boundaries.py new file mode 100644 index 0000000..a84d1c6 --- /dev/null +++ b/IfcPython/read_boundaries.py @@ -0,0 +1,387 @@ +# coding: utf8 +"""This module reads IfcRelSpaceBoundary from an IFC file and display them in FreeCAD""" +import os + +import ifcopenshell +import ifcopenshell.geom + +import FreeCAD +import FreeCADGui +import Part +import uuid + + +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) + +"""With IfcOpenShell 0.6.0a1 recreating face from wires seems to give more consistant results. +Especially when inner boundaries touch outer boundary""" +BREP = False + +# IfcOpenShell/IFC default unit is m, FreeCAD internal unit is mm +SCALE = 1000 + + +def display_boundaries(ifc_path, doc=FreeCAD.ActiveDocument): + """Display IfcRelSpaceBoundaries from selected IFC file into FreeCAD documennt""" + # Create default groups + group = get_group(doc, "RelSpaceBoundary") + group.addProperty("App::PropertyString", "ApplicationIdentifier") + group.addProperty("App::PropertyString", "ApplicationVersion") + group_2nd = get_group(doc, "SecondLevel") + group.addObject(group_2nd) + + ifc_file = ifcopenshell.open(ifc_path) + + owning_application = ifc_file.by_type("IfcApplication")[0] + group.ApplicationIdentifier = owning_application.ApplicationIdentifier + group.ApplicationVersion = owning_application.Version + + spaces = ifc_file.by_type("IfcSpace") + + for space in spaces: + space_full_name = f"{space.Name} {space.LongName}" + space_group = doc.addObject("App::DocumentObjectGroup", f"Space_{space.Name}") + space_group.Label = space_full_name + group_2nd.addObject(space_group) + + # All boundaries have their placement relative to space placement + space_placement = get_placement(space) + for ifc_boundary in (b for b in space.BoundedBy if b.Name == "2ndLevel"): + face = make_relspaceboundary("Face") + # face = doc.addObject("Part::Feature", "Face") + space_group.addObject(face) + face.Shape = create_fc_shape(ifc_boundary) + face.Placement = space_placement + face.Label = "{} {}".format( + ifc_boundary.RelatedBuildingElement.id(), + ifc_boundary.RelatedBuildingElement.Name, + ) + face.ViewObject.ShapeColor = get_color(ifc_boundary.RelatedBuildingElement) + face.GlobalId = ifc_boundary.GlobalId + face.InternalOrExternalBoundary = ifc_boundary.InternalOrExternalBoundary + face.PhysicalOrVirtualBoundary = ifc_boundary.PhysicalOrVirtualBoundary + # face.RelatedBuildingElement = get_or_create_wall(ifc_boundary.RelatedBuildingElement) + face.OriginalBoundary = face + + for space in spaces: + # Find inner boundaries + for ifc_boundary in (b for b in space.BoundedBy if b.Name == "2ndLevel"): + try: + if ifc_boundary.ParentBoundary: + pass + except AttributeError: + pass + + # create_geo_ext_boundaries(doc, group_2nd) + # create_geo_int_boundaries(doc, group_2nd) + + doc.recompute() + + +def get_or_create_wall(ifc_wall): + + return + + +def get_wall_thickness(ifc_wall): + wall_thickness = 0 + for association in ifc_wall.HasAssociations: + if not association.is_a("IfcRelAssociatesMaterial"): + continue + for material_layer in association.RelatingMaterial.ForLayerSet.MaterialLayers: + wall_thickness += material_layer.LayerThickness + return wall_thickness + + +def create_geo_ext_boundaries(doc, group_2nd): + group_geo_ext = doc.copyObject(group_2nd, True) # True = whith_dependencies + group_geo_ext.Label = "geoExt" + is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit" + for fc_space in group_geo_ext.Group: + for fc_boundary in fc_space.Group: + wall_thickness = 200 + if fc_boundary.InternalOrExternalBoundary != "INTERNAL": + lenght = wall_thickness + if is_from_revit: + lenght /= 2 + fc_boundary.Placement.move(fc_boundary.Shape.normalAt(0, 0) * lenght) + else: + lenght = wall_thickness / 2 + if is_from_revit: + continue + fc_boundary.Placement.move(fc_boundary.Shape.normalAt(0, 0) * lenght) + + +def create_geo_int_boundaries(doc, group_2nd): + group_geo_int = doc.copyObject(group_2nd, True) # True = whith_dependencies + group_geo_int.Label = "geoInt" + is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit" + for fc_space in group_geo_int.Group: + for fc_boundary in fc_space.Group: + if fc_boundary.InternalOrExternalBoundary != "INTERNAL": + if is_from_revit: + wall_thickness = 200 + lenght = -wall_thickness / 2 + fc_boundary.Placement.move( + fc_boundary.Shape.normalAt(0, 0) * lenght + ) + + +def create_fc_shape(space_boundary): + """ Create Part shape from ifc geometry""" + if BREP: + try: + return _part_by_brep( + space_boundary.ConnectionGeometry.SurfaceOnRelatingElement + ) + except RuntimeError: + print(f"Failed to generate brep from {space_boundary}") + fallback = True + if not BREP or fallback: + try: + return part_by_wires( + space_boundary.ConnectionGeometry.SurfaceOnRelatingElement + ) + except RuntimeError: + print(f"Failed to generate mesh from {space_boundary}") + return _part_by_mesh( + space_boundary.ConnectionGeometry.SurfaceOnRelatingElement + ) + + +def _part_by_brep(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.brep_data) + fc_shape.scale(SCALE) + return fc_shape + + +def _part_by_mesh(ifc_entity): + """ Create a Part Shape from mesh generated by ifcopenshell from ifc geometry""" + return Part.Face(_polygon_by_mesh(ifc_entity)) + + +def _polygon_by_mesh(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(SCALE, SCALE, SCALE) + for i in range(0, len(ifc_verts), 3) + ] + fc_verts = verts_clean(fc_verts) + + return Part.makePolygon(fc_verts) + + +def verts_clean(vertices): + """For some reason, vertices are not always clean and sometime a same vertex is repeated""" + new_verts = list() + for i in range(len(vertices) - 1): + if vertices[i] != vertices[i + 1]: + new_verts.append(vertices[i]) + new_verts.append(vertices[-1]) + return new_verts + + +def part_by_wires(ifc_entity): + """ Create a Part Shape from ifc geometry""" + boundaries = list() + boundaries.append(_polygon_by_mesh(ifc_entity.OuterBoundary)) + try: + inner_boundaries = ifc_entity.InnerBoundaries + for inner_boundary in tuple(inner_boundaries) if inner_boundaries else tuple(): + boundaries.append(_polygon_by_mesh(inner_boundary)) + except RuntimeError: + pass + fc_shape = Part.makeFace(boundaries, "Part::FaceMakerBullseye") + matrix = get_matrix(ifc_entity.BasisSurface.Position) + fc_shape = fc_shape.transformGeometry(matrix) + return fc_shape + + +def get_matrix(position): + """Transform position to FreeCAD.Matrix""" + location = FreeCAD.Vector(position.Location.Coordinates).scale(SCALE, SCALE, 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 get_placement(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] *= SCALE + m_l.extend(line) + return FreeCAD.Matrix(*m_l) + + +def get_color(ifc_product): + """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), + } + for product, color in product_colors.items(): + if ifc_product.is_a(product): + return color + else: + print(f"No color found for {ifc_product.is_a()}") + return (0.0, 0.0, 0.0) + + +def get_group(doc, name): + """Get group by name or create one if not found""" + group = doc.findObjects("App::DocumentObjectGroup", name) + if group: + return group[0] + else: + return doc.addObject("App::DocumentObjectGroup", name) + + +def make_relspaceboundary(obj_name, ifc_entity=None): + """Stantard FreeCAD FeaturePython Object creation method + ifc_entity : Optionnally provide a + """ + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name) + # ViewProviderRelSpaceBoundary(obj.ViewObject) + fpo = RelSpaceBoundary(obj) + obj.ViewObject.Proxy = 0 + return obj + + +class RelSpaceBoundary: + """Wrapping IFC entity :  + https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary2ndlevel.htm""" + + def __init__(self, obj): + self.Type = "IfcRelSpaceBoundary" + obj.Proxy = self + category_name = "BEM" + ifc_attributes = "IFC Attributes" + obj.addProperty("App::PropertyString", "GlobalId", ifc_attributes) + obj.addProperty("App::PropertyLink", "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::PropertyLink", "CorrespondingBoundary", ifc_attributes) + obj.addProperty("App::PropertyLink", "ParentBoundary", ifc_attributes) + obj.addProperty("App::PropertyLinkList", "InnerBoundaries", ifc_attributes) + obj.addProperty("App::PropertyLink", "OriginalBoundary", category_name) + + +class Wall: + """Wrapping IFC entity :  + https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/chapter-1.htm""" + + def __init__(self, obj): + self.Type = "IfcRelSpaceBoundary" + obj.Proxy = self + category_name = "BEM" + ifc_attributes = "IFC Attributes" + obj.addProperty("App::PropertyString", "GlobalId", ifc_attributes) + obj.addProperty("App::PropertyLink", "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::PropertyLink", "CorrespondingBoundary", ifc_attributes) + obj.addProperty("App::PropertyLink", "ParentBoundary", ifc_attributes) + obj.addProperty("App::PropertyLinkList", "InnerBoundaries", ifc_attributes) + obj.addProperty("App::PropertyLink", "OriginalBoundary", category_name) + + def onChanged(self, fp, prop): + """Do something when a property has changed""" + return + + def execute(self, fp): + """Do something when doing a recomputation, this method is mandatory""" + return + + +if __name__ == "__main__": + TEST_FOLDER = "/home/cyril/git/BIMxBEM/IfcTestFiles/" + TEST_FILES = [ + "Triangle_R19.ifc", + "Triangle_ACAD.ifc", + "2Storey_ACAD.ifc", + "2Storey_R19.ifc", + ] + IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[0]) + DOC = FreeCAD.ActiveDocument + if DOC: # Remote debugging + import ptvsd + + # Allow other computers to attach to ptvsd at this IP address and port. + ptvsd.enable_attach(address=("localhost", 5678), redirect_output=True) + # Pause the program until a remote debugger is attached + ptvsd.wait_for_attach() + # breakpoint() + + display_boundaries(ifc_path=IFC_PATH, doc=DOC) + FreeCADGui.activeView().viewIsometric() + FreeCADGui.SendMsgToActiveView("ViewFit") + else: + FreeCADGui.showMainWindow() + DOC = FreeCAD.newDocument() + + display_boundaries(ifc_path=IFC_PATH, doc=DOC) + + FreeCADGui.activeView().viewIsometric() + FreeCADGui.SendMsgToActiveView("ViewFit") + + FreeCADGui.exec_loop()