diff --git a/IfcPython/read_boundaries.py b/IfcPython/read_boundaries.py index 2070b25..4869c4b 100644 --- a/IfcPython/read_boundaries.py +++ b/IfcPython/read_boundaries.py @@ -1,622 +1,801 @@ # coding: utf8 """This module reads IfcRelSpaceBoundary from an IFC file and display them in FreeCAD""" import os import itertools import ifcopenshell import ifcopenshell.geom import FreeCAD import FreeCADGui import Part 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_or_create_group(doc, "RelSpaceBoundary") group.addProperty("App::PropertyString", "ApplicationIdentifier") group.addProperty("App::PropertyString", "ApplicationVersion") + group.addProperty("App::PropertyString", "ApplicationFullName") group_2nd = get_or_create_group(doc, "SecondLevel") group.addObject(group_2nd) elements_group = get_or_create_group(doc, "Elements") ifc_file = ifcopenshell.open(ifc_path) owning_application = ifc_file.by_type("IfcApplication")[0] group.ApplicationIdentifier = owning_application.ApplicationIdentifier group.ApplicationVersion = owning_application.Version + group.ApplicationFullName = owning_application.ApplicationFullName # Generate elements (Door, Window, Wall, Slab etc…) without their geometry - for ifc_entity in ( - e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries - ): + ifc_elements = (e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries) + for ifc_entity in ifc_elements: elements_group.addObject(create_fc_object_from_entity(ifc_entity)) + # Associate Host / Hosted elements + associate_host_element(ifc_file, elements_group) + # Generate boundaries spaces = ifc_file.by_type("IfcSpace") for space in spaces: space_full_name = f"{space.Name} {space.LongName}" space_group = group_2nd.newObject( "App::DocumentObjectGroup", f"Space_{space.Name}" ) space_group.Label = space_full_name # 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(ifc_boundary) space_group.addObject(face) face.Placement = space_placement - element = get_related_element(doc, elements_group.Group, ifc_boundary) + element = get_related_element(doc, elements_group, ifc_boundary) if element: face.RelatedBuildingElement = element append(element, "ProvidesBoundaries", face) + # face.ParentBoundary = get_parent_boundary(doc, elements_group, ifc_boundary) face.RelatingSpace = space_group # Associate CorrespondingBoundary associate_corresponding_boundaries(group_2nd) - # Associate hosted elements + # Associate hosted elements an fill gaps for space_group in group_2nd.Group: fc_boundaries = space_group.Group - # Minimal number of boundary is 5 - 3 vertical faces, 2 horizontal faces + # 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"{space_group.Label} has less than 5 boundaries" # Find coplanar boundaries to identify hosted boundaries and fill gaps (2b) - for fc_boundary_1, fc_boundary_2 in itertools.combinations(fc_boundaries, 2): - if is_coplanar(fc_boundary_1, fc_boundary_2): - append(fc_boundary_1, "CoplanarWith", fc_boundary_2) - append(fc_boundary_2, "CoplanarWith", fc_boundary_1) - - # Associate hosted elements and fill gaps - for fc_boundary in fc_boundaries: - if fc_boundary.IsHosted: - # TODO : Handle cases where there is more than 1 coplanar boundary - if len(fc_boundary.CoplanarWith) == 1: - host_element = fc_boundary.CoplanarWith[0] - fc_boundary.ParentBoundary = host_element - append(host_element, "InnerBoundaries", fc_boundary) - continue - non_hosted_coplanar = [ - b for b in fc_boundary.CoplanarWith if not b.IsHosted - ] - # FIXME : Simplification which doesn't handle case where 2b touch a corner - if not non_hosted_coplanar: - continue - elif len(non_hosted_coplanar) == 1: - # TODO : Find closest vertices - # for vextex in fc_boundary.Shape.Vertexes: + associate_coplanar_boundaries(fc_boundaries) - # TODO : Move vertices at mid distance or create a 2b boundary - pass + # Associate hosted elements + associate_inner_boundaries(fc_boundaries) - # Find CorrespondingBoundary + # FIXME: Fill 2b gaps + # fill2b(fc_boundaries) # for space_group in group_2nd.Group: # find_coincident_in_space(space_group) # # Find coincident points # fc_boundaries = space_group.Group # for fc_boundary_1 in fc_boundaries: # for i, vertex_1 in enumerate(fc_boundary_1.Shape.Vertexes): # if fc_boundary_1.Proxy.coincident_boundaries[i]: # continue # fc_boundary_1.Proxy.coincident_boundaries[i] = find_coincident( # vertex_1.Point, fc_boundary_1, space_group # ) # fc_boundary_1.CoincidentBoundaries = ( # fc_boundary_1.Proxy.coincident_boundaries # ) - # create_geo_ext_boundaries(doc, group_2nd) - # create_geo_int_boundaries(doc, group_2nd) + create_geo_ext_boundaries(doc, group_2nd) + create_geo_int_boundaries(doc, group_2nd) doc.recompute() + +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: + host = get_element_by_guid(get_host_guid(ifc_entity), elements_group) + hosted = get_element_by_guid(ifc_entity.GlobalId, elements_group) + append(host, "HostedElements", hosted) + hosted.HostElement = host + + +def fill2b(fc_boundaries): + """Modify boundary to include area of type 2b between boundaries""" + for fc_boundary in fc_boundaries: + non_hosted_shared_element = [ + b for b in fc_boundary.ShareRelatedElementWith if not b.IsHosted + ] + # FIXME : Simplification which doesn't handle case where 2b touch a corner + # FIXME : Currently assuming 2b boundaries are bounded by 4 vertices + if not non_hosted_shared_element: + continue + elif len(non_hosted_shared_element) == 1: + # Find side by side corresponding edges + fc_boundary1 = fc_boundary + fc_boundary2 = non_hosted_shared_element[0] + edges1 = fc_boundary1.Shape.OuterWire.Edges + edges2 = fc_boundary2.Shape.OuterWire.Edges + min_distance = 100000 + closest_indexes = None + for (i, edge1), (j, edge2) in itertools.product( + enumerate(edges1), enumerate(edges2) + ): + if edge1.Length <= edge2.Length: + mid_point = edge1.CenterOfMass + line_segment = (v.Point for v in edge2.Vertexes) + else: + mid_point = edge2.CenterOfMass + line_segment = (v.Point for v in edge1.Vertexes) + distance = mid_point.distanceToLineSegment(*line_segment).Length + if distance < min_distance: + min_distance = distance + closest_indexes = (i, j) + i, j = closest_indexes + + # Compute centerline + vec1 = edges1[i].Vertexes[0].Point + vec2 = edges2[j].Vertexes[0].Point + + line_segment = (v.Point for v in edges2[j].Vertexes) + vec2 = vec2 + vec2.distanceToLineSegment(*line_segment) / 2 + mid_line = Part.Line(vec1, vec2) + + # Compute intersection on center line between edges + vxs1_len = len(fc_boundary1.Shape.Vertexes) + vxs2_len = len(fc_boundary2.Shape.Vertexes) + b1_v1 = mid_line.intersectCC(edges1[i - 1].Curve) + b1_v2 = mid_line.intersectCC(edges1[(i + 1) % vxs1_len].Curve) + b2_v1 = mid_line.intersectCC(edges2[j - 1].Curve) + b2_v2 = mid_line.intersectCC(edges2[(j + 1) % vxs2_len].Curve) + vectors = [v.Point for v in fc_boundary1.Shape.OuterWire.Vertexes] + vectors[i] = b1_v1 + vectors[(i + 1) % len(vectors)] = b1_v2 + vectors.append(vectors[0]) + wires = [Part.makePolygon(vectors)] + wires.extend(fc_boundary1.Shape.Wires[1:]) + new_shape = Part.Face(wires) + fc_boundary1.Shape = new_shape + + +def associate_inner_boundaries(fc_boundaries): + """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( + 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 + host_element = candidates.pop() + fc_boundary.ParentBoundary = host_element + append(host_element, "InnerBoundaries", fc_boundary) + + +def associate_coplanar_boundaries(fc_boundaries): + """ Find coplanar boundaries to identify hosted boundaries and fill gaps (2b) + FIXME: Apparently in ArchiCAD, doors are not coplanar. + Idea: Make it coplanar with other boundaries sharing the same space+wall before""" + for fc_boundary_1, fc_boundary_2 in itertools.combinations(fc_boundaries, 2): + if is_coplanar(fc_boundary_1, fc_boundary_2): + append(fc_boundary_1, "ShareRelatedElementWith", fc_boundary_2) + append(fc_boundary_2, "ShareRelatedElementWith", fc_boundary_1) + + def associate_corresponding_boundaries(group_2nd): # Associate CorrespondingBoundary for space_group in group_2nd.Group: for fc_boundary in space_group.Group: associate_corresponding_boundary(fc_boundary) def is_coplanar(shape_1, shape_2): """Intended for RelSpaceBoundary use only For some reason native Part.Shape.isCoplanar(Part.Shape) do not always work""" return get_plane(shape_1).toShape().isCoplanar(get_plane(shape_2).toShape()) def get_plane(fc_boundary): """Intended for RelSpaceBoundary use only""" return Part.Plane( fc_boundary.Shape.Vertexes[0].Point, fc_boundary.Shape.normalAt(0, 0) ) def append(doc_object, fc_property, value): """Intended to manipulate FreeCAD list like properties only""" current_value = getattr(doc_object, fc_property) current_value.append(value) setattr(doc_object, fc_property, current_value) def clean_corresponding_candidates(fc_boundary): other_boundaries = fc_boundary.RelatedBuildingElement.ProvidesBoundaries 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): """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) if len(other_boundaries) == 1: corresponding_boundary = other_boundaries[0] else: center_of_mass = fc_boundary.Shape.CenterOfMass min_lenght = 10000 # No element has 10 m for boundary in other_boundaries: distance = center_of_mass.distanceToPoint(boundary.Shape.CenterOfMass) if distance < min_lenght: min_lenght = distance corresponding_boundary = boundary try: fc_boundary.CorrespondingBoundary = corresponding_boundary corresponding_boundary.CorrespondingBoundary = fc_boundary except NameError: # TODO: What to do with uncorrectly classified boundaries which have no corresponding boundary - FreeCAD.Console.PrintLog(f"Boundary {fc_boundary.GlobalId} from space {fc_boundary}") + FreeCAD.Console.PrintLog( + f"Boundary {fc_boundary.GlobalId} from space {fc_boundary}" + ) return def find_coincident_in_space(space_group): fc_boundaries = space_group.Group for fc_boundary_1 in fc_boundaries: for i, vertex_1 in enumerate(fc_boundary_1.Shape.Vertexes): coincident_boundary = find_coincident(i, fc_boundary_1, fc_boundaries) fc_boundary_1.CoincidentBoundaries = coincident_boundary["boundary"] py_proxy.coincident_indexes[i] = coincident_boundary["index"] fc_boundary_1.CoincidentBoundaries = py_proxy.coincident_boundaries fc_boundary_1.CoincidentVertexIndexList = py_proxy.coincident_indexes def find_coincident(index_1, fc_boundary_1, fc_boundaries): point1 = fc_boundary_1.Shape.Vertexes[index_1].Point for fc_boundary_2 in (b for b in fc_boundaries if b != fc_boundary_1): for j, vertex_2 in enumerate(fc_boundary_2.Shape.Vertexes): py_proxy = fc_boundary_2.Proxy # Consider vector.isEqual(vertex.Point) if precision issue if point1.isEqual(vertex_2.Point, 1): py_proxy.coincident_boundaries[j] = fc_boundary_1 py_proxy.coincident_indexes[j] = index_1 return {"boundary": fc_boundary_2, "index": j} else: raise LookupError def get_related_element(doc, group, ifc_entity): + elements = group.Group if not ifc_entity.RelatedBuildingElement: return guid = ifc_entity.RelatedBuildingElement.GlobalId - for element in group: + for element in elements: if element.GlobalId == guid: return element -def get_wall_thickness(ifc_wall): - wall_thickness = 0 - for association in ifc_wall.HasAssociations: +def get_host_guid(ifc_entity): + return ( + ifc_entity.FillsVoids[0] + .RelatingOpeningElement.VoidsElements[0] + .RelatingBuildingElement.GlobalId + ) + + +def get_element_by_guid(guid, elements_group): + for fc_element in elements_group.Group: + if fc_element.GlobalId == guid: + return fc_element + else: + FreeCAD.Console.PrintLog(f"Unable to get element by {guid}") + + +def get_thickness(ifc_entity): + thickness = 0 + if ifc_entity.IsDecomposedBy: + ifc_entity = ifc_entity.IsDecomposedBy[0].RelatedObjects[0] + + for association in ifc_entity.HasAssociations: if not association.is_a("IfcRelAssociatesMaterial"): continue - for material_layer in association.RelatingMaterial.ForLayerSet.MaterialLayers: - wall_thickness += material_layer.LayerThickness - return wall_thickness + try: + material_layers = association.RelatingMaterial.ForLayerSet.MaterialLayers + except AttributeError: + material_layers = association.RelatingMaterial.MaterialLayers + for material_layer in material_layers: + thickness += material_layer.LayerThickness * SCALE + return 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" + """Create boundaries necessary for SIA calculations""" + bem_group = doc.addObject("App::DocumentObjectGroup", "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: + is_from_archicad = group_2nd.getParentGroup().ApplicationFullName == "ARCHICAD-64" + for space in group_2nd.Group: + for boundary in space.Group: + if boundary.IsHosted: + continue + bem_boundary = make_bem_boundary(boundary) + bem_group.addObject(bem_boundary) + thickness = boundary.RelatedBuildingElement.Thickness.Value + ifc_type = boundary.RelatedBuildingElement.IfcType + normal = boundary.Shape.normalAt(0, 0) + if is_from_archicad: + normal = -normal + if boundary.InternalOrExternalBoundary != "INTERNAL": + lenght = thickness + if is_from_revit and ifc_type.startswith("IfcWall"): lenght /= 2 - fc_boundary.Placement.move(fc_boundary.Shape.normalAt(0, 0) * lenght) + bem_boundary.Placement.move(normal * lenght) else: - lenght = wall_thickness / 2 - if is_from_revit: - continue - fc_boundary.Placement.move(fc_boundary.Shape.normalAt(0, 0) * lenght) + type1 = {"IfcSlab"} + if ifc_type in type1: + if normal.z > 0: + lenght = thickness + else: + continue + else: + if is_from_revit: + continue + lenght = thickness / 2 + bem_boundary.Placement.move(normal * 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" + """Create boundaries necessary for SIA calculations""" + bem_group = doc.addObject("App::DocumentObjectGroup", "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 - ) + is_from_archicad = group_2nd.getParentGroup().ApplicationFullName == "ARCHICAD-64" + for fc_space in group_2nd.Group: + for boundary in fc_space.Group: + if boundary.IsHosted: + continue + normal = boundary.Shape.normalAt(0, 0) + if is_from_archicad: + normal = -normal + + bem_boundary = make_bem_boundary(boundary) + bem_group.addObject(bem_boundary) + if is_from_revit: + thickness = boundary.RelatedBuildingElement.Thickness.Value + lenght = -thickness / 2 + bem_boundary.Placement.move(normal * 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_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 (0.9, 1.0, 0.9) + return (1.0, 0.0, 1.0) ifc_product = ifc_boundary.RelatedBuildingElement 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 else: print(f"No color found for {ifc_product.is_a()}") return (0.0, 0.0, 0.0) def get_or_create_group(doc, name): """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) def make_relspaceboundary(ifc_entity): """Stantard FreeCAD FeaturePython Object creation method""" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "RelSpaceBoundary") # ViewProviderRelSpaceBoundary(obj.ViewObject) RelSpaceBoundary(obj, ifc_entity) try: obj.ViewObject.Proxy = 0 except AttributeError: FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui") return obj class Root: """Wrapping various IFC entity : https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcroot.htm """ def __init__(self, obj, ifc_entity): self.Type = self.__class__.__name__ obj.Proxy = self ifc_attributes = "IFC Attributes" obj.addProperty("App::PropertyString", "IfcType", "IFC") obj.addProperty("App::PropertyString", "GlobalId", ifc_attributes) obj.addProperty("App::PropertyString", "Description", ifc_attributes) obj.GlobalId = ifc_entity.GlobalId obj.IfcType = ifc_entity.is_a() self.set_label(obj, ifc_entity) try: obj.Description = ifc_entity.Description except TypeError: pass def onChanged(self, obj, prop): """Do something when a property has changed""" return def execute(self, obj): """Do something when doing a recomputation, this method is mandatory""" return @staticmethod def set_label(obj, ifc_entity): """Allow specific method for specific elements""" obj.Label = "{} {}".format(ifc_entity.id(), ifc_entity.Name) @classmethod def create(cls, obj_name, ifc_entity=None): """Stantard FreeCAD FeaturePython Object creation method ifc_entity : Optionnally provide a base entity. """ obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name) feature_python_object = cls(obj) return obj class RelSpaceBoundary(Root): """Wrapping IFC entity :  https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary2ndlevel.htm""" def __init__(self, obj, ifc_entity): super().__init__(obj, ifc_entity) obj.Proxy = self category_name = "BEM" ifc_attributes = "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) + obj.addProperty("App::PropertyLink", "ProcessedShape", category_name) obj.addProperty("App::PropertyLinkList", "CoincidentBoundaries", category_name) - obj.addProperty("App::PropertyLinkList", "CoplanarWith", category_name) + obj.addProperty( + "App::PropertyLinkList", "ShareRelatedElementWith", category_name + ) obj.addProperty( "App::PropertyIntegerList", "CoincidentVertexIndexList", category_name ) obj.addProperty("App::PropertyBool", "IsHosted", category_name) obj.addProperty("App::PropertyArea", "Area", category_name) obj.addProperty("App::PropertyArea", "AreaWithHosted", category_name) obj.ViewObject.ShapeColor = get_color(ifc_entity) obj.GlobalId = ifc_entity.GlobalId obj.InternalOrExternalBoundary = ifc_entity.InternalOrExternalBoundary obj.PhysicalOrVirtualBoundary = ifc_entity.PhysicalOrVirtualBoundary obj.Shape = create_fc_shape(ifc_entity) obj.Area = obj.AreaWithHosted = obj.Shape.Area self.set_label(obj, ifc_entity) - if not obj.PhysicalOrVirtualBoundary == 'VIRTUAL': + if not obj.PhysicalOrVirtualBoundary == "VIRTUAL": obj.IsHosted = bool(ifc_entity.RelatedBuildingElement.FillsVoids) self.coincident_boundaries = self.coincident_indexes = [None] * len( obj.Shape.Vertexes ) self.coplanar_with = [] def onChanged(self, obj, prop): super().onChanged(obj, prop) if prop == "InnerBoundaries": obj.AreaWithHosted = self.recompute_area_with_hosted(obj) @staticmethod def recompute_area_with_hosted(obj): """Recompute area including inner boundaries""" area = obj.Area for boundary in obj.InnerBoundaries: area = area + boundary.Area return area @staticmethod def set_label(obj, ifc_entity): try: obj.Label = "{} {}".format( ifc_entity.RelatedBuildingElement.id(), ifc_entity.RelatedBuildingElement.Name, ) except AttributeError: - FreeCAD.Console.PrintLog(f"{ifc_entity.GlobalId} has no RelatedBuildingElement") + FreeCAD.Console.PrintLog( + f"{ifc_entity.GlobalId} has no RelatedBuildingElement" + ) return def create_fc_object_from_entity(ifc_entity): """Stantard FreeCAD FeaturePython Object creation method""" obj_name = "Element" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name) - # ViewProviderRelSpaceBoundary(obj.ViewObject) Element(obj, ifc_entity) try: obj.ViewObject.Proxy = 0 except AttributeError: FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui") return 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, ifc_entity): super().__init__(obj, ifc_entity) self.Type = "IfcRelSpaceBoundary" obj.Proxy = self ifc_attributes = "IFC Attributes" + bem_category = "BEM" obj.addProperty("App::PropertyLinkList", "HasAssociations", ifc_attributes) obj.addProperty("App::PropertyLinkList", "FillsVoids", ifc_attributes) obj.addProperty("App::PropertyLinkList", "HasOpenings", ifc_attributes) obj.addProperty("App::PropertyLinkList", "ProvidesBoundaries", ifc_attributes) + obj.addProperty("App::PropertyLinkList", "HostedElements", bem_category) + obj.addProperty("App::PropertyLink", "HostElement", bem_category) + obj.addProperty("App::PropertyLength", "Thickness", ifc_attributes) + + ifc_walled_entities = {"IfcWall", "IfcSlab", "IfcRoof"} + for entity_class in ifc_walled_entities: + if ifc_entity.is_a(entity_class): + obj.Thickness = get_thickness(ifc_entity) + + +def make_bem_boundary(boundary): + """Stantard FreeCAD FeaturePython Object creation method""" + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "BEMBoundary") + # ViewProviderRelSpaceBoundary(obj.ViewObject) + BEMBoundary(obj, boundary) + try: + obj.ViewObject.Proxy = 0 + obj.ViewObject.ShapeColor = boundary.ViewObject.ShapeColor + except AttributeError: + FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui") + return obj + + +class BEMBoundary: + def __init__(self, obj, boundary): + self.Type = "BEMBoundary" + category_name = "BEM" + obj.addProperty("App::PropertyLink", "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() + obj.Area = obj.Shape.Area + obj.AreaWithHosted = self.recompute_area_with_hosted(obj) + self.set_label(obj) + + @staticmethod + def recompute_area_with_hosted(obj): + """Recompute area including inner boundaries""" + area = obj.Area + for boundary in obj.SourceBoundary.InnerBoundaries: + area = area + boundary.Area + return area + + @staticmethod + def set_label(obj): + obj.Label = obj.SourceBoundary.Label if __name__ == "__main__": TEST_FOLDER = "/home/cyril/git/BIMxBEM/IfcTestFiles/" TEST_FILES = [ "Triangle_2x3_A22.ifc", "Triangle_2x3_R19.ifc", "2Storey_2x3_A22.ifc", "2Storey_2x3_R19.ifc", ] - IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[0]) + IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[1]) 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() class box: pass