diff --git a/src/ifcclash/ifcclash/collider.py b/src/ifcclash/ifcclash/collider.py deleted file mode 100644 index 62a912df5e..0000000000 --- a/src/ifcclash/ifcclash/collider.py +++ /dev/null @@ -1,121 +0,0 @@ -# IfcClash - IFC-based clash detection. -# Copyright (C) 2020, 2021 Dion Moult -# -# This file is part of IfcClash. -# -# IfcClash is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcClash is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcClash. If not, see . - -import hppfcl -import numpy as np -import ifcopenshell - - -class Collider: - def __init__(self, logger): - self.logger = logger - self.groups = {} - self.tree = ifcopenshell.geom.tree() - - def create_group(self, name): - self.logger.info(f"Creating group {name}") - self.groups[name] = {"elements": {}, "objects": {}} - - def create_objects(self, name, ifc_file, iterator, elements): - import time - - start = time.time() - self.logger.info(f"Adding objects {name}") - assert iterator.initialize() - while True: - self.tree.add_element(iterator.get_native()) - shape = iterator.get() - self.create_object(name, shape.guid, shape) - if not iterator.next(): - break - self.logger.info(f"Tree finished {time.time() - start}") - start = time.time() - self.groups[name]["elements"].update({e.GlobalId: e for e in elements}) - self.logger.info(f"Element metadata finished {time.time() - start}") - start = time.time() - - def create_object(self, group_name, id, shape): - obj = hppfcl.CollisionObject( - self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data) - ) - self.groups[group_name]["objects"][id] = obj - - def collide_internal(self, name): - return self.collide_narrowphase(name, name, self.collide_broadphase(name, name)) - - def collide_group(self, name1, name2): - return self.collide_narrowphase(name1, name2, self.collide_broadphase(name1, name2)) - - def collide_broadphase(self, name1, name2): - import time - - start = time.time() - self.logger.info("Starting broadphase") - potential_collisions = [] - checked_collisions = set() - for id, element in self.groups[name1]["elements"].items(): - checked_collisions.add(id) - box_filter = self.tree.select_box(element) - pairs = [ - {"id1": id, "id2": e.GlobalId} - for e in box_filter - if e.GlobalId not in checked_collisions and e.GlobalId in self.groups[name2]["elements"] - ] - potential_collisions.extend(pairs) - self.logger.info(f"Finished broadphase {time.time() - start}") - return potential_collisions - - def collide_narrowphase(self, name1, name2, potential_collisions): - import time - - start = time.time() - self.logger.info("Starting narrowphase") - collisions = [] - for data in potential_collisions: - result = hppfcl.CollisionResult() - hppfcl.collide( - self.groups[name1]["objects"][data["id1"]], - self.groups[name2]["objects"][data["id2"]], - hppfcl.CollisionRequest(), - result, - ) - if result.isCollision(): - collisions.append({"id1": data["id1"], "id2": data["id2"], "collision": result}) - self.logger.info(f"Finished narrowphase {time.time() - start}") - return collisions - - def create_transform(self, m): - mat = np.array([[m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]]) - mat.transpose() - return hppfcl.Transform3f(mat[:3, :3], mat[:3, 3]) - - def create_bvh(self, mesh): - v = mesh.verts - f = mesh.faces - mesh_verts = np.array([[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]) - mesh_faces = [(int(f[i]), int(f[i + 1]), int(f[i + 2])) for i in range(0, len(f), 3)] - - bvh = hppfcl.BVHModelOBB() - bvh.beginModel(len(mesh_faces), len(mesh_verts)) - vertices = hppfcl.StdVec_Vec3f() - [vertices.append(v) for v in mesh_verts] - triangles = hppfcl.StdVec_Triangle() - [triangles.append(hppfcl.Triangle(f[0], f[1], f[2])) for f in mesh_faces] - bvh.addSubModel(vertices, triangles) - bvh.endModel() - return bvh diff --git a/src/ifcclash/ifcclash/ifcclash.py b/src/ifcclash/ifcclash/ifcclash.py index 1406f40aee..b66fe7689d 100644 --- a/src/ifcclash/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash/ifcclash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # IfcClash - IFC-based clash detection. -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2020-2024 Dion Moult # # This file is part of IfcClash. # @@ -19,14 +19,13 @@ # along with IfcClash. If not, see . -import numpy as np import json -import sys +import time +import numpy as np import multiprocessing import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.selector -from . import collider class Clasher: @@ -34,55 +33,78 @@ class Clasher: self.settings = settings self.geom_settings = ifcopenshell.geom.settings() self.clash_sets = [] - self.collider = collider.Collider(self.settings.logger) - self.selector = ifcopenshell.util.selector.Selector() + self.logger = self.settings.logger + self.groups = {} self.ifcs = {} + self.tree = None def clash(self): - existing_limit = sys.getrecursionlimit() for clash_set in self.clash_sets: self.process_clash_set(clash_set) def process_clash_set(self, clash_set): - self.collider.create_group("a") + self.tree = ifcopenshell.geom.tree() + self.create_group("a") for source in clash_set["a"]: source["ifc"] = self.load_ifc(source["file"]) self.add_collision_objects("a", source["ifc"], source.get("mode", None), source.get("selector", None)) - if "b" in clash_set: - self.collider.create_group("b") + if "b" in clash_set and clash_set["b"]: + self.create_group("b") for source in clash_set["b"]: source["ifc"] = self.load_ifc(source["file"]) self.add_collision_objects("b", source["ifc"], source.get("mode", None), source.get("selector", None)) - results = self.collider.collide_group("a", "b") + b = "b" else: - results = self.collider.collide_internal("a") + b = "a" + + mode = clash_set["mode"] + if mode == "intersection": + results = self.tree.clash_intersection_many( + list(self.groups["a"]["elements"].values()), + list(self.groups[b]["elements"].values()), + tolerance=clash_set["tolerance"], + check_all=clash_set["check_all"], + ) + elif mode == "collision": + results = self.tree.clash_collision_many( + list(self.groups["a"]["elements"].values()), + list(self.groups[b]["elements"].values()), + allow_touching=clash_set["allow_touching"], + ) + elif mode == "clearance": + results = self.tree.clash_clearance_many( + list(self.groups["a"]["elements"].values()), + list(self.groups[b]["elements"].values()), + clearance=clash_set["clearance"], + check_all=clash_set["check_all"], + ) processed_results = {} for result in results: - element1 = self.get_element(clash_set["a"], result["id1"]) - if "b" in clash_set: - element2 = self.get_element(clash_set["b"], result["id2"]) - else: - element2 = self.get_element(clash_set["a"], result["id2"]) + element1 = result.a + element2 = result.b - contact = result["collision"].getContacts()[0] - processed_results[f"{result['id1']}-{result['id2']}"] = { - "a_global_id": result["id1"], - "b_global_id": result["id2"], + processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = { + "a_global_id": element1.get_argument(0), + "b_global_id": element2.get_argument(0), "a_ifc_class": element1.is_a(), "b_ifc_class": element2.is_a(), - "a_name": element1.Name, - "b_name": element2.Name, - "normal": list(contact.normal), - "position": list(contact.pos), - "penetration_depth": contact.penetration_depth, + "a_name": element1.get_argument(2), + "b_name": element2.get_argument(2), + "type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type], + "p1": list(result.p1), + "p2": list(result.p2), + "distance": result.distance, } clash_set["clashes"] = processed_results + self.logger.info(f"Found clashes: {len(processed_results.keys())}") + + def create_group(self, name): + self.logger.info(f"Creating group {name}") + self.groups[name] = {"elements": {}, "objects": {}} def load_ifc(self, path): - import time - start = time.time() self.settings.logger.info(f"Loading IFC {path}") ifc = self.ifcs.get(path, None) @@ -93,21 +115,36 @@ class Clasher: return ifc def add_collision_objects(self, name, ifc_file, mode=None, selector=None): - import time - start = time.time() self.settings.logger.info("Creating iterator") - if not mode: - elements = ifc_file.by_type("IfcElement") + if not mode or not selector: + elements = set(ifc_file.by_type("IfcElement")) + elements -= set(ifc_file.by_type("IfcFeatureElement")) elif mode == "e": - elements = set(ifc_file.by_type("IfcElement")) - set(self.selector.parse(ifc_file, selector)) + elements = set(ifc_file.by_type("IfcElement")) + elements -= set(ifc_file.by_type("IfcFeatureElement")) + elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, selector)) elif mode == "i": - elements = self.selector.parse(ifc_file, selector) + elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, selector)) iterator = ifcopenshell.geom.iterator( self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements ) self.settings.logger.info(f"Iterator creation finished {time.time() - start}") - self.collider.create_objects(name, ifc_file, iterator, elements) + + start = time.time() + self.logger.info(f"Adding objects {name}") + assert iterator.initialize() + while True: + self.tree.add_element(iterator.get_native(), should_triangulate=True) + # self.tree.add_element(iterator.get()) + shape = iterator.get() + if not iterator.next(): + break + self.logger.info(f"Tree finished {time.time() - start}") + start = time.time() + self.groups[name]["elements"].update({e.GlobalId: e for e in elements}) + self.logger.info(f"Element metadata finished {time.time() - start}") + start = time.time() def export(self): if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": @@ -123,7 +160,9 @@ class Clasher: title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}' topic = bcfxml.add_topic(title, title, "IfcClash") viewpoint = topic.add_viewpoint_from_point_and_guids( - np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"], + np.array(clash["position"]), + clash["a_global_id"], + clash["b_global_id"], ) snapshot = self.get_viewpoint_snapshot(viewpoint) if snapshot: @@ -147,15 +186,6 @@ class Clasher: with open(self.settings.output, "w", encoding="utf-8") as clashes_file: json.dump(clash_sets, clashes_file, indent=4) - def get_element(self, clash_set, global_id): - for source in clash_set: - try: - element = source["ifc"].by_guid(global_id) - if element: - return element - except: - pass - def smart_group_clashes(self, clash_sets, max_clustering_distance): from sklearn.cluster import OPTICS from collections import defaultdict