diff --git a/.gitignore b/.gitignore index 5318cf6..13ddd4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,186 +1,177 @@ ### Windows template # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp *.exe # Windows shortcuts *.lnk ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # IDE specific folders .idea/ .vscode # CMake cmake-build-debug/ cmake-build-release/ -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml -# Cursive Clojure plugin -.idea/replstate.xml - # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C/C++ extensions *.so *.dll # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # pyCharm settings _windows -# dbnavigator settings -dbnavigator.xml - *.VC.opendb *.pdb *.lib *.ilk *.exp *.obj *.tlog *.idb *.VC.db .vs/ ifcopenshell/ \ No newline at end of file diff --git a/FreeCAD/BIMxBEM/MANIFEST.in b/FreeCAD/BIMxBEM/MANIFEST.in new file mode 100644 index 0000000..d160d13 --- /dev/null +++ b/FreeCAD/BIMxBEM/MANIFEST.in @@ -0,0 +1 @@ +recursive-include freecad/bem/resources * diff --git a/FreeCAD/BIMxBEM/freecad/bem/__init__.py b/FreeCAD/BIMxBEM/freecad/bem/__init__.py new file mode 100644 index 0000000..f80dcdc --- /dev/null +++ b/FreeCAD/BIMxBEM/freecad/bem/__init__.py @@ -0,0 +1,4 @@ +import os +from .version import __version__ + +ICONPATH = os.path.join(os.path.dirname(__file__), "resources") diff --git a/IfcPython/read_boundaries.py b/FreeCAD/BIMxBEM/freecad/bem/boundaries.py similarity index 64% copy from IfcPython/read_boundaries.py copy to FreeCAD/BIMxBEM/freecad/bem/boundaries.py index 3ba0c83..be9aaed 100644 --- a/IfcPython/read_boundaries.py +++ b/FreeCAD/BIMxBEM/freecad/bem/boundaries.py @@ -1,892 +1,1082 @@ # 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 +import FreeCADGui from bem_xml import BEMxml -WRITE_XML = True -GUI_UP = True - 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") +def generate_space(ifc_space, parent, doc=FreeCAD.ActiveDocument): + fc_space = create_space_from_entity(ifc_space) + parent.addObject(fc_space) + + boundaries = fc_space.newObject("App::DocumentObjectGroup", "Boundaries") + fc_space.Boundaries = boundaries + second_levels = boundaries.newObject("App::DocumentObjectGroup", "SecondLevel") + fc_space.SecondLevel = second_levels + + # All boundaries have their placement relative to space placement + space_placement = get_placement(ifc_space) + for ifc_boundary in (b for b in ifc_space.BoundedBy if b.Name == "2ndLevel"): + face = make_relspaceboundary(ifc_boundary) + second_levels.addObject(face) + face.Placement = space_placement + element = get_related_element(ifc_boundary, doc) + if element: + face.RelatedBuildingElement = element + append(element, "ProvidesBoundaries", face) + face.RelatingSpace = fc_space + + +def generate_containers(ifc_parent, fc_parent, doc=FreeCAD.ActiveDocument): + for rel_aggregates in ifc_parent.IsDecomposedBy: + for element in rel_aggregates.RelatedObjects: + if element.is_a("IfcSpace"): + generate_space(element, fc_parent, doc) + else: + fc_container = create_container_from_entity(element) + fc_parent.addObject(fc_container) + generate_containers(element, fc_container, doc) - 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 +def get_elements(doc=FreeCAD.ActiveDocument): + """Generator throught FreeCAD document element of specific ifc_type""" + for element in doc.Objects: + try: + if isinstance(element.Proxy, Element): + yield element + except AttributeError: + continue + + +def get_elements_by_ifctype(ifc_type: str, doc=FreeCAD.ActiveDocument): + """Generator throught FreeCAD document element of specific ifc_type""" + for element in doc.Objects: + try: + if element.IfcType == ifc_type: + yield element + except AttributeError: + continue - # Generate projects - ifc_projects = ifc_file.by_type("IfcProject") - projects = get_or_create_group(doc, "Projects") - for ifc_project in ifc_projects: - projects.addObject(create_project_from_entity(ifc_project)) + +def generate_ifc_rel_space_boundaries(ifc_path, doc=FreeCAD.ActiveDocument): + """Display IfcRelSpaceBoundaries from selected IFC file into FreeCAD documennt""" + ifc_file = ifcopenshell.open(ifc_path) # Generate elements (Door, Window, Wall, Slab etc…) without their geometry + elements_group = get_or_create_group("Elements", doc) ifc_elements = (e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries) for ifc_entity in ifc_elements: elements_group.addObject(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: - fc_space = create_space_from_entity(group_2nd, space) - - # 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) - fc_space.addObject(face) - face.Placement = space_placement - 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 = fc_space + # Generate projects structure and boundaries + for ifc_project in ifc_file.by_type("IfcProject"): + project = create_project_from_entity(ifc_project) + generate_containers(ifc_project, project, doc) # Associate CorrespondingBoundary - associate_corresponding_boundaries(group_2nd) + associate_corresponding_boundaries(doc) + + # Join over splitted boundaries + for space in get_elements_by_ifctype("IfcSpace", doc): + join_over_splitted_boundaries(space) + + # Associate Host / Hosted elements + associate_host_element(ifc_file, elements_group) # Associate hosted elements an fill gaps - for fc_space in group_2nd.Group: - fc_boundaries = fc_space.Group + for fc_space in get_elements_by_ifctype("IfcSpace", doc): + fc_boundaries = fc_space.SecondLevel.Group # Minimal number of boundary is 5: 3 vertical faces, 2 horizontal faces # If there is less than 5 boundaries there is an issue or a new case to analyse if len(fc_boundaries) == 5: continue elif len(fc_boundaries) < 5: assert ValueError, f"{fc_space.Label} has less than 5 boundaries" # Find coplanar boundaries to identify hosted boundaries and fill gaps (2b) associate_coplanar_boundaries(fc_boundaries) # Associate hosted elements associate_inner_boundaries(fc_boundaries) - # 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) + +def create_geo_boundaries(doc=FreeCAD.ActiveDocument): + """Create SIA specific boundaries cf. https://www.sia.ch/fr/services/sia-norm/""" + for space in get_elements_by_ifctype("IfcSpace", doc): + close_space_boundaries(space) + find_closest_edges(space) + # create_geo_ext_boundaries(doc) + # sia_interiors = boundaries.newObject("App::DocumentObjectGroup", "SIA_Interiors") + # sia_exteriors = boundaries.newObject("App::DocumentObjectGroup", "SIA_Exteriors") + # create_geo_int_boundaries(doc) doc.recompute() - if WRITE_XML: - bem_xml = BEMxml() - for project in projects.Group: - bem_xml.write_project(project) - for space in group_2nd.Group: - bem_xml.write_space(space) + +def write_xml(doc=FreeCAD.ActiveDocument): + bem_xml = BEMxml() + for project in get_elements_by_ifctype("IfcProject", doc): + bem_xml.write_project(project) + for space in get_elements_by_ifctype("IfcSpace", doc): + bem_xml.write_space(space) for boundary in space.Group: bem_xml.write_boundary(boundary) - for building_element in elements_group.Group: - bem_xml.write_building_elements(building_element) - xml_path = "./output.xml" if os.name == "nt" else "/home/cyril/git/BIMxBEM/output.xml" - bem_xml.write_to_file(xml_path) + for building_element in get_elements(): + bem_xml.write_building_elements(building_element) + 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 close_space_boundaries(space): + sia = space.Boundaries.newObject("App::DocumentObjectGroup", "SIA") + + join_over_splitted_boundaries(space) + boundaries = space.SecondLevel.Group + for rel_boundary1 in boundaries: + for edge1 in rel_boundary1.Shape.OuterWire.Edges: + mid_point = edge1.CenterOfMass + for rel_boundary2 in boundaries: + if rel_boundary1 == rel_boundary2: + continue + for edge2 in enumerate(rel_boundary2.Shape.OuterWire.Edges): + pass + + +def join_over_splitted_boundaries(space): + boundaries = space.SecondLevel.Group + if len(boundaries) < 5: + return + elements_dict = dict() + for rel_boundary in boundaries: + key = f"{rel_boundary.RelatedBuildingElement.Id}" + corresponding_boundary = rel_boundary.CorrespondingBoundary + if corresponding_boundary: + key += str(corresponding_boundary.Id) + elements_dict.setdefault(key, []).append(rel_boundary) + for boundary_list in elements_dict.values(): + # Case 1 : only 1 boundary related to the same element. Cannot group boundaries. + if len(boundary_list) == 1: + continue + # Case 2 : more than 1 boundary related to the same element might be grouped. + join_boundaries(boundary_list) + + +def join_boundaries(boundaries: list): + result_boundary = boundaries.pop() + result_shape = result_boundary.Shape + while boundaries: + edges1 = result_shape.OuterWire.Edges + for boundary2 in boundaries: + edges2 = boundary2.Shape.OuterWire.Edges + for (ei1, edge1), (ei2, edge2) in itertools.product( + enumerate(edges1), enumerate(edges2) + ): + if not is_collinear(edge1, edge2): + continue + vectors1 = get_boundary_outer_vectors(result_boundary) + vectors2 = get_boundary_outer_vectors(boundary2) + v0_0, v0_1, v0_2, v0_3 = [ + vectors1[i % len(vectors1)] for i in range(ei1 - 1, ei1 + 3) + ] + v1_0, v1_1, v1_2, v1_3 = [ + vectors1[i % len(vectors2)] for i in range(ei2 - 1, ei2 + 3) + ] + tolerance = 1 # mm + if v0_1.isEqual(v1_1, tolerance): + pass + elif v0_1.isEqual(v1_2, tolerance): + pass + + + +def get_boundary_outer_vectors(boundary): + return [vx.Point for vx in boundary.Shape.OuterWire.Vertexes] + + +def is_collinear(edge1, edge2): + v0_0, v0_1 = (vx.Point for vx in edge1.Vertexes) + v1_0, v1_1 = (vx.Point for vx in edge2.Vertexes) + if is_collinear_or_parallel(v0_0, v0_1, v1_0, v1_1): + return v0_0 == v1_0 or is_collinear_or_parallel(v0_0, v0_1, v0_0, v1_0) + + +def is_collinear_or_parallel(v0_0, v0_1, v1_0, v1_1): + return abs(direction(v0_0, v0_1).dot(direction(v1_0, v1_1))) > 0.9999 + + +def direction(v0, v1): + return (v0 - v1).normalize() + + +def find_closest_edges(space): + boundaries = [b for b in space.SecondLevel.Group if not b.IsHosted] + closest_dict = dict() + for i, boundary in enumerate(boundaries): + n = len(boundary.Shape.OuterWire.Edges) + closest_dict[i] = { + "boundaries": [-1] * n, + "edges": [-1] * n, + "distances": [10000] * n, + } + for (bi1, boundary1), (bi2, boundary2) in itertools.combinations( + enumerate(boundaries), 2 + ): + distances1 = closest_dict[bi1]["distances"] + distances2 = closest_dict[bi2]["distances"] + edges1 = boundary1.Shape.OuterWire.Edges + edges2 = boundary2.Shape.OuterWire.Edges + for (ei1, edge1), (ei2, edge2) in itertools.product( + enumerate(edges1), enumerate(edges2) + ): + if not is_low_angle(edge1, edge2): + continue + distance = distances1[ei1] + edge_to_edge = compute_distance(edge1, edge2) + if edge_to_edge < distance: + closest_dict[bi1]["boundaries"][ei1] = bi2 + closest_dict[bi1]["edges"][ei1] = ei2 + distances1[ei1] = round(edge_to_edge) + + distance = distances2[ei2] + edge_to_edge = compute_distance(edge2, edge1) + if edge_to_edge < distance: + closest_dict[bi2]["boundaries"][ei2] = bi1 + closest_dict[bi2]["edges"][ei2] = ei1 + distances2[ei2] = round(edge_to_edge) + + for i, boundary in enumerate(boundaries): + boundary.ClosestBoundaries = closest_dict[i]["boundaries"] + boundary.ClosestEdges = closest_dict[i]["edges"] + boundary.ClosestDistance = closest_dict[i]["distances"] + + +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 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): +def associate_corresponding_boundaries(doc=FreeCAD.ActiveDocument): # Associate CorrespondingBoundary - for space_group in group_2nd.Group: - for fc_boundary in space_group.Group: - associate_corresponding_boundary(fc_boundary) + for fc_boundary in get_elements_by_ifctype("IfcRelSpaceBoundary", doc): + 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): if fc_boundary.PhysicalOrVirtualBoundary == "VIRTUAL": return [] 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}" ) 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 +def get_related_element(ifc_entity, doc=FreeCAD.ActiveDocument): if not ifc_entity.RelatedBuildingElement: return guid = ifc_entity.RelatedBuildingElement.GlobalId - for element in elements: - if element.GlobalId == guid: - return element + for element in doc.Objects: + try: + if element.GlobalId == guid: + return element + except AttributeError: + continue 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 try: material_layers = association.RelatingMaterial.ForLayerSet.MaterialLayers except AttributeError: - material_layers = association.RelatingMaterial.MaterialLayers + try: + material_layers = association.RelatingMaterial.MaterialLayers + except AttributeError: + # TODO: Fallback method to handle materials with no layers. eg. association.RelatingMaterial.Materials + continue + for material_layer in material_layers: thickness += material_layer.LayerThickness * SCALE return thickness -def create_geo_ext_boundaries(doc, group_2nd): +def create_geo_ext_boundaries(doc=FreeCAD.ActiveDocument): """Create boundaries necessary for SIA calculations""" - bem_group = doc.addObject("App::DocumentObjectGroup", "geoExt") - is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit" - is_from_archicad = group_2nd.getParentGroup().ApplicationFullName == "ARCHICAD-64" - for space in group_2nd.Group: - for boundary in space.Group: - if boundary.IsHosted or boundary.PhysicalOrVirtualBoundary == "VIRTUAL": - continue - bem_boundary = make_bem_boundary(boundary, "geoExt") - 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 - bem_boundary.Placement.move(normal * lenght) - else: - type1 = {"IfcSlab"} - if ifc_type in type1: - if normal.z > 0: - lenght = thickness - else: - continue + project = next(get_elements_by_ifctype("IfcProject"), doc) + is_from_revit = project.ApplicationIdentifier == "Revit" + is_from_archicad = project.ApplicationFullName == "ARCHICAD-64" + for boundary in get_elements_by_ifctype("IfcRelSpaceBoundary", doc): + if boundary.IsHosted or boundary.PhysicalOrVirtualBoundary == "VIRTUAL": + continue + bem_boundary = make_bem_boundary(boundary, "geoExt") + boundary.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 + bem_boundary.Placement.move(normal * lenght) + else: + type1 = {"IfcSlab"} + if ifc_type in type1: + if normal.z > 0: + lenght = thickness else: - if is_from_revit: - continue - lenght = thickness / 2 - bem_boundary.Placement.move(normal * lenght) + continue + else: + if is_from_revit: + continue + lenght = thickness / 2 + bem_boundary.Placement.move(normal * lenght) def create_geo_int_boundaries(doc, group_2nd): """Create boundaries necessary for SIA calculations""" bem_group = doc.addObject("App::DocumentObjectGroup", "geoInt") is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit" 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 or boundary.PhysicalOrVirtualBoundary == "VIRTUAL": continue normal = boundary.Shape.normalAt(0, 0) if is_from_archicad: normal = -normal bem_boundary = make_bem_boundary(boundary, "geoInt") bem_group.addObject(bem_boundary) 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 * 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 (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): +def get_or_create_group(name, doc=FreeCAD.ActiveDocument): """Get group by name or create one if not found""" group = doc.findObjects("App::DocumentObjectGroup", name) if group: return group[0] return doc.addObject("App::DocumentObjectGroup", name) 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 + obj.addExtension("App::GroupExtensionPython", self) + 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", "Description", ifc_attributes) obj.Id = ifc_entity.id() 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 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, ifc_entity): super().__init__(obj, ifc_entity) obj.Proxy = self bem_category = "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", "ProcessedShape", bem_category) - obj.addProperty("App::PropertyLinkList", "CoincidentBoundaries", 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::PropertyBoolList", "ClosestHasSameNormal", bem_category) obj.addProperty( "App::PropertyLinkList", "ShareRelatedElementWith", bem_category ) - obj.addProperty( - "App::PropertyIntegerList", "CoincidentVertexIndexList", 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", "geoInt", bem_category) obj.addProperty("App::PropertyLink", "geoExt", bem_category) 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": 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" ) 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) 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, geo_type): """Stantard FreeCAD FeaturePython Object creation method""" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "BEMBoundary") # ViewProviderRelSpaceBoundary(obj.ViewObject) BEMBoundary(obj, boundary) setattr(boundary, geo_type, obj) 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 +def create_container_from_entity(ifc_entity): + """Stantard FreeCAD FeaturePython Object creation method""" + obj_name = "Project" + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name) + Container(obj, ifc_entity) + if FreeCAD.GuiUp: + obj.ViewObject.Proxy = ViewProviderRoot(obj.ViewObject) + return 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, ifc_entity): + super().__init__(obj, ifc_entity) + self.ifc_entity = ifc_entity + self.setProperties(obj) + + def setProperties(self, obj): + return + + def create_project_from_entity(ifc_entity): """Stantard FreeCAD FeaturePython Object creation method""" obj_name = "Project" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name) Project(obj, ifc_entity) - try: - obj.ViewObject.Proxy = 0 - except AttributeError: - FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui") + if FreeCAD.GuiUp: + obj.ViewObject.Proxy = ViewProviderRoot(obj.ViewObject) return obj class Project(Root): """Representation of an IfcProject: https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcproject.htm""" def __init__(self, obj, ifc_entity): super().__init__(obj, ifc_entity) + self.ifc_entity = ifc_entity + self.setProperties(obj) + + def setProperties(self, obj): + ifc_entity = self.ifc_entity 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) obj.LongName = ifc_entity.LongName obj.TrueNorth = FreeCAD.Vector( *ifc_entity.RepresentationContexts[0].TrueNorth.DirectionRatios ) obj.WorldCoordinateSystem = FreeCAD.Vector( ifc_entity.RepresentationContexts[ 0 ].WorldCoordinateSystem.Location.Coordinates ) + owning_application = "OwningApplication" + obj.addProperty( + "App::PropertyString", "ApplicationIdentifier", owning_application + ) + obj.addProperty("App::PropertyString", "ApplicationVersion", owning_application) + obj.addProperty( + "App::PropertyString", "ApplicationFullName", owning_application + ) + owning_application = ifc_entity.OwnerHistory.OwningApplication + obj.ApplicationIdentifier = owning_application.ApplicationIdentifier + obj.ApplicationVersion = owning_application.Version + obj.ApplicationFullName = owning_application.ApplicationFullName -def create_space_from_entity(group_2nd, ifc_entity): - obj = group_2nd.newObject( - "App::DocumentObjectGroup", f"Space_{ifc_entity.Name}" - ) - 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", "Description", ifc_attributes) - - obj.Id = ifc_entity.id() - obj.GlobalId = ifc_entity.GlobalId - obj.IfcType = ifc_entity.is_a() - - space_full_name = f"{ifc_entity.Name} {ifc_entity.LongName}" - obj.Label = space_full_name - try: - obj.Description = ifc_entity.Description - except TypeError: - pass +def create_space_from_entity(ifc_entity): + """Stantard FreeCAD FeaturePython Object creation method""" + obj_name = "Space" + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name) + Space(obj, ifc_entity) + if FreeCAD.GuiUp: + obj.ViewObject.Proxy = ViewProviderRoot(obj.ViewObject) return obj +class Space(Root): + """Representation of an IfcProject: + https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcproject.htm""" + + def __init__(self, obj, ifc_entity): + super().__init__(obj, ifc_entity) + self.ifc_entity = ifc_entity + self.setProperties(obj) + + def setProperties(self, obj): + ifc_entity = self.ifc_entity + 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) + + space_full_name = f"{ifc_entity.Name} {ifc_entity.LongName}" + obj.Label = space_full_name + try: + obj.Description = ifc_entity.Description + except TypeError: + pass + + return obj + + if __name__ == "__main__": - if os.name == 'nt': + if os.name == "nt": TEST_FOLDER = r"C:\git\BIMxBEM\IfcTestFiles" else: 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", "0014_Vernier112D_ENE_ModèleÉnergétique_R20.ifc", - "Investigation_test_R19.ifc" + "Investigation_test_R19.ifc", ] - IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[0]) + IFC_PATH = os.path.join(TEST_FOLDER, TEST_FILES[2]) 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) + generate_ifc_rel_space_boundaries(IFC_PATH, DOC) + create_geo_boundaries(DOC) FreeCADGui.activeView().viewIsometric() FreeCADGui.SendMsgToActiveView("ViewFit") FreeCADGui.exec_loop() - - -class box: - pass diff --git a/FreeCAD/BIMxBEM/freecad/bem/commands.py b/FreeCAD/BIMxBEM/freecad/bem/commands.py new file mode 100644 index 0000000..084dc6b --- /dev/null +++ b/FreeCAD/BIMxBEM/freecad/bem/commands.py @@ -0,0 +1,67 @@ +import os + +import FreeCAD + +from freecad.bem import boundaries + + +class ImportRelSpaceBoundary: + def IsActive(self): + return bool(FreeCAD.ActiveDocument) + + def GetResources(self): + return { + "Pixmap": "icon.svg", + "MenuText": "Import", + "ToolTip": "Import IfcRelSpaceBoundary for selected IFC file", + } + + def Activated(self): + if os.name == "nt": + test_folder = r"C:\git\BIMxBEM\IfcTestFiles" + else: + 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", + "0014_Vernier112D_ENE_ModèleÉnergétique_R20.ifc", + "Investigation_test_R19.ifc", + ] + ifc_path = os.path.join(test_folder, test_files[2]) + boundaries.generate_ifc_rel_space_boundaries( + ifc_path, doc=FreeCAD.ActiveDocument + ) + return + + +class GenerateBemBoundaries: + def IsActive(self): + return bool(FreeCAD.ActiveDocument) + + def GetResources(self): + return { + "Pixmap": "icon.svg", + "MenuText": "Generate SIA boundaries", + "ToolTip": "Generate SIA specific BEM boundaries", + } + + def Activated(self): + boundaries.create_geo_boundaries() + return + + +class WriteToXml: + def IsActive(self): + return bool(FreeCAD.ActiveDocument) + + def GetResources(self): + return { + "Pixmap": "icon.svg", + "MenuText": "Import", + "ToolTip": "Import IfcRelSpaceBoundary for selected IFC file", + } + + def Activated(self): + return diff --git a/FreeCAD/BIMxBEM/freecad/bem/init_gui.py b/FreeCAD/BIMxBEM/freecad/bem/init_gui.py new file mode 100644 index 0000000..2b837fc --- /dev/null +++ b/FreeCAD/BIMxBEM/freecad/bem/init_gui.py @@ -0,0 +1,61 @@ +import os +import sys + +import FreeCADGui as Gui +import FreeCAD as App + +from freecad.bem import ICONPATH + + +class BIMxBEM(Gui.Workbench): + """ + class which gets initiated at starup of the gui + """ + + MenuText = "BIMxBEM" + ToolTip = "a simple template workbench" + Icon = os.path.join(ICONPATH, "template_resource.svg") + toolbox = ["Import", "Generate"] + + def GetClassName(self): + return "Gui::PythonWorkbench" + + def Initialize(self): + """ + This function is called at the first activation of the workbench. + here is the place to import all the commands + """ + sys.path.append("/home/cyril/git/BIMxBEM") + from freecad.bem import commands + + App.Console.PrintMessage("switching to BIMxBEM workbench") + self.appendToolbar("Tools", self.toolbox) + self.appendMenu("Tools", self.toolbox) + Gui.addCommand("Import", commands.ImportRelSpaceBoundary()) + Gui.addCommand("Generate", commands.GenerateBemBoundaries()) + + def reload(self): + from importlib import reload + from freecad.bem import commands + + reload(commands) + App.Console.PrintMessage("Reloading BIMxBEM workbench") + self.appendToolbar("Tools", self.toolbox) + self.appendMenu("Tools", self.toolbox) + Gui.addCommand("Import", commands.ImportRelSpaceBoundary()) + Gui.addCommand("Generate", commands.GenerateBemBoundaries()) + + def Activated(self): + """ + code which should be computed when a user switch to this workbench + """ + pass + + def Deactivated(self): + """ + code which should be computed when this workbench is deactivated + """ + pass + + +Gui.addWorkbench(BIMxBEM()) diff --git a/FreeCAD/BIMxBEM/freecad/bem/resources/template_resource.svg b/FreeCAD/BIMxBEM/freecad/bem/resources/template_resource.svg new file mode 100644 index 0000000..509be26 --- /dev/null +++ b/FreeCAD/BIMxBEM/freecad/bem/resources/template_resource.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + T + diff --git a/FreeCAD/BIMxBEM/setup.py b/FreeCAD/BIMxBEM/setup.py new file mode 100644 index 0000000..e36df5b --- /dev/null +++ b/FreeCAD/BIMxBEM/setup.py @@ -0,0 +1,21 @@ +from setuptools import setup +import os +# from freecad.bem.version import __version__ +# name: this is the name of the distribution. +# Packages using the same name here cannot be installed together + +version_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), + "freecad", "bem", "version.py") +with open(version_path) as fp: + exec(fp.read()) + +setup(name='freecad.bem', + version=str(__version__), + packages=['freecad', + 'freecad.bem'], + maintainer="looooo", + maintainer_email="sppedflyer@gmail.com", + url="https://github.com/FreeCAD/Workbench-Starterkit", + description="template for a freecad extensions, installable with pip", + install_requires=['numpy'], # should be satisfied by FreeCAD's system dependencies already + include_package_data=True) diff --git a/IfcPython/read_boundaries.py b/IfcPython/read_boundaries.py index 3ba0c83..59bddf8 100644 --- a/IfcPython/read_boundaries.py +++ b/IfcPython/read_boundaries.py @@ -1,892 +1,897 @@ # 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 from bem_xml import BEMxml WRITE_XML = True GUI_UP = True 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 projects ifc_projects = ifc_file.by_type("IfcProject") projects = get_or_create_group(doc, "Projects") for ifc_project in ifc_projects: projects.addObject(create_project_from_entity(ifc_project)) # Generate elements (Door, Window, Wall, Slab etc…) without their geometry 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: fc_space = create_space_from_entity(group_2nd, space) # 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) fc_space.addObject(face) face.Placement = space_placement 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 = fc_space # Associate CorrespondingBoundary associate_corresponding_boundaries(group_2nd) # Associate hosted elements an fill gaps for fc_space in group_2nd.Group: fc_boundaries = fc_space.Group # Minimal number of boundary is 5: 3 vertical faces, 2 horizontal faces # If there is less than 5 boundaries there is an issue or a new case to analyse if len(fc_boundaries) == 5: continue elif len(fc_boundaries) < 5: assert ValueError, f"{fc_space.Label} has less than 5 boundaries" # Find coplanar boundaries to identify hosted boundaries and fill gaps (2b) associate_coplanar_boundaries(fc_boundaries) # Associate hosted elements associate_inner_boundaries(fc_boundaries) # 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) doc.recompute() if WRITE_XML: bem_xml = BEMxml() for project in projects.Group: bem_xml.write_project(project) for space in group_2nd.Group: bem_xml.write_space(space) - for boundary in space.Group: - bem_xml.write_boundary(boundary) + for boundary in space.Group: + bem_xml.write_boundary(boundary) for building_element in elements_group.Group: bem_xml.write_building_elements(building_element) xml_path = "./output.xml" if os.name == "nt" else "/home/cyril/git/BIMxBEM/output.xml" bem_xml.write_to_file(xml_path) 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): if fc_boundary.PhysicalOrVirtualBoundary == "VIRTUAL": return [] 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}" ) 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 elements: if element.GlobalId == guid: return element 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 try: material_layers = association.RelatingMaterial.ForLayerSet.MaterialLayers except AttributeError: - material_layers = association.RelatingMaterial.MaterialLayers + try: + material_layers = association.RelatingMaterial.MaterialLayers + except AttributeError: + # TODO: Fallback method to handle materials with no layers. eg. association.RelatingMaterial.Materials + continue + for material_layer in material_layers: thickness += material_layer.LayerThickness * SCALE return thickness def create_geo_ext_boundaries(doc, group_2nd): """Create boundaries necessary for SIA calculations""" bem_group = doc.addObject("App::DocumentObjectGroup", "geoExt") is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit" is_from_archicad = group_2nd.getParentGroup().ApplicationFullName == "ARCHICAD-64" for space in group_2nd.Group: for boundary in space.Group: if boundary.IsHosted or boundary.PhysicalOrVirtualBoundary == "VIRTUAL": continue bem_boundary = make_bem_boundary(boundary, "geoExt") 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 bem_boundary.Placement.move(normal * lenght) else: 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): """Create boundaries necessary for SIA calculations""" bem_group = doc.addObject("App::DocumentObjectGroup", "geoInt") is_from_revit = group_2nd.getParentGroup().ApplicationIdentifier == "Revit" 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 or boundary.PhysicalOrVirtualBoundary == "VIRTUAL": continue normal = boundary.Shape.normalAt(0, 0) if is_from_archicad: normal = -normal bem_boundary = make_bem_boundary(boundary, "geoInt") bem_group.addObject(bem_boundary) 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 * 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 (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::PropertyInteger", "Id", ifc_attributes) obj.addProperty("App::PropertyString", "GlobalId", ifc_attributes) obj.addProperty("App::PropertyString", "Description", ifc_attributes) obj.Id = ifc_entity.id() 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 bem_category = "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", "ProcessedShape", bem_category) obj.addProperty("App::PropertyLinkList", "CoincidentBoundaries", bem_category) obj.addProperty( "App::PropertyLinkList", "ShareRelatedElementWith", bem_category ) obj.addProperty( "App::PropertyIntegerList", "CoincidentVertexIndexList", 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", "geoInt", bem_category) obj.addProperty("App::PropertyLink", "geoExt", bem_category) 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": 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" ) 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) 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, geo_type): """Stantard FreeCAD FeaturePython Object creation method""" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "BEMBoundary") # ViewProviderRelSpaceBoundary(obj.ViewObject) BEMBoundary(obj, boundary) setattr(boundary, geo_type, obj) 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 def create_project_from_entity(ifc_entity): """Stantard FreeCAD FeaturePython Object creation method""" obj_name = "Project" obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", obj_name) Project(obj, ifc_entity) try: obj.ViewObject.Proxy = 0 except AttributeError: FreeCAD.Console.PrintLog("No ViewObject ok if running with no Gui") return obj class Project(Root): """Representation of an IfcProject: https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcproject.htm""" def __init__(self, obj, ifc_entity): super().__init__(obj, ifc_entity) 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) obj.LongName = ifc_entity.LongName obj.TrueNorth = FreeCAD.Vector( *ifc_entity.RepresentationContexts[0].TrueNorth.DirectionRatios ) obj.WorldCoordinateSystem = FreeCAD.Vector( ifc_entity.RepresentationContexts[ 0 ].WorldCoordinateSystem.Location.Coordinates ) def create_space_from_entity(group_2nd, ifc_entity): obj = group_2nd.newObject( "App::DocumentObjectGroup", f"Space_{ifc_entity.Name}" ) 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", "Description", ifc_attributes) obj.Id = ifc_entity.id() obj.GlobalId = ifc_entity.GlobalId obj.IfcType = ifc_entity.is_a() space_full_name = f"{ifc_entity.Name} {ifc_entity.LongName}" obj.Label = space_full_name try: obj.Description = ifc_entity.Description except TypeError: pass return obj if __name__ == "__main__": if os.name == 'nt': TEST_FOLDER = r"C:\git\BIMxBEM\IfcTestFiles" else: 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", "0014_Vernier112D_ENE_ModèleÉnergétique_R20.ifc", "Investigation_test_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() class box: pass diff --git a/IfcPython/write_to_ifc.py b/IfcPython/write_to_ifc.py index 8a87720..4af4ec0 100644 --- a/IfcPython/write_to_ifc.py +++ b/IfcPython/write_to_ifc.py @@ -1,20 +1,32 @@ # coding: utf8 """This module write IfcRelSpaceBoundary to an ifc file""" import ifcopenshell # Default location and directions ORIGIN = (0.0, 0.0, 0.0) DIR_X = (1.0, 0.0, 0.0) DIR_Y = (0.0, 1.0, 0.0) DIR_Z = (0.0, 0.0, 1.0) def create_ifc_axis2placement(ifc_file, point=ORIGIN, dir_z=DIR_Z, dir_x=DIR_X): point = ifc_file.createIfcCartesianPoint(point) dir_z = ifc_file.createIfcDirection(dir_z) dir_x = ifc_file.createIfcDirection(dir_x) return ifc_file.createIfcAxis2Placement3D(point, dir_z, dir_x) + def create_boundary(ifc_file, points): - pass \ No newline at end of file + ifc_file.createIfcRelSpaceBoundary( + GlobalId=ifcopenshell.guid.new(), + Ownerhistory=None, + Name=None, + Description=None, + RelatingSpace=relating_space, + RelatedBuildingElement=relating_building, + ConnectionGeometry, + PhysicalOrVirtualBoundary, + InternalOrExternalBoundary, + ) + diff --git a/Notes.md b/Notes.md index 3ae84e3..7b5708f 100644 --- a/Notes.md +++ b/Notes.md @@ -1,55 +1,61 @@ * [IfcSpace]() : * BoundedBy : Référence les IfcRelSpaceBoundary * ObjectPlacement : Les IfcRelSpaceBoundary sont placés relativement à ce placement * [IfcRelSpaceBoundary](https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary.htm) : * RelatingSpace : Pour obtenir l'IfcSpace * RelatedBuildingElement : Pour obtenir l'IfcElement associé (ex. IfcWall) * Obtenir la direction normale : * Option 1 : Shape.normalAt(0,0) (Fonction FreeCAD). Elle pointe vers l'extérieur. ```python for face in doc.findObjects("Part::Feature"): face.Placement.move(face.Shape.normalAt(0, 0)*200) ``` * Option 2 : Appliquer la matrice à la direction * InternalOrExternalBoundary : 'EXTERNAL' ou 'INTERNAL' * PhysicalOrVirtualBoundary : Virtual si pas de séparation physique. Comment le traiter ? * Nouveauté IFC4 : nouvelle classe avec attributs supplémentaires (ParentBoundary, CorrespondingBoundary) où la face opposée est déjà renseignée ainsi que cas échéant l'éléments dans lequel elle est hébergée. * [IfcWall]() / [IfcDoor]() / [IfcWindow]() : * ConnectedTo : Contient les connections avec d'autres éléments tel que IfcWall * RelatedElement : Élement correspondant (ex: IfcWall) * RelatedConnectionType : 'ATEND', 'START' Peu utile étant donné qu'un mur peut être commun à plusieurs locaux. Les extrémités des murs ne sont pas forcément les extrémités des IfcRelSpaceBoundary. Peut-être utiles pour repérer les murs intermédiaires qui coupe un IfcRelSpaceBoundary en 2 surfaces côte à côte ? * HasOpenings : Référence les ouvertures hébergés. Probablement plus pratique de passer par l'IfcRelSpaceBoundary référençant * ProvidesBoundaries : Référence les IfcRelSpaceBoundary lié à l'élément. Utile pour voir quel élément est en vis à vis ? * HasAssociations : Contient notamment les matériaux associés. * HasCovering : Certains logiciels pourrait donner l'isolation dans un élément séparé ? (TODO: Vérifier définition schéma IFC) * GlobalId : uuid à enregistrer pour mise à jour IFC etc… Vérifier si 2 formes sont coplanaires : Part.Shape.isCoplanar(other_shape) Concernant les surfaces modifiées : * Les points coincidants entre les faces doivent le rester après translation. Attention : * Pas forcément un segment, 1 seul point peut être coincident avec une autre face * 1 point ne coincide pas forcément avec un autre point (ligne / face) Part.Vertex.Point -> FreeCAD.Vector 1. Repérer les points coincidents (méthode d'enregistrement de la coincidence, performance de calcul ?) 2. Calculer la ligne à l'intersection des plans. (Part.Plane.intersectSS) 3. Projeter les points coincidents sur la ligne. (FreeCAD.Vector.projectToLine) Dans Revit, l'export d'un modèle en cm pose problème -> exporter en m en attendant de trouver la solution. IfcOpenShell ne sort pas les dimensions en m avec input cm ? Ajouter une pour annuler le traitement si il n'y a aucune IfcRelSpaceBoundary dans le fichier ou si le fichier est vide / n'existe pas Procédure / cahier des charges par application (Envoyer procédure export avec IfcRelSpaceBoundary) Medial axis -Straight Skeleton \ No newline at end of file +Straight Skeleton + +Centre entre 2 droites : +* Parallèles : droite parallèle à interdistance +* Non parallèles : Origine -> Intersection des droites. Direction -> ??? + +# TODO : Génerer un modèle test avec surfaces surdécoupées \ No newline at end of file