diff --git a/freecad/bem/bem_xml.py b/freecad/bem/bem_xml.py index e455886..e7bc4a9 100644 --- a/freecad/bem/bem_xml.py +++ b/freecad/bem/bem_xml.py @@ -1,244 +1,247 @@ # coding: utf8 """This module write boundaries informations to an xml format for BEM software use © 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 xml.etree.ElementTree as ET +import typing import FreeCAD +if typing.TYPE_CHECKING: + from freecad.bem.entities import * + SCALE = 1000 class BEMxml: """Contains methods to write each kind of object to BEMxml""" def __init__(self): self.root = ET.Element("bimbem") self.tree = ET.ElementTree(self.root) self.projects = ET.SubElement(self.root, "Projects") self.spaces = ET.SubElement(self.root, "Spaces") self.boundaries = ET.SubElement(self.root, "Boundaries") self.building_elements = ET.SubElement(self.root, "BuildingElements") self.materials = ET.SubElement(self.root, "Materials") self.sites = None self.buildings = None self.storeys = None @staticmethod def write_id(xml_element, fc_object): ET.SubElement(xml_element, "Id").text = str(fc_object.Id) @staticmethod def write_name(xml_element, fc_object): ET.SubElement(xml_element, "Name").text = fc_object.IfcName @staticmethod def write_description(xml_element, fc_object): ET.SubElement(xml_element, "Description").text = fc_object.Description def write_root_attrib(self, xml_element, fc_object): self.write_id(xml_element, fc_object) ET.SubElement(xml_element, "GlobalId").text = fc_object.GlobalId self.write_name(xml_element, fc_object) self.write_description(xml_element, fc_object) ET.SubElement(xml_element, "IfcType").text = fc_object.IfcType @staticmethod - def write_attrib(parent, fc_object, attributes): + def write_attributes(parent, fc_object, attributes): for attrib in attributes: - ET.SubElement(parent, attrib).text = str(getattr(fc_object, attrib)) + value = getattr(fc_object, attrib) + if isinstance(value, FreeCAD.Units.Quantity): + if value.Unit.Type == "Length": + value = value.Value / SCALE + else: + value = value.Value + ET.SubElement(parent, attrib).text = str(value or "") - def write_project(self, fc_object): + def write_project(self, fc_object: "ProjectFeature") -> None: project = ET.SubElement(self.projects, "Project") self.write_root_attrib(project, fc_object) ET.SubElement(project, "LongName").text = fc_object.LongName north = ET.SubElement(project, "TrueNorth") ET.SubElement(north, "point", unitary_vector_to_dict(fc_object.TrueNorth)) wcs = ET.SubElement(project, "WorldCoordinateSystem") ET.SubElement(wcs, "point", vector_to_dict(fc_object.WorldCoordinateSystem)) self.sites = ET.SubElement(project, "Sites") for site in fc_object.Group: self.write_site(site) - def write_site(self, fc_object): + def write_site(self, fc_object: "ContainerFeature") -> None: site = ET.SubElement(self.sites, "Site") self.write_root_attrib(site, fc_object) self.buildings = ET.SubElement(site, "Buildings") for building in fc_object.Group: self.write_building(building) - def write_building(self, fc_object): + def write_building(self, fc_object: "ContainerFeature") -> None: site = ET.SubElement(self.buildings, "Building") self.write_root_attrib(site, fc_object) self.storeys = ET.SubElement(site, "Storeys") for storey in fc_object.Group: self.write_storey(storey) - def write_storey(self, fc_object): + def write_storey(self, fc_object: "ContainerFeature") -> None: storey = ET.SubElement(self.storeys, "Storey") self.write_root_attrib(storey, fc_object) spaces = ET.SubElement(storey, "Spaces") for space in fc_object.Group: ET.SubElement(spaces, "Space").text = str(space.Id) - def write_space(self, fc_object): + def write_space(self, fc_object: "SpaceFeature") -> None: space = ET.SubElement(self.spaces, "Space") self.write_root_attrib(space, fc_object) - self.write_attrib(space, fc_object, ("LongName",)) + self.write_attributes(space, fc_object, ("LongName",)) boundaries = ET.SubElement(space, "Boundaries") for boundary in fc_object.SecondLevel.Group: ET.SubElement(boundaries, "Boundary").text = str(boundary.Id) - def write_boundary(self, fc_object): + def write_boundary(self, fc_object: "RelSpaceBoundaryFeature") -> None: boundary = ET.SubElement(self.boundaries, "Boundary") self.write_root_attrib(boundary, fc_object) id_references = ( "CorrespondingBoundary", "RelatedBuildingElement", ) for name in id_references: self.append_id_element(boundary, fc_object, name) text_references = ( "InternalOrExternalBoundary", "PhysicalOrVirtualBoundary", "LesoType", ) for name in text_references: self.append_text_element(boundary, fc_object, name) ET.SubElement(boundary, "ParentBoundary").text = ( str(fc_object.ParentBoundary) if fc_object.ParentBoundary else "" ) ET.SubElement(boundary, "RelatingSpace").text = ( str(fc_object.RelatingSpace) if fc_object.RelatingSpace else "" ) inner_boundaries = ET.SubElement(boundary, "InnerBoundaries") for fc_inner_b in fc_object.InnerBoundaries: ET.SubElement(inner_boundaries, "InnerBoundary").text = str(fc_inner_b.Id) self.write_shape(boundary, fc_object) + self.write_attributes(boundary, fc_object, ("UndergroundDepth",)) is_hosted = fc_object.IsHosted ET.SubElement(boundary, "IsHosted").text = "true" if is_hosted else "false" if not is_hosted and fc_object.PhysicalOrVirtualBoundary != "VIRTUAL": for geo_type in ("SIA_Interior", "SIA_Exterior"): geo = ET.SubElement(boundary, geo_type) fc_geo = getattr(fc_object, geo_type) self.write_shape(geo, fc_geo) def write_building_elements(self, fc_object): building_element = ET.SubElement(self.building_elements, "BuildingElement") self.write_root_attrib(building_element, fc_object) ET.SubElement(building_element, "Thickness").text = str( fc_object.Thickness.Value / SCALE ) ET.SubElement(building_element, "ThermalTransmittance").text = str( fc_object.ThermalTransmittance or "" ) boundaries = ET.SubElement(building_element, "ProvidesBoundaries") for element_id in fc_object.ProvidesBoundaries: ET.SubElement(boundaries, "Id").text = str(element_id) if fc_object.Material: ET.SubElement(building_element, "Material").text = str( fc_object.Material.Id or "" ) - @staticmethod - def write_class_attrib(xml_element, fc_object): - for attrib in fc_object.Proxy.attributes: - value = getattr(fc_object, attrib) - if isinstance(value, FreeCAD.Units.Quantity): - if value.Unit.Type == "Length": - value = value.Value / SCALE - else: - value = value.Value - ET.SubElement(xml_element, attrib).text = str(value) + def write_class_attrib(self, xml_element, fc_object): + self.write_attributes(xml_element, fc_object, fc_object.Proxy.attributes) @staticmethod def write_psets(xml_element, fc_object): for props in fc_object.Proxy.psets_dict.values(): for prop in props: ET.SubElement(xml_element, prop).text = str(getattr(fc_object, prop)) def write_material(self, fc_object): proxy = fc_object.Proxy material = ET.SubElement(self.materials, type(proxy).__name__) self.write_id(material, fc_object) self.write_name(material, fc_object) self.write_description(material, fc_object) self.write_class_attrib(material, fc_object) self.write_psets(material, fc_object) if not proxy.part_name: return parts = ET.SubElement(material, proxy.parts_name) for values in zip(*[getattr(fc_object, prop) for prop in proxy.part_props]): part = ET.SubElement(parts, proxy.part_name) for i, value, attrib in zip(range(len(values)), values, proxy.part_attribs): if i == 0: ET.SubElement(part, attrib).text = str(value.Id) else: ET.SubElement(part, attrib).text = str(value) @staticmethod def write_shape(xml_element, fc_object): geom = ET.SubElement(xml_element, "geom") for wire in fc_object.Proxy.get_wires(fc_object): polygon = ET.SubElement(geom, "Polygon") for vertex in wire.Vertexes: ET.SubElement(polygon, "point", vector_to_dict(vertex.Point)) ET.SubElement(xml_element, "Area").text = fc_area_to_si_xml(fc_object.Area) ET.SubElement(xml_element, "AreaWithHosted").text = fc_area_to_si_xml( fc_object.AreaWithHosted ) @staticmethod def append_id_element(xml_element, fc_object, name): value = getattr(fc_object, name) ET.SubElement(xml_element, name).text = str(value.Id) if value else "" @staticmethod def append_text_element(xml_element, fc_object, name): ET.SubElement(xml_element, name).text = getattr(fc_object, name) def write_to_file(self, full_path): self.tree.write(full_path, encoding="UTF-8", xml_declaration=True) def tostring(self): # Return a bytes # return ET.tostring(self.root, encoding="utf8", method="xml") # Return a string return ET.tostring(self.root, encoding="unicode") def vector_to_dict(vector): """Convert a FreeCAD.Vector into a dict to write it as attribute in xml""" return {key: str(getattr(vector, key) / SCALE) for key in ("x", "y", "z")} def unitary_vector_to_dict(vector): return {key: str(getattr(vector, key)) for key in ("x", "y", "z")} def fc_area_to_si_xml(fc_area): return str(fc_area.getValueAs("m^2")) if __name__ == "__main__": OUTPUT = BEMxml() OUTPUT.write_project(None) OUTPUT_FILE = "output.xml" OUTPUT.tree.write(OUTPUT_FILE, encoding="UTF-8", xml_declaration=True) diff --git a/freecad/bem/boundaries.py b/freecad/bem/boundaries.py index 0e0c7f8..ae71699 100644 --- a/freecad/bem/boundaries.py +++ b/freecad/bem/boundaries.py @@ -1,808 +1,876 @@ # coding: utf8 """This module adapt IfcRelSpaceBoundary and create SIA specific bem boundaries 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 itertools import os from collections import namedtuple -from typing import NamedTuple +import typing +from typing import NamedTuple, Iterable, List import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.element import ifcopenshell.util.unit import FreeCAD import FreeCADGui import Part from freecad.bem import materials from freecad.bem.bem_xml import BEMxml from freecad.bem.bem_logging import logger, LOG_STREAM from freecad.bem import utils from freecad.bem.entities import ( RelSpaceBoundary, BEMBoundary, Element, ) from freecad.bem.ifc_importer import IfcImporter, TOLERANCE +if typing.TYPE_CHECKING: + from freecad.bem.typing import ( + SpaceFeature, + ContainerFeature, + ) # pylint: disable=no-name-in-module, import-error + def processing_sia_boundaries(doc=FreeCAD.ActiveDocument) -> None: """Create SIA specific boundaries cf. https://www.sia.ch/fr/services/sia-norm/""" for space in utils.get_elements_by_ifctype("IfcSpace", doc): ensure_hosted_element_are(space) ensure_hosted_are_coplanar(space) compute_space_area(space) set_boundary_normal(space) join_over_splitted_boundaries(space, doc) handle_curtain_walls(space, doc) find_closest_edges(space) set_leso_type(space) + ensure_external_earch_is_set(space, doc) create_sia_boundaries(doc) doc.recompute() +def ensure_external_earch_is_set(space: "SpaceFeature", doc=FreeCAD.ActiveDocument): + sites: List["ContainerFeature"] = list( + utils.get_elements_by_ifctype("IfcSite", doc) + ) + ground_bound_box = get_ground_bound_box(sites) + if space.Shape.BoundBox.ZMin - ground_bound_box.ZMax > 1000: + return + ground_shape = Part.Compound([]) + for site in sites: + ground_shape.add(site.Shape) + if not ground_shape.BoundBox.isValid(): + ground_shape = Part.Plane().toShape() + for boundary in space.SecondLevel.Group: + if boundary.InternalOrExternalBoundary in ( + "INTERNAL", + "EXTERNAL_EARTH", + "EXTERNAL_WATER", + "EXTERNAL_FIRE", + ): + continue + if not is_underground(boundary, ground_shape): + continue + boundary.InternalOrExternalBoundary = "EXTERNAL_EARTH" + + +def is_underground(boundary, ground_shape) -> bool: + closest_points = ground_shape.distToShape(boundary.Shape)[1][0] + direction: FreeCAD.Vector = closest_points[1] - closest_points[0] + if direction.z > 1000: + return False + if boundary.LesoType == "Flooring": + el_thickness = getattr( + getattr(getattr(boundary, "RelatedBuildingElement", 0), "Thickness", 0), + "Value", + 0, + ) + if direction.z - el_thickness * 1.5 > 0: + return False + boundary.UndergroundDepth = abs(direction.z - el_thickness) + return True + if boundary.LesoType == "Wall": + bbox = boundary.Shape.BoundBox + if (bbox.ZMax + bbox.ZMin)/2 + direction.z < 0: + return True + if boundary.LesoType == "Ceiling": + if direction.z < TOLERANCE: + return True + return False + + +def get_ground_bound_box(sites: Iterable["ContainerFeature"]) -> FreeCAD.BoundBox: + boundbox = FreeCAD.BoundBox() + for site in sites: + boundbox.add(site.Shape.BoundBox) + return boundbox if boundbox.isValid() else FreeCAD.BoundBox(0, 0, -30000, 0, 0, 0) + + def set_boundary_normal(space): faces = space.Shape.Faces for boundary in space.SecondLevel.Group: if boundary.IsHosted: continue center_of_mass = utils.get_outer_wire(boundary).CenterOfMass face = min( faces, key=lambda x: x.Surface.projectPoint(center_of_mass, "LowerDistance") ) face_normal = face.normalAt( *face.Surface.projectPoint(center_of_mass, "LowerDistanceParameters") ) normal = utils.get_normal_at(boundary) if normal.dot(face_normal) < 0: normal = -normal boundary.Normal = normal for hosted in boundary.InnerBoundaries: hosted.Normal = normal def compute_space_area(space: Part.Feature): """Compute both gross and net area""" z_min = space.Shape.BoundBox.ZMin z_sre = z_min + 1000 # 1 m above ground. See SIA 380:2015 &3.2.3 p.26-27 sre_plane = Part.Plane(FreeCAD.Vector(0, 0, z_sre), FreeCAD.Vector(0, 0, 1)) space.Area = space.Shape.common(sre_plane.toShape()).Area # TODO: Not valid yet as it return net area. Find a way to get gross space volume space.AreaAE = space.Area def handle_curtain_walls(space, doc) -> None: """Add an hosted window with full area in curtain wall boundaries as they are not handled by BEM softwares""" for boundary in space.SecondLevel.Group: if getattr(boundary.RelatedBuildingElement, "IfcType", "") != "IfcCurtainWall": continue fake_window = doc.copyObject(boundary) fake_window.IsHosted = True fake_window.LesoType = "Window" fake_window.ParentBoundary = boundary.Id fake_window.GlobalId = ifcopenshell.guid.new() fake_window.Id = IfcId.new(doc) RelSpaceBoundary.set_label(fake_window) space.SecondLevel.addObject(fake_window) # Host cannot be an empty face so inner wire is scaled down a little inner_wire = utils.get_outer_wire(boundary).scale(0.999) inner_wire = utils.project_wire_to_plane(inner_wire, utils.get_plane(boundary)) utils.append_inner_wire(boundary, inner_wire) utils.append(boundary, "InnerBoundaries", fake_window) if FreeCAD.GuiUp: fake_window.ViewObject.ShapeColor = (0.0, 0.7, 1.0) class IfcId: """Generate new id for generated boundaries missing from ifc and keep track of last id used""" current_id = 0 @classmethod def new(cls, doc) -> int: if not cls.current_id: cls.current_id = max((getattr(obj, "Id", 0) for obj in doc.Objects)) cls.current_id += 1 return cls.current_id def write_xml(doc=FreeCAD.ActiveDocument) -> BEMxml: """Read BEM infos for FreeCAD file and write it to an xml. xml is stored in an object to allow different outputs""" bem_xml = BEMxml() for project in utils.get_elements_by_ifctype("IfcProject", doc): bem_xml.write_project(project) for space in utils.get_elements_by_ifctype("IfcSpace", doc): bem_xml.write_space(space) for boundary in space.SecondLevel.Group: bem_xml.write_boundary(boundary) for building_element in utils.get_by_class(doc, Element): bem_xml.write_building_elements(building_element) for material in utils.get_by_class( doc, (materials.Material, materials.ConstituentSet, materials.LayerSet) ): bem_xml.write_material(material) return bem_xml def output_xml_to_path(bem_xml, xml_path=None): if not xml_path: xml_path = ( "./output.xml" if os.name == "nt" else "/home/cyril/git/BIMxBEM/output.xml" ) bem_xml.write_to_file(xml_path) def join_over_splitted_boundaries(space, doc=FreeCAD.ActiveDocument): boundaries = space.SecondLevel.Group # Considered as the minimal size for an oversplit to occur (1 ceiling, 3 wall, 1 flooring) if len(boundaries) <= 5: return elements_dict = dict() for rel_boundary in boundaries: try: key = f"{rel_boundary.RelatedBuildingElement.Id}" except AttributeError: if rel_boundary.PhysicalOrVirtualBoundary == "VIRTUAL": logger.info("IfcElement %s is VIRTUAL. Modeling error ?") key = "VIRTUAL" else: logger.warning( "IfcElement %s has no RelatedBuildingElement", rel_boundary.Id ) corresponding_boundary = rel_boundary.CorrespondingBoundary if corresponding_boundary: key += str(corresponding_boundary.Id) elements_dict.setdefault(key, []).append(rel_boundary) for key, boundary_list in elements_dict.items(): # None coplanar boundaries should not be connected. # eg. round wall splitted with multiple orientations. # Case1: No oversplitted boundaries if len(boundary_list) == 1: continue coplanar_boundaries = list([]) for boundary in boundary_list: if not coplanar_boundaries: coplanar_boundaries.append([boundary]) continue for coplanar_list in coplanar_boundaries: # TODO: Test if this test is not too strict considering precision if utils.is_coplanar(boundary, coplanar_list[0]): coplanar_list.append(boundary) break else: coplanar_boundaries.append([boundary]) for coplanar_list in coplanar_boundaries: # Case 1 : only 1 boundary related to the same element. Cannot group boundaries. if len(coplanar_list) == 1: continue # Case 2 : more than 1 boundary related to the same element might be grouped. try: join_coplanar_boundaries(coplanar_list, doc) except Part.OCCError: logger.warning( f"Cannot join boundaries in space <{space.Id}> with key <{key}>" ) class CommonSegment(NamedTuple): index1: int index2: int opposite_dir: FreeCAD.Vector def join_coplanar_boundaries(boundaries: list, doc=FreeCAD.ActiveDocument): """Try to join coplanar boundaries""" boundary1 = boundaries.pop() remove_from_doc = list() def find_common_segment(wire1, wire2): """Find if wires have common segments and between which edges return named tuple with edge index from each wire and if they have opposite direction""" for (ei1, edge1), (ei2, edge2) in itertools.product( enumerate(wire1.Edges), enumerate(wire2.Edges) ): if wire1 == wire2 and ei1 == ei2: continue common_segment = edges_have_common_segment(edge1, edge2) if common_segment: return CommonSegment(ei1, ei2, common_segment.opposite_dir) def edges_have_common_segment(edge1, edge2): """Check if edges have common segments and tell if these segments have same direction""" p0_1, p0_2 = utils.get_vectors_from_shape(edge1) p1_1, p1_2 = utils.get_vectors_from_shape(edge2) v0_12 = p0_2 - p0_1 v1_12 = p1_2 - p1_1 dir0 = (v0_12).normalize() dir1 = (v1_12).normalize() # if edge1 and edge2 are not collinear no junction is possible. if not ( (dir0.isEqual(dir1, TOLERANCE) or dir0.isEqual(-dir1, TOLERANCE)) and v0_12.cross(p1_1 - p0_1).Length < TOLERANCE ): return # Check in which order vectors1 and vectors2 should be connected if dir0.isEqual(dir1, TOLERANCE): p0_1_next_point, other_point = p1_1, p1_2 opposite_dir = False else: p0_1_next_point, other_point = p1_2, p1_1 opposite_dir = True # Check if edge1 and edge2 have a common segment if not ( dir0.dot(p0_1_next_point - p0_1) < dir0.dot(p0_2 - p0_1) and dir0.negative().dot(other_point - p0_2) < dir0.negative().dot(p0_1 - p0_2) ): return return CommonSegment(None, None, opposite_dir) def join_boundaries(boundary1, boundary2): wire1 = utils.get_outer_wire(boundary1) vectors1 = utils.get_vectors_from_shape(wire1) wire2 = utils.get_outer_wire(boundary2) vectors2 = utils.get_vectors_from_shape(wire2) common_segment = find_common_segment(wire1, wire2) if not common_segment: return False ei1, ei2, opposite_dir = common_segment # join vectors1 and vectors2 at indexes new_points = vectors2[ei2 + 1 :] + vectors2[: ei2 + 1] if not opposite_dir: new_points.reverse() # Efficient way to insert elements at index : https://stackoverflow.com/questions/14895599/insert-an-element-at-specific-index-in-a-list-and-return-updated-list/48139870#48139870 pylint: disable=line-too-long vectors1[ei1 + 1 : ei1 + 1] = new_points inner_wires = utils.get_inner_wires(boundary1)[:] inner_wires.extend(utils.get_inner_wires(boundary2)) if not boundary1.IsHosted: for inner_boundary in boundary2.InnerBoundaries: utils.append(boundary1, "InnerBoundaries", inner_boundary) inner_boundary.ParentBoundary = boundary1.Id # Update shape utils.clean_vectors(vectors1) utils.close_vectors(vectors1) wire1 = Part.makePolygon(vectors1) utils.generate_boundary_compound(boundary1, wire1, inner_wires) RelSpaceBoundary.recompute_areas(boundary1) return True while True: for boundary2 in boundaries: if join_boundaries(boundary1, boundary2): boundaries.remove(boundary2) remove_from_doc.append(boundary2) break else: logger.warning( - f"Unable to join boundaries RelSpaceBoundary Id <{boundary1.Id}> with boundaries <{(b.Id for b in boundaries)}>" + f"""Unable to join boundaries RelSpaceBoundary Id <{boundary1.Id}> + with boundaries <{(b.Id for b in boundaries)}>""" ) break wire1 = utils.get_outer_wire(boundary1) vectors1 = utils.get_vectors_from_shape(wire1) inner_wires = utils.get_inner_wires(boundary1)[:] while True: common_segment = find_common_segment(wire1, wire1) if not common_segment: break ei1, ei2 = common_segment[0:2] # join vectors1 and vectors2 at indexes vectors_split1 = vectors1[: ei1 + 1] + vectors1[ei2 + 1 :] vectors_split2 = vectors1[ei1 + 1 : ei2 + 1] utils.clean_vectors(vectors_split1) utils.clean_vectors(vectors_split2) area1 = Part.Face(Part.makePolygon(vectors_split1 + [vectors_split1[0]])).Area area2 = Part.Face(Part.makePolygon(vectors_split2 + [vectors_split2[0]])).Area if area1 > area2: vectors1 = vectors_split1 inner_vectors = vectors_split2 else: vectors1 = vectors_split2 inner_vectors = vectors_split1 utils.close_vectors(inner_vectors) inner_polygon = Part.makePolygon(inner_vectors) if Part.Face(inner_polygon).Area > TOLERANCE: inner_wires.extend([inner_polygon]) # Update shape utils.close_vectors(vectors1) wire1 = Part.makePolygon(vectors1) utils.generate_boundary_compound(boundary1, wire1, inner_wires) RelSpaceBoundary.recompute_areas(boundary1) # Clean FreeCAD document if join operation was a success for fc_object in remove_from_doc: doc.removeObject(fc_object.Name) def ensure_hosted_element_are(space): for boundary in space.SecondLevel.Group: try: ifc_type = boundary.RelatedBuildingElement.IfcType except AttributeError: continue if not is_typically_hosted(ifc_type): continue if boundary.IsHosted and boundary.ParentBoundary: continue def are_too_far(boundary1, boundary2): max_distance = getattr( getattr(boundary2.RelatedBuildingElement, "Thickness", 0), "Value", 0 ) return ( boundary1.Shape.distToShape(boundary2.Shape)[0] - max_distance > TOLERANCE ) def find_host(boundary): fallback_solution = None for boundary2 in space.SecondLevel.Group: if not utils.are_parallel_boundaries(boundary, boundary2): continue if are_too_far(boundary, boundary2): continue fallback_solution = boundary2 for inner_wire in utils.get_inner_wires(boundary2): if ( not abs(Part.Face(inner_wire).Area - boundary.Area.Value) < TOLERANCE ): continue return boundary2 if not fallback_solution: raise HostNotFound( f"No host found for RelSpaceBoundary Id<{boundary.Id}>" ) logger.warning( f"Using fallback solution to resolve host of RelSpaceBoundary Id<{boundary.Id}>" ) return fallback_solution try: host = find_host(boundary) except HostNotFound as err: logger.exception(err) boundary.IsHosted = True boundary.ParentBoundary = host.Id utils.append(host, "InnerBoundaries", boundary) def ensure_hosted_are_coplanar(space): for boundary in space.SecondLevel.Group: for inner_boundary in boundary.InnerBoundaries: if utils.is_coplanar(inner_boundary, boundary): continue utils.project_boundary_onto_plane(inner_boundary, utils.get_plane(boundary)) outer_wire = utils.get_outer_wire(boundary) inner_wires = utils.get_inner_wires(boundary) inner_wire = utils.get_outer_wire(inner_boundary) inner_wires.append(inner_wire) try: face = boundary.Shape.Faces[0] face = face.cut(Part.Face(inner_wire)) except RuntimeError: pass boundary.Shape = Part.Compound([face, outer_wire, *inner_wires]) def is_typically_hosted(ifc_type: str): """Say if given ifc_type is typically hosted eg. windows, doors""" usually_hosted_types = ("IfcWindow", "IfcDoor", "IfcOpeningElement") for usual_type in usually_hosted_types: if ifc_type.startswith(usual_type): return True return False class HostNotFound(LookupError): pass -def find_closest_edges(space): +def find_closest_edges(space: "SpaceFeature") -> None: """Find closest boundary and edge to be able to reconstruct a closed shell""" boundaries = [b for b in space.SecondLevel.Group if not b.IsHosted] Closest = namedtuple("Closest", ["boundary", "edge", "distance"]) # Initialise defaults values for boundary in boundaries: n_edges = len(utils.get_outer_wire(boundary).Edges) boundary.Proxy.closest = [ Closest(boundary=None, edge=-1, distance=100000) ] * n_edges def compare_closest(boundary1, ei1, edge1, boundary2, ei2, edge2): is_closest = False distance = boundary1.Proxy.closest[ei1].distance edge_to_edge = compute_distance(edge1, edge2) if distance <= TOLERANCE: return # Perfect match if edge_to_edge <= TOLERANCE: is_closest = True elif edge_to_edge - distance - TOLERANCE <= 0: # Case 1 : boundaries point in same direction so all solution are valid. dot_dir = boundary2.Normal.dot(boundary1.Normal) if abs(dot_dir) >= 1 - TOLERANCE: is_closest = True # Case 2 : boundaries intersect else: # Check if projection on plane intersection cross boundary1. # If so edge2 cannot be a valid solution. pnt1 = edge1.CenterOfMass - plane_intersect = utils.get_plane(boundary1).intersect( + plane_intersect = utils.get_plane(boundary1).intersectSS( utils.get_plane(boundary2) )[0] v_ab = plane_intersect.Direction v_ap = pnt1 - plane_intersect.Location pnt2 = pnt1 + FreeCAD.Vector().projectToLine(v_ap, v_ab) try: projection_edge = Part.makeLine(pnt1, pnt2) common = projection_edge.common(boundary1.Shape.Faces[0]) if common.Length <= TOLERANCE: is_closest = True # Catch case where pnt1 == pnt2 which is fore sure a valid solution. except Part.OCCError: is_closest = True if is_closest: boundary1.Proxy.closest[ei1] = Closest(boundary2, ei2, edge_to_edge) # Loop through all boundaries and edges to find the closest edge for boundary1, boundary2 in itertools.combinations(boundaries, 2): # If boundary1 and boundary2 are facing an opposite direction no match possible if boundary2.Normal.dot(boundary1.Normal) <= -1 + TOLERANCE: continue edges1 = utils.get_outer_wire(boundary1).Edges edges2 = utils.get_outer_wire(boundary2).Edges for (ei1, edge1), (ei2, edge2) in itertools.product( enumerate(edges1), enumerate(edges2) ): if not is_low_angle(edge1, edge2): continue compare_closest(boundary1, ei1, edge1, boundary2, ei2, edge2) compare_closest( # pylint: disable=arguments-out-of-order boundary2, ei2, edge2, boundary1, ei1, edge1 ) # Store found values in standard FreeCAD properties for boundary in boundaries: closest_boundaries, boundary.ClosestEdges, closest_distances = ( list(i) for i in zip(*boundary.Proxy.closest) ) boundary.ClosestBoundaries = [b.Id if b else -1 for b in closest_boundaries] boundary.ClosestDistance = [int(d) for d in closest_distances] def set_leso_type(space): for boundary in space.SecondLevel.Group: boundary.LesoType = define_leso_type(boundary) def define_leso_type(boundary): try: ifc_type = boundary.RelatedBuildingElement.IfcType except AttributeError: if boundary.PhysicalOrVirtualBoundary != "VIRTUAL": logger.warning(f"Unable to define LesoType for boundary <{boundary.Id}>") return "Unknown" if ifc_type.startswith("IfcWindow"): return "Window" elif ifc_type.startswith("IfcDoor"): return "Door" elif ifc_type.startswith("IfcWall"): return "Wall" elif ifc_type.startswith("IfcSlab") or ifc_type == "IfcRoof": # Pointing up => Ceiling. Pointing down => Flooring if boundary.Normal.z > 0: return "Ceiling" return "Flooring" elif ifc_type.startswith("IfcOpeningElement"): return "Opening" else: logger.warning(f"Unable to define LesoType for Boundary Id <{boundary.Id}>") return "Unknown" def compute_distance(edge1, edge2): mid_point = edge1.CenterOfMass line_segment = (v.Point for v in edge2.Vertexes) return mid_point.distanceToLineSegment(*line_segment).Length def is_low_angle(edge1, edge2): dir1 = (edge1.Vertexes[1].Point - edge1.Vertexes[0].Point).normalize() dir2 = (edge2.Vertexes[1].Point - edge2.Vertexes[0].Point).normalize() return abs(dir1.dot(dir2)) > 0.5 # Low angle considered as < 30°. cos(pi/3)=0.5. def create_sia_boundaries(doc=FreeCAD.ActiveDocument): """Create boundaries necessary for SIA calculations""" project = next(utils.get_elements_by_ifctype("IfcProject", doc)) is_from_revit = project.ApplicationIdentifier == "Revit" for space in utils.get_elements_by_ifctype("IfcSpace", doc): create_sia_ext_boundaries(space, is_from_revit) create_sia_int_boundaries(space, is_from_revit) rejoin_boundaries(space, "SIA_Exterior") rejoin_boundaries(space, "SIA_Interior") def rejoin_boundaries(space, sia_type): """ Rejoin boundaries after their translation to get a correct close shell surfaces. 1 Fill gaps between boundaries (2b) 2 Fill gaps gerenate by translation to make a boundary on the inside or outside boundary of building elements https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/schema/ifcproductextension/lexical/ifcrelspaceboundary2ndlevel.htm # pylint: disable=line-too-long """ base_boundaries = space.SecondLevel.Group for base_boundary in base_boundaries: lines = [] boundary1 = getattr(base_boundary, sia_type) if ( base_boundary.IsHosted or base_boundary.PhysicalOrVirtualBoundary == "VIRTUAL" or not base_boundary.RelatedBuildingElement ): continue b1_plane = utils.get_plane(boundary1) for b2_id, (ei1, ei2) in zip( base_boundary.ClosestBoundaries, enumerate(base_boundary.ClosestEdges) ): base_boundary2 = utils.get_in_list_by_id(base_boundaries, b2_id) boundary2 = getattr(base_boundary2, sia_type, None) if not boundary2: logger.warning(f"Cannot find corresponding boundary with id <{b2_id}>") lines.append( utils.line_from_edge(utils.get_outer_wire(base_boundary).Edges[ei1]) ) continue # Case 1 : boundaries are not parallel if not base_boundary.Normal.isEqual(base_boundary2.Normal, TOLERANCE): plane_intersect = b1_plane.intersect(utils.get_plane(boundary2)) if plane_intersect: lines.append(plane_intersect[0]) continue # Case 2 : boundaries are parallel line1 = utils.line_from_edge(utils.get_outer_wire(boundary1).Edges[ei1]) try: line2 = utils.line_from_edge(utils.get_outer_wire(boundary2).Edges[ei2]) except IndexError: logger.warning( - f"Cannot find closest edge index <{ei2}> in boundary id <{b2_id}> to rejoin boundary <{base_boundary.Id}>" + f"""Cannot find closest edge index <{ei2}> in boundary id <{b2_id}> + to rejoin boundary <{base_boundary.Id}>""" ) lines.append( utils.line_from_edge(utils.get_outer_wire(base_boundary).Edges[ei1]) ) continue # Case 2a : edges are not parallel if abs(line1.Direction.dot(line2.Direction)) < 1 - TOLERANCE: line_intersect = line1.intersect2d(line2, b1_plane) if line_intersect: point1 = b1_plane.value(*line_intersect[0]) if line1.Direction.dot(line2.Direction) > 0: point2 = point1 + line1.Direction + line2.Direction else: point2 = point1 + line1.Direction - line2.Direction continue # Case 2b : edges are parallel else: point1 = (line1.Location + line2.Location) * 0.5 point2 = point1 + line1.Direction try: lines.append(Part.Line(point1, point2)) except Part.OCCError: logger.exception( f"Failure in boundary id <{base_boundary.Id}> {point1} and {point2} are equal" ) # Generate new shape try: outer_wire = utils.polygon_from_lines(lines, b1_plane) except utils.NoIntersectionError: # TODO: Investigate to see why this happens logger.exception(f"Unable to rejoin boundary Id <{base_boundary.Id}>") continue except Part.OCCError: logger.exception( f"Invalid geometry while rejoining boundary Id <{base_boundary.Id}>" ) continue try: Part.Face(outer_wire) except Part.OCCError: logger.exception(f"Unable to rejoin boundary Id <{base_boundary.Id}>") continue inner_wires = utils.get_inner_wires(boundary1) utils.generate_boundary_compound(boundary1, outer_wire, inner_wires) boundary1.Area = area = boundary1.Shape.Area for inner_boundary in base_boundary.InnerBoundaries: area = area + inner_boundary.Shape.Area boundary1.AreaWithHosted = area def create_sia_ext_boundaries(space, is_from_revit): """Create SIA boundaries from RelSpaceBoundaries and translate it if necessary""" sia_group_obj = space.Boundaries.newObject( "App::DocumentObjectGroup", "SIA_Exteriors" ) space.SIA_Exteriors = sia_group_obj for boundary1 in space.SecondLevel.Group: if boundary1.IsHosted or boundary1.PhysicalOrVirtualBoundary == "VIRTUAL": continue bem_boundary = BEMBoundary.create(boundary1, "SIA_Exterior") sia_group_obj.addObject(bem_boundary) if not boundary1.RelatedBuildingElement: continue thickness = boundary1.RelatedBuildingElement.Thickness.Value ifc_type = boundary1.RelatedBuildingElement.IfcType normal = boundary1.Shape.Faces[0].normalAt(0, 0) # EXTERNAL: there is multiple possible values for external so testing internal is better. if boundary1.InternalOrExternalBoundary != "INTERNAL": lenght = thickness if is_from_revit and ifc_type.startswith("IfcWall"): lenght /= 2 bem_boundary.Placement.move(normal * lenght) # INTERNAL. TODO: Check during tests if NOTDEFINED case need to be handled ? else: type1 = {"IfcSlab"} if ifc_type in type1: lenght = thickness / 2 else: if is_from_revit: continue lenght = thickness / 2 bem_boundary.Placement.move(normal * lenght) def create_sia_int_boundaries(space, is_from_revit): """Create boundaries necessary for SIA calculations""" sia_group_obj = space.Boundaries.newObject( "App::DocumentObjectGroup", "SIA_Interiors" ) space.SIA_Interiors = sia_group_obj for boundary in space.SecondLevel.Group: if boundary.IsHosted or boundary.PhysicalOrVirtualBoundary == "VIRTUAL": continue normal = boundary.Normal bem_boundary = BEMBoundary.create(boundary, "SIA_Interior") sia_group_obj.addObject(bem_boundary) if not boundary.RelatedBuildingElement: continue ifc_type = boundary.RelatedBuildingElement.IfcType if is_from_revit and ifc_type.startswith("IfcWall"): thickness = boundary.RelatedBuildingElement.Thickness.Value lenght = thickness / 2 bem_boundary.Placement.move(normal.negative() * lenght) class XmlResult(NamedTuple): xml: str log: str def generate_bem_xml_from_file(ifc_path: str) -> XmlResult: ifc_importer = IfcImporter(ifc_path) ifc_importer.generate_rel_space_boundaries() doc = ifc_importer.doc processing_sia_boundaries(doc) xml_str = write_xml(doc).tostring() log_str = LOG_STREAM.getvalue() return XmlResult(xml_str, log_str) def process_test_file(ifc_path, doc): ifc_importer = IfcImporter(ifc_path, doc) ifc_importer.generate_rel_space_boundaries() processing_sia_boundaries(doc) bem_xml = write_xml(doc) output_xml_to_path(bem_xml) ifc_importer.xml = bem_xml ifc_importer.log = LOG_STREAM.getvalue() if FreeCAD.GuiUp: FreeCADGui.activeView().viewIsometric() FreeCADGui.SendMsgToActiveView("ViewFit") with open("./boundaries.log", "w", encoding="utf-8") as log_file: log_file.write(ifc_importer.log) return ifc_importer if __name__ == "__main__": if os.name == "nt": TEST_FOLDER = r"C:\git\BIMxBEM\IfcTestFiles" else: TEST_FOLDER = "/home/cyril/git/BIMxBEM/IfcTestFiles/" TEST_FILES = { 0: "Triangle_2x3_A23.ifc", 1: "Triangle_2x3_R19.ifc", 2: "2Storey_2x3_A22.ifc", 3: "2Storey_2x3_R19.ifc", 4: "0014_Vernier112D_ENE_ModèleÉnergétique_R20.ifc", 6: "Investigation_test_R19.ifc", 7: "OverSplitted_R20_2x3.ifc", 8: "ExternalEarth_R20_2x3.ifc", 9: "ExternalEarth_R20_IFC4.ifc", 10: "Ersatzneubau Alphütte_1-1210_31_23.ifc", 11: "GRAPHISOFT_ARCHICAD_Sample_Project_Hillside_House_v1.ifczip", 12: "GRAPHISOFT_ARCHICAD_Sample_Project_S_Office_v1.ifczip", 13: "Cas1_EXPORT_REVIT_IFC2x3 (EDITED)_Space_Boundaries.ifc", 14: "Cas1_EXPORT_REVIT_IFC4DTV (EDITED)_Space_Boundaries.ifc", 15: "Cas1_EXPORT_REVIT_IFC4RV (EDITED)_Space_Boundaries.ifc", 16: "Cas1_EXPORT_REVIT_IFC4RV (EDITED)_Space_Boundaries_RECREATED.ifc", 17: "Cas2_EXPORT_REVIT_IFC4RV (EDITED)_Space_Boudaries.ifc", 18: "Cas2_EXPORT_REVIT_IFC4DTV (EDITED)_Space_Boundaries_RECREATED.ifc", 19: "Cas2_EXPORT_REVIT_IFC4DTV (EDITED)_Space_Boundaries.ifc", 20: "Cas2_EXPORT_REVIT_IFC2x3 (EDITED)_Space_Boundaries.ifc", 21: "Temoin.ifc", 22: "1708 maquette test 01.ifc", 23: "test 02-03 mur int baseslab dalle de sol.ifc", 24: "test 02-06 murs composites.ifc", 25: "test 02-07 dalle étage et locaux mansardés.ifc", 26: "test 02-08 raccords nettoyés étage.ifc", 27: "test 02-09 sous-sol.ifc", + 28: "test 02-02 mur matériau simple.ifc", } - IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[1]) + IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[27]) 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() process_test_file(IFC_PATH, DOC) else: FreeCADGui.showMainWindow() DOC = FreeCAD.newDocument() process_test_file(IFC_PATH, DOC) # xml_str = generate_bem_xml_from_file(IFC_PATH) FreeCADGui.exec_loop() diff --git a/freecad/bem/entities.py b/freecad/bem/entities.py index 9f472de..0bd2cbe 100644 --- a/freecad/bem/entities.py +++ b/freecad/bem/entities.py @@ -1,447 +1,448 @@ # 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::PropertyInteger", "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::PropertyInteger", "ParentBoundary", ifc_attributes) obj.addProperty("App::PropertyLinkList", "InnerBoundaries", ifc_attributes) obj.addProperty("App::PropertyVector", "Normal", 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.Id) obj.RelatingSpace = ifc_entity.RelatingSpace.id() 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" 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 = "IfcRelSpaceBoundary" 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::PropertyLinkList", "FillsVoids", ifc_attributes) obj.addProperty("App::PropertyLinkList", "HasOpenings", ifc_attributes) obj.addProperty( "App::PropertyIntegerList", "ProvidesBoundaries", ifc_attributes ) obj.addProperty("App::PropertyFloat", "ThermalTransmittance", ifc_attributes) obj.addProperty("App::PropertyLinkList", "HostedElements", bem_category) obj.addProperty("App::PropertyInteger", "HostElement", bem_category) obj.addProperty("App::PropertyLength", "Thickness", bem_category) @classmethod def read_from_ifc(cls, obj, ifc_entity): super().read_from_ifc(obj, ifc_entity) obj.Label = f"{obj.Id}_{obj.IfcType}" super().read_pset_from_ifc( obj, ifc_entity, [ "ThermalTransmittance", ], ) class 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::PropertyInteger", "SourceBoundary", category_name) obj.SourceBoundary = boundary.Id 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 "" obj.TrueNorth = FreeCAD.Vector( *ifc_entity.RepresentationContexts[0].TrueNorth.DirectionRatios ) 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.entity_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/typing.pyi b/freecad/bem/typing.pyi index 63990e6..eb5cbd8 100644 --- a/freecad/bem/typing.pyi +++ b/freecad/bem/typing.pyi @@ -1,119 +1,134 @@ # coding: utf8 """This module contains stubs for FreeCAD Feature Python. © 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 List import FreeCAD import Part -from freecad.bem.entities import Root, Container, RelSpaceBoundary, BEMBoundary, Space, Element, Project +from freecad.bem.entities import ( + Root, + Container, + RelSpaceBoundary, + BEMBoundary, + Space, + Element, + Project, +) from freecad.bem.materials import Material, LayerSet, ConstituentSet class RootFeature(Part.Feature): Proxy: Root = ... Group: List[Part.Feature] = ... IfcType: str = ... Id: int = ... GlobalId: str = ... IfcName: str = ... Description: str = ... - + Shape: Part.Shape = ... class RelSpaceBoundaryFeature(RootFeature): Proxy: RelSpaceBoundary = ... RelatingSpace: int = ... RelatedBuildingElement: ElementFeature = ... PhysicalOrVirtualBoundary: str = ... InternalOrExternalBoundary: str = ... CorrespondingBoundary: RelSpaceBoundaryFeature = ... ParentBoundary: int = ... InnerBoundaries: List[RelSpaceBoundaryFeature] = ... + UndergroundDepth: PropertyLength = ... Normal: FreeCAD.Vector = ... ClosestBoundaries: List[int] = ... ClosestEdges: List[int] = ... ClosestDistance: List[int] = ... IsHosted: bool = ... PropertyArea: PropertyArea = ... AreaWithHosted: PropertyArea = ... SIA_Interior: BEMBoundaryFeature = ... SIA_Exterior: BEMBoundaryFeature = ... LesoType: str = ... class ElementFeature(RootFeature): Proxy: Element = ... Material: MaterialFeature = ... FillsVoids: List[ElementFeature] = ... HasOpenings: List[ElementFeature] = ... ProvidesBoundaries: List[int] = ... ThermalTransmittance: float = ... HostedElements: List[ElementFeature] = ... HostElement: int = ... Thickness: PropertyLength = ... class BEMBoundaryFeature(Part.Feature): Proxy: BEMBoundary = ... SourceBoundary: int = ... Area: PropertyArea = ... AreaWithHosted: PropertyArea = ... class ContainerFeature(RootFeature): Proxy: Container = ... class ProjectFeature(RootFeature): Proxy: Project = ... LongName: str = ... TrueNorth: FreeCAD.Vector = ... WorldCoordinateSystem: FreeCAD.Vector = ... ApplicationIdentifier: str = ... ApplicationVersion: str = ... ApplicationFullName: str = ... +class SecondLevelGroup: + Group: List[RelSpaceBoundaryFeature] = ... + +class SIAGroups: + Group: List[BEMBoundaryFeature] = ... + class SpaceFeature(RootFeature): Proxy: Space = ... LongName: str = ... - Boundaries: List[Group] = ... - SecondLevel: Group = ... - SIA_Interiors: Group = ... - SIA_Exteriors: Group = ... + Boundaries: List[SecondLevelGroup] = ... + SecondLevel: SecondLevelGroup = ... + SIA_Interiors: SIAGroups = ... + SIA_Exteriors: SIAGroups = ... Area: PropertyArea = ... AreaAE: PropertyArea = ... class MaterialFeature(Part.Feature): Proxy: Material = ... Id: int = ... IfcName: str = ... - Description: str = ... + Description: str = ... Category: str = ... - MassDensity: str = ... - Porosity: str = ... + MassDensity: str = ... + Porosity: str = ... VisibleTransmittance: float = ... SolarTransmittance: float = ... ThermalIrTransmittance: float = ... ThermalIrEmissivityBack: float = ... ThermalIrEmissivityFront: float = ... SpecificHeatCapacity: float = ... ThermalConductivity: float = ... class LayerSetFeature(Part.Feature): Proxy: LayerSet Thicknesses: List[float] = ... - Id: int = ... + Id: int = ... MaterialLayers: List[MaterialFeature] = ... IfcName: str = ... - Description: str = ... + Description: str = ... TotalThickness: str = ... class ConstituentSetFeature(Part.Feature): Proxy: ConstituentSet - Id: int = ... + Id: int = ... IfcName: str = ... - Description: str = ... + Description: str = ... Fractions: List[float] = ... Categories: List[str] = ... MaterialConstituents: List[MaterialFeature] = ...