diff --git a/src/ifcclash/collision.py b/src/ifcclash/collision.py deleted file mode 100644 index b5dcf620f5..0000000000 --- a/src/ifcclash/collision.py +++ /dev/null @@ -1,663 +0,0 @@ -# This code is taken from the trimesh project at https://github.com/mikedh/trimesh/blob/master/trimesh/collision.py -# License MIT https://github.com/mikedh/trimesh/blob/master/LICENSE.md - -import numpy as np - -import collections - -try: - # pip install python-fcl - import fcl -except BaseException: - fcl = None - - -class ContactData(object): - """ - Data structure for holding information about a collision contact. - """ - - def __init__(self, names, contact): - """ - Initialize a ContactData. - - Parameters - ---------- - names : list of str - The names of the two objects in order. - contact : fcl.Contact - The contact in question. - """ - self.names = names - self._inds = {names[0]: contact.b1, names[1]: contact.b2} - self._point = contact.pos - self.raw = contact - - @property - def point(self): - """ - The 3D point of intersection for this contact. - - Returns - ------- - point : (3,) float - The intersection point. - """ - return self._point - - def index(self, name): - """ - Returns the index of the face in contact for the mesh with - the given name. - - Parameters - ---------- - name : str - The name of the target object. - - Returns - ------- - index : int - The index of the face in collison - """ - return self._inds[name] - - -class DistanceData(object): - """ - Data structure for holding information about a distance query. - """ - - def __init__(self, names, result): - """ - Initialize a DistanceData. - - Parameters - ---------- - names : list of str - The names of the two objects in order. - contact : fcl.DistanceResult - The distance query result. - """ - self.names = set(names) - self._inds = {names[0]: result.b1, names[1]: result.b2} - self._points = {names[0]: result.nearest_points[0], names[1]: result.nearest_points[1]} - self._distance = result.min_distance - - @property - def distance(self): - """ - Returns the distance between the two objects. - - Returns - ------- - distance : float - The euclidean distance between the objects. - """ - return self._distance - - def index(self, name): - """ - Returns the index of the closest face for the mesh with - the given name. - - Parameters - ---------- - name : str - The name of the target object. - - Returns - ------- - index : int - The index of the face in collisoin. - """ - return self._inds[name] - - def point(self, name): - """ - The 3D point of closest distance on the mesh with the given name. - - Parameters - ---------- - name : str - The name of the target object. - - Returns - ------- - point : (3,) float - The closest point. - """ - return self._points[name] - - -class CollisionManager(object): - """ - A mesh-mesh collision manager. - """ - - def __init__(self): - """ - Initialize a mesh-mesh collision manager. - """ - if fcl is None: - raise ValueError("No FCL Available!") - # {name: {geom:, obj}} - self._objs = {} - # {id(bvh) : str, name} - # unpopulated values will return None - self._names = collections.defaultdict(lambda: None) - - # cache BVH objects - # {mesh.md5(): fcl.BVHModel object} - self._bvh = {} - self._manager = fcl.DynamicAABBTreeCollisionManager() - self._manager.setup() - - def add_object(self, name, mesh, transform=None): - """ - Add an object to the collision manager. - - If an object with the given name is already in the manager, - replace it. - - Parameters - ---------- - name : str - An identifier for the object - mesh : Trimesh object - The geometry of the collision object - transform : (4,4) float - Homogeneous transform matrix for the object - """ - - # if no transform passed, assume identity transform - if transform is None: - transform = np.eye(4) - transform = np.asanyarray(transform, dtype=np.float64) - if transform.shape != (4, 4): - raise ValueError("transform must be (4,4)!") - - # create or recall from cache BVH - bvh = self._get_BVH(mesh) - # create the FCL transform from (4,4) matrix - t = fcl.Transform(transform[:3, :3], transform[:3, 3]) - o = fcl.CollisionObject(bvh, t) - - # Add collision object to set - if name in self._objs: - self._manager.unregisterObject(self._objs[name]) - self._objs[name] = {"obj": o, "geom": bvh} - # store the name of the geometry - self._names[id(bvh)] = name - - self._manager.registerObject(o) - self._manager.update() - return o - - def remove_object(self, name): - """ - Delete an object from the collision manager. - - Parameters - ---------- - name : str - The identifier for the object - """ - if name in self._objs: - self._manager.unregisterObject(self._objs[name]["obj"]) - self._manager.update(self._objs[name]["obj"]) - # remove objects from _objs - geom_id = id(self._objs.pop(name)["geom"]) - # remove names - self._names.pop(geom_id) - else: - raise ValueError("{} not in collision manager!".format(name)) - - def set_transform(self, name, transform): - """ - Set the transform for one of the manager's objects. - This replaces the prior transform. - - Parameters - ---------- - name : str - An identifier for the object already in the manager - transform : (4,4) float - A new homogeneous transform matrix for the object - """ - if name in self._objs: - o = self._objs[name]["obj"] - o.setRotation(transform[:3, :3]) - o.setTranslation(transform[:3, 3]) - self._manager.update(o) - else: - raise ValueError("{} not in collision manager!".format(name)) - - def in_collision_single(self, mesh, transform=None, return_names=False, return_data=False): - """ - Check a single object for collisions against all objects in the - manager. - - Parameters - ---------- - mesh : Trimesh object - The geometry of the collision object - transform : (4,4) float - Homogeneous transform matrix - return_names : bool - If true, a set is returned containing the names - of all objects in collision with the object - return_data : bool - If true, a list of ContactData is returned as well - - Returns - ------------ - is_collision : bool - True if a collision occurs and False otherwise - names : set of str - [OPTIONAL] The set of names of objects that collided with the - provided one - contacts : list of ContactData - [OPTIONAL] All contacts detected - """ - if transform is None: - transform = np.eye(4) - - # Create FCL data - b = self._get_BVH(mesh) - t = fcl.Transform(transform[:3, :3], transform[:3, 3]) - o = fcl.CollisionObject(b, t) - - # Collide with manager's objects - cdata = fcl.CollisionData() - if return_names or return_data: - cdata = fcl.CollisionData(request=fcl.CollisionRequest(num_max_contacts=100000, enable_contact=True)) - - self._manager.collide(o, cdata, fcl.defaultCollisionCallback) - result = cdata.result.is_collision - - # If we want to return the objects that were collision, collect them. - objs_in_collision = set() - contact_data = [] - if return_names or return_data: - for contact in cdata.result.contacts: - cg = contact.o1 - if cg == b: - cg = contact.o2 - name = self._extract_name(cg) - - names = (name, "__external") - if cg == contact.o2: - names = reversed(names) - - if return_names: - objs_in_collision.add(name) - if return_data: - contact_data.append(ContactData(names, contact)) - - if return_names and return_data: - return result, objs_in_collision, contact_data - elif return_names: - return result, objs_in_collision - elif return_data: - return result, contact_data - else: - return result - - def in_collision_internal(self, return_names=False, return_data=False): - """ - Check if any pair of objects in the manager collide with one another. - - Parameters - ---------- - return_names : bool - If true, a set is returned containing the names - of all pairs of objects in collision. - return_data : bool - If true, a list of ContactData is returned as well - - Returns - ------- - is_collision : bool - True if a collision occurred between any pair of objects - and False otherwise - names : set of 2-tup - The set of pairwise collisions. Each tuple - contains two names in alphabetical order indicating - that the two corresponding objects are in collision. - contacts : list of ContactData - All contacts detected - """ - cdata = fcl.CollisionData() - if return_names or return_data: - cdata = fcl.CollisionData(request=fcl.CollisionRequest(num_max_contacts=1000000, enable_contact=True)) - - self._manager.collide(cdata, fcl.defaultCollisionCallback) - - result = cdata.result.is_collision - - objs_in_collision = set() - contact_data = [] - if return_names or return_data: - for contact in cdata.result.contacts: - names = (self._extract_name(contact.o1), self._extract_name(contact.o2)) - - if return_names: - objs_in_collision.add(tuple(sorted(names))) - if return_data: - contact_data.append(ContactData(names, contact)) - - if return_names and return_data: - return result, objs_in_collision, contact_data - elif return_names: - return result, objs_in_collision - elif return_data: - return result, contact_data - else: - return result - - def in_collision_other(self, other_manager, return_names=False, return_data=False): - """ - Check if any object from this manager collides with any object - from another manager. - - Parameters - ------------------- - other_manager : CollisionManager - Another collision manager object - return_names : bool - If true, a set is returned containing the names - of all pairs of objects in collision. - return_data : bool - If true, a list of ContactData is returned as well - - Returns - ------------- - is_collision : bool - True if a collision occurred between any pair of objects - and False otherwise - names : set of 2-tup - The set of pairwise collisions. Each tuple - contains two names (first from this manager, - second from the other_manager) indicating - that the two corresponding objects are in collision. - contacts : list of ContactData - All contacts detected - """ - cdata = fcl.CollisionData() - if return_names or return_data: - cdata = fcl.CollisionData(request=fcl.CollisionRequest(num_max_contacts=100000, enable_contact=True)) - self._manager.collide(other_manager._manager, cdata, fcl.defaultCollisionCallback) - result = cdata.result.is_collision - - objs_in_collision = set() - contact_data = [] - if return_names or return_data: - for contact in cdata.result.contacts: - reverse = False - names = (self._extract_name(contact.o1), other_manager._extract_name(contact.o2)) - if names[0] is None: - names = (self._extract_name(contact.o2), other_manager._extract_name(contact.o1)) - reverse = True - - if return_names: - objs_in_collision.add(names) - if return_data: - if reverse: - names = reversed(names) - contact_data.append(ContactData(names, contact)) - - if return_names and return_data: - return result, objs_in_collision, contact_data - elif return_names: - return result, objs_in_collision - elif return_data: - return result, contact_data - else: - return result - - def min_distance_single(self, mesh, transform=None, return_name=False, return_data=False): - """ - Get the minimum distance between a single object and any - object in the manager. - - Parameters - --------------- - mesh : Trimesh object - The geometry of the collision object - transform : (4,4) float - Homogeneous transform matrix for the object - return_names : bool - If true, return name of the closest object - return_data : bool - If true, a DistanceData object is returned as well - - Returns - ------------- - distance : float - Min distance between mesh and any object in the manager - name : str - The name of the object in the manager that was closest - data : DistanceData - Extra data about the distance query - """ - if transform is None: - transform = np.eye(4) - - # Create FCL data - b = self._get_BVH(mesh) - - t = fcl.Transform(transform[:3, :3], transform[:3, 3]) - o = fcl.CollisionObject(b, t) - - # Collide with manager's objects - ddata = fcl.DistanceData() - if return_data: - ddata = fcl.DistanceData(fcl.DistanceRequest(enable_nearest_points=True), fcl.DistanceResult()) - - self._manager.distance(o, ddata, fcl.defaultDistanceCallback) - - distance = ddata.result.min_distance - - # If we want to return the objects that were collision, collect them. - name, data = None, None - if return_name or return_data: - cg = ddata.result.o1 - if cg == b: - cg = ddata.result.o2 - - name = self._extract_name(cg) - - names = (name, "__external") - if cg == ddata.result.o2: - names = reversed(names) - data = DistanceData(names, ddata.result) - - if return_name and return_data: - return distance, name, data - elif return_name: - return distance, name - elif return_data: - return distance, data - else: - return distance - - def min_distance_internal(self, return_names=False, return_data=False): - """ - Get the minimum distance between any pair of objects in the manager. - - Parameters - ------------- - return_names : bool - If true, a 2-tuple is returned containing the names - of the closest objects. - return_data : bool - If true, a DistanceData object is returned as well - - Returns - ----------- - distance : float - Min distance between any two managed objects - names : (2,) str - The names of the closest objects - data : DistanceData - Extra data about the distance query - """ - ddata = fcl.DistanceData() - if return_data: - ddata = fcl.DistanceData(fcl.DistanceRequest(enable_nearest_points=True), fcl.DistanceResult()) - - self._manager.distance(ddata, fcl.defaultDistanceCallback) - - distance = ddata.result.min_distance - - names, data = None, None - if return_names or return_data: - names = (self._extract_name(ddata.result.o1), self._extract_name(ddata.result.o2)) - data = DistanceData(names, ddata.result) - names = tuple(sorted(names)) - - if return_names and return_data: - return distance, names, data - elif return_names: - return distance, names - elif return_data: - return distance, data - else: - return distance - - def min_distance_other(self, other_manager, return_names=False, return_data=False): - """ - Get the minimum distance between any pair of objects, - one in each manager. - - Parameters - ---------- - other_manager : CollisionManager - Another collision manager object - return_names : bool - If true, a 2-tuple is returned containing - the names of the closest objects. - return_data : bool - If true, a DistanceData object is returned as well - - Returns - ----------- - distance : float - The min distance between a pair of objects, - one from each manager. - names : 2-tup of str - A 2-tuple containing two names (first from this manager, - second from the other_manager) indicating - the two closest objects. - data : DistanceData - Extra data about the distance query - """ - ddata = fcl.DistanceData() - if return_data: - ddata = fcl.DistanceData(fcl.DistanceRequest(enable_nearest_points=True), fcl.DistanceResult()) - - self._manager.distance(other_manager._manager, ddata, fcl.defaultDistanceCallback) - - distance = ddata.result.min_distance - - names, data = None, None - if return_names or return_data: - reverse = False - names = (self._extract_name(ddata.result.o1), other_manager._extract_name(ddata.result.o2)) - if names[0] is None: - reverse = True - names = (self._extract_name(ddata.result.o2), other_manager._extract_name(ddata.result.o1)) - - dnames = tuple(names) - if reverse: - dnames = reversed(dnames) - data = DistanceData(dnames, ddata.result) - - if return_names and return_data: - return distance, names, data - elif return_names: - return distance, names - elif return_data: - return distance, data - else: - return distance - - def _get_BVH(self, mesh): - """ - Get a BVH for a mesh. - - Parameters - ------------- - mesh : Trimesh - Mesh to create BVH for - - Returns - -------------- - bvh : fcl.BVHModel - BVH object of source mesh - """ - bvh = mesh_to_BVH(mesh) - return bvh - - def _extract_name(self, geom): - """ - Retrieve the name of an object from the manager by its - CollisionObject, or return None if not found. - - Parameters - ----------- - geom : CollisionObject or BVHModel - Input model - - Returns - ------------ - names : hashable - Name of input geometry - """ - return self._names[id(geom)] - - -def mesh_to_BVH(mesh): - """ - Create a BVHModel object from a Trimesh object - - Parameters - ----------- - mesh : Trimesh - Input geometry - - Returns - ------------ - bvh : fcl.BVHModel - BVH of input geometry - """ - bvh = fcl.BVHModel() - bvh.beginModel(num_tris_=len(mesh.faces), num_vertices_=len(mesh.vertices)) - bvh.addSubModel(verts=mesh.vertices, triangles=mesh.faces) - bvh.endModel() - return bvh - - -def scene_to_collision(scene): - """ - Create collision objects from a trimesh.Scene object. - - Parameters - ------------ - scene : trimesh.Scene - Scene to create collision objects for - - Returns - ------------ - manager : CollisionManager - CollisionManager for objects in scene - objects: {node name: CollisionObject} - Collision objects for nodes in scene - """ - manager = CollisionManager() - objects = {} - for node in scene.graph.nodes_geometry: - T, geometry = scene.graph[node] - objects[node] = manager.add_object(name=node, mesh=scene.geometry[geometry], transform=T) - return manager, objects diff --git a/src/ifcclash/ifcclash.py b/src/ifcclash/ifcclash.py deleted file mode 100644 index a17cc79d00..0000000000 --- a/src/ifcclash/ifcclash.py +++ /dev/null @@ -1,454 +0,0 @@ -#!/usr/bin/env python3 - -import collision -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.util.selector -import multiprocessing -import numpy as np -import json -import sys -import argparse -import logging - - -class Mesh: - faces: [] - vertices: [] - - -class IfcClasher: - def __init__(self, settings): - self.settings = settings - self.geom_settings = ifcopenshell.geom.settings() - self.clash_sets = [] - self.clash_data = {"meshes": {}} - self.global_data = {"meshes": {}, "matrices": {}} - - def clash(self): - for clash_set in self.clash_sets: - self.process_clash_set(clash_set) - - def process_clash_set(self, clash_set): - for ab in ["a", "b"]: - self.settings.logger.info(f"Creating collision manager {ab} ...") - clash_set[f"{ab}_cm"] = collision.CollisionManager() - self.settings.logger.info(f"Loading files {ab} ...") - for data in clash_set[ab]: - data["ifc"] = ifcopenshell.open(data["file"]) - self.patch_ifc(data["ifc"]) - self.settings.logger.info(f"Creating collision data for {ab} ...") - if len(data["ifc"].by_type("IfcElement")) > 0: - self.add_collision_objects(data, clash_set[f"{ab}_cm"]) - - if "b" in clash_set and clash_set["b"]: - results = clash_set["a_cm"].in_collision_other(clash_set["b_cm"], return_data=True) - else: - results = clash_set["a_cm"].in_collision_internal(return_data=True) - - if not results[0]: - return - - tolerance = clash_set["tolerance"] if "tolerance" in clash_set else 0.01 - clash_set["clashes"] = {} - - for contact in results[1]: - a_global_id, b_global_id = contact.names - a = self.get_element(clash_set["a"], a_global_id) - if "b" in clash_set and clash_set["b"]: - b = self.get_element(clash_set["b"], b_global_id) - else: - b = self.get_element(clash_set["a"], b_global_id) - if contact.raw.penetration_depth < tolerance: - continue - - # fcl returns contact data for faces that aren't actually - # penetrating, but just touching. If our tolerance is zero, then we - # consider these as clashes and we move on. If our tolerance is not - # zero, fcl has a strange behaviour where the penetration depth can - # be a large number even though objects are just touching - # https://github.com/flexible-collision-library/fcl/issues/503 In - # this case, I don't trust the penetration depth and I run my own - # triangle-triangle intersection test. Optimistically, this skips - # the false positives. Conservatively, we let the user manually deal - # with the false positives and we mark it as a clash. - is_optimistic = True # TODO: let user configure this - - if is_optimistic and tolerance != 0: - # We'll now check if the contact data's two faces are actually - # intersecting, using this brute force check: - # https://stackoverflow.com/questions/7113344/find-whether-two-triangles-intersect-or-not - # I'm not very good at this kind of code. If you know this stuff - # please help rewrite this. - - # Get vertices of clashing tris - p1 = self.global_data["meshes"][contact.names[0]].faces[contact.index(contact.names[0])] - p2 = self.global_data["meshes"][contact.names[1]].faces[contact.index(contact.names[1])] - m1 = self.global_data["matrices"][contact.names[0]] - m2 = self.global_data["matrices"][contact.names[1]] - v1 = [] - v2 = [] - - for v in p1: - v1.append( - (m1 @ np.array([*self.global_data["meshes"][contact.names[0]].vertices[v], 1]))[0:3].round(2) - ) - for v in p2: - v2.append( - (m2 @ np.array([*self.global_data["meshes"][contact.names[1]].vertices[v], 1]))[0:3].round(2) - ) - - tri1_x = 0 - tri2_x = 0 - tri1_x += 1 if self.intersect_line_triangle(v1[0], v1[1], v2[0], v2[1], v2[2]) is not None else 0 - tri1_x += 1 if self.intersect_line_triangle(v1[1], v1[2], v2[0], v2[1], v2[2]) is not None else 0 - tri1_x += 1 if self.intersect_line_triangle(v1[2], v1[0], v2[0], v2[1], v2[2]) is not None else 0 - - tri2_x += 1 if self.intersect_line_triangle(v2[0], v2[1], v1[0], v1[1], v1[2]) is not None else 0 - tri2_x += 1 if self.intersect_line_triangle(v2[1], v2[2], v1[0], v1[1], v1[2]) is not None else 0 - tri2_x += 1 if self.intersect_line_triangle(v2[2], v2[0], v1[0], v1[1], v1[2]) is not None else 0 - intersections = [tri1_x, tri2_x] - if intersections == [0, 2] or intersections == [2, 0] or intersections == [1, 1]: - # This is a penetrating collision - pass - else: - # This is probably two triangles which just touch - continue - - key = f"{a_global_id}-{b_global_id}" - - if ( - key in clash_set["clashes"] - and clash_set["clashes"][key]["penetration_depth"] > contact.raw.penetration_depth - ): - continue - - clash_set["clashes"][key] = { - "a_global_id": a_global_id, - "b_global_id": b_global_id, - "a_ifc_class": a.is_a(), - "b_ifc_class": b.is_a(), - "a_name": a.Name, - "b_name": b.Name, - "normal": list(contact.raw.normal), - "position": list(contact.raw.pos), - "penetration_depth": contact.raw.penetration_depth, - } - - # https://stackoverflow.com/questions/42740765/intersection-between-line-and-triangle-in-3d - def intersect_line_triangle(self, q1, q2, p1, p2, p3): - def signed_tetra_volume(a, b, c, d): - return np.sign(np.dot(np.cross(b - a, c - a), d - a) / 6.0) - - s1 = signed_tetra_volume(q1, p1, p2, p3) - s2 = signed_tetra_volume(q2, p1, p2, p3) - - if s1 != s2: - s3 = signed_tetra_volume(q1, q2, p1, p2) - s4 = signed_tetra_volume(q1, q2, p2, p3) - s5 = signed_tetra_volume(q1, q2, p3, p1) - if s3 == s4 and s4 == s5: - n = np.cross(p2 - p1, p3 - p1) - t = -np.dot(q1, n - p1) / np.dot(q1, q2 - q1) - return q1 + t * (q2 - q1) - return None - - def export(self): - if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": - return self.export_bcfxml() - self.export_json() - - def export_bcfxml(self): - import bcf - import bcf.bcfxml - - for i, clash_set in enumerate(self.clash_sets): - bcfxml = bcf.bcfxml.BcfXml() - bcfxml.new_project() - bcfxml.project.name = clash_set["name"] - bcfxml.edit_project() - for key, clash in clash_set["clashes"].items(): - topic = bcf.data.Topic() - topic.title = "{}/{} and {}/{}".format( - clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"] - ) - topic = bcfxml.add_topic(topic) - viewpoint = bcf.data.Viewpoint() - viewpoint.perspective_camera = bcf.data.PerspectiveCamera() - position = np.array(clash["position"]) - point = position + np.array((5, 5, 5)) # Dumb, but works! - viewpoint.perspective_camera.camera_view_point.x = point[0] - viewpoint.perspective_camera.camera_view_point.y = point[1] - viewpoint.perspective_camera.camera_view_point.z = point[2] - mat = self.get_track_to_matrix(point, position) - viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1 - viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1 - viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1 - viewpoint.perspective_camera.camera_up_vector.x = mat[0][1] - viewpoint.perspective_camera.camera_up_vector.y = mat[1][1] - viewpoint.perspective_camera.camera_up_vector.z = mat[2][1] - viewpoint.components = bcf.data.Components() - c1 = bcf.data.Component() - c1.ifc_guid = clash["a_global_id"] - c2 = bcf.data.Component() - c2.ifc_guid = clash["b_global_id"] - viewpoint.components.selection.append(c1) - viewpoint.components.selection.append(c2) - viewpoint.components.visibility = bcf.data.ComponentVisibility() - viewpoint.components.visibility.default_visibility = True - viewpoint.snapshot = self.get_viewpoint_snapshot(viewpoint, mat) - bcfxml.add_viewpoint(topic, viewpoint) - if i == 0: - bcfxml.save_project(self.settings.output) - else: - bcfxml.save_project(self.settings.output + f".{i}") - - def get_viewpoint_snapshot(self, viewpoint, mat): - return None # Possible to overload this function in a GUI application if used as a library - - # https://blender.stackexchange.com/questions/68834/recreate-to-track-quat-with-two-vectors-using-python/141706#141706 - def get_track_to_matrix(self, camera_position, target_position): - camera_direction = camera_position - target_position - camera_direction = camera_direction / np.linalg.norm(camera_direction) - camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction) - camera_right = camera_right / np.linalg.norm(camera_right) - camera_up = np.cross(camera_direction, camera_right) - camera_up = camera_up / np.linalg.norm(camera_up) - rotation_transform = np.zeros((4, 4)) - rotation_transform[0, :3] = camera_right - rotation_transform[1, :3] = camera_up - rotation_transform[2, :3] = camera_direction - rotation_transform[-1, -1] = 1 - translation_transform = np.eye(4) - translation_transform[:3, -1] = - camera_position - look_at_transform = np.matmul(rotation_transform, translation_transform) - return np.linalg.inv(look_at_transform) - - def export_json(self): - results = self.clash_sets.copy() - for result in results: - del result["a_cm"] - del result["b_cm"] - for ab in ["a", "b"]: - for data in result[ab]: - if "ifc" in data: - del data["ifc"] - with open(self.settings.output, "w", encoding="utf-8") as clashes_file: - json.dump(results, clashes_file, indent=4) - - def get_element(self, clash_group, global_id): - for data in clash_group: - try: - element = data["ifc"].by_guid(global_id) - if element: - return element - except: - pass - - def add_collision_objects(self, data, cm): - self.clash_data["meshes"] = {} - selector = ifcopenshell.util.selector.Selector() - if "selector" not in data: - iterator = ifcopenshell.geom.iterator( - self.geom_settings, - data["ifc"], - multiprocessing.cpu_count(), - exclude=(data["ifc"].by_type("IfcSpatialStructureElement")), - ) - elif data["mode"] == "e": - iterator = ifcopenshell.geom.iterator( - self.geom_settings, - data["ifc"], - multiprocessing.cpu_count(), - exclude=selector.parse(data["ifc"], data["selector"]), - ) - elif data["mode"] == "i": - iterator = ifcopenshell.geom.iterator( - self.geom_settings, - data["ifc"], - multiprocessing.cpu_count(), - include=selector.parse(data["ifc"], data["selector"]), - ) - valid_file = iterator.initialize() - if not valid_file: - return False - old_progress = -1 - while True: - progress = iterator.progress() // 2 - if progress > old_progress: - print("\r[" + "#" * progress + " " * (50 - progress) + "]", end="") - old_progress = progress - self.add_collision_object(data, cm, iterator.get()) - if not iterator.next(): - break - - def add_collision_object(self, data, cm, shape): - if shape is None: - return - element = data["ifc"].by_id(shape.guid) - self.settings.logger.info("Creating object {}".format(element)) - mesh_name = f"mesh-{shape.geometry.id}" - if mesh_name in self.clash_data["meshes"]: - mesh = self.clash_data["meshes"][mesh_name] - else: - mesh = self.create_mesh(shape) - self.clash_data["meshes"][mesh_name] = mesh - self.global_data["meshes"][shape.guid] = mesh - - m = shape.transformation.matrix.data - 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() - self.global_data["matrices"][shape.guid] = mat - cm.add_object(shape.guid, mesh, mat) - - def create_mesh(self, shape): - f = shape.geometry.faces - v = shape.geometry.verts - mesh = Mesh() - mesh.vertices = np.array([[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]) - mesh.faces = np.array([[f[i], f[i + 1], f[i + 2]] for i in range(0, len(f), 3)]) - return mesh - - def patch_ifc(self, ifc_file): - project = ifc_file.by_type("IfcProject")[0] - sites = self.find_decomposed_ifc_class(project, "IfcSite") - for site in sites: - self.patch_placement_to_origin(site) - buildings = self.find_decomposed_ifc_class(project, "IfcBuilding") - for building in buildings: - self.patch_placement_to_origin(building) - - def find_decomposed_ifc_class(self, element, ifc_class): - results = [] - rel_aggregates = element.IsDecomposedBy - if not rel_aggregates: - return results - for rel_aggregate in rel_aggregates: - for part in rel_aggregate.RelatedObjects: - if part.is_a(ifc_class): - results.append(part) - results.extend(self.find_decomposed_ifc_class(part, ifc_class)) - return results - - def patch_placement_to_origin(self, element): - element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, 0.0) - if element.ObjectPlacement.RelativePlacement.Axis: - element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0.0, 0.0, 1.0) - if element.ObjectPlacement.RelativePlacement.RefDirection: - element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1.0, 0.0, 0.0) - - def smart_group_clashes(self, clash_sets, max_clustering_distance): - from sklearn.cluster import OPTICS - from collections import defaultdict - - count_of_input_clashes = 0 - count_of_clash_sets = 0 - count_of_smart_groups = 0 - count_of_final_clash_sets = 0 - - count_of_clash_sets = len(clash_sets) - - for clash_set in clash_sets: - if not "clashes" in clash_set.keys(): - print(f"Skipping clash set [{clash_set['name']}] since it contains no clash results.") - continue - clashes = clash_set["clashes"] - if len(clashes) == 0: - print(f"Skipping clash set [{clash_set['name']}] since it contains no clash results.") - continue - - count_of_input_clashes += len(clashes) - - positions = [] - for clash in clashes.values(): - positions.append(clash["position"]) - - data = np.array(positions) - - # INPUTS - # set the desired maximum distance between the grouped points - if max_clustering_distance > 0: - max_distance_between_grouped_points = max_clustering_distance - else: - max_distance_between_grouped_points = 3 - - model = OPTICS(min_samples=2, max_eps=max_distance_between_grouped_points) - model.fit_predict(data) - pred = model.fit_predict(data) - - # Insert the smart groups into the clashes - if len(pred) == len(clashes.values()): - i = 0 - for clash in clashes.values(): - int_prediction = int(pred[i]) - if int_prediction == -1: - # ungroup this clash since it's a single clash that we were not able to group. - new_clash_group_number = np.amax(pred).item() + 1 + i - clash["smart_group"] = new_clash_group_number - else: - clash["smart_group"] = int_prediction - i += 1 - - # Create JSON with smart_groups that contain GlobalIDs - output_clash_sets = defaultdict(list) - for clash_set in clash_sets: - if not "clashes" in clash_set.keys(): - continue - smart_groups = defaultdict(list) - for clash_id, content in clash_set["clashes"].items(): - if "smart_group" in content: - object_id_list = list() - # Clash has been grouped, let's extract it. - object_id_list.append(content["a_global_id"]) - object_id_list.append(content["b_global_id"]) - smart_groups[content["smart_group"]].append(object_id_list) - count_of_smart_groups += len(smart_groups) - output_clash_sets[clash_set["name"]].append(smart_groups) - - # Rename the clash groups to something more sensible - for clash_set, smart_groups in output_clash_sets.items(): - clash_set_name = clash_set - # Only select the clashes that correspond to the actively selected IFC Clash Set - i = 1 - new_smart_group_name = "" - for smart_group, global_id_pairs in list(smart_groups[0].items()): - new_smart_group_name = f"{clash_set_name} - {i}" - smart_groups[0][new_smart_group_name] = smart_groups[0].pop(smart_group) - i += 1 - - count_of_final_clash_sets = len(output_clash_sets) - print( - f"Took {count_of_input_clashes} clashes in {count_of_clash_sets} clash sets and turned", - f"them into {count_of_smart_groups} smart groups in {count_of_final_clash_sets} clash sets", - ) - - return output_clash_sets - - -class IfcClashSettings: - def __init__(self): - self.logger = None - self.output = "clashes.json" - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Clashes geometry between two IFC files") - parser.add_argument("input", type=str, help="A JSON dataset describing a series of clashsets") - parser.add_argument( - "-o", "--output", type=str, help="The JSON diff file to output. Defaults to output.json", default="output.json" - ) - args = parser.parse_args() - - settings = IfcClashSettings() - settings.output = args.output - settings.logger = logging.getLogger("Clash") - settings.logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stdout) - handler.setLevel(logging.DEBUG) - settings.logger.addHandler(handler) - ifc_clasher = IfcClasher(settings) - with open(args.input, "r") as clash_sets_file: - ifc_clasher.clash_sets = json.loads(clash_sets_file.read()) - ifc_clasher.clash() - ifc_clasher.export() diff --git a/src/ifcclash/ifcclash/collider.py b/src/ifcclash/ifcclash/collider.py index 710ad3c1a4..a2fd0314d4 100644 --- a/src/ifcclash/ifcclash/collider.py +++ b/src/ifcclash/ifcclash/collider.py @@ -1,77 +1,73 @@ import hppfcl import numpy as np -from aabbtree import AABB -from aabbtree import AABBTree +import ifcopenshell class Collider: def __init__(self): self.groups = {} + self.tree = ifcopenshell.geom.tree() def create_group(self, name): - self.groups[name] = {"tree": AABBTree(), "objects": {}} + self.groups[name] = {"elements": {}, "objects": {}} + + def create_objects(self, name, ifc_file, iterator, elements): + self.tree.add_iterator(iterator) + self.groups[name]["elements"].update({e.GlobalId: e for e in elements}) + + # Temporary hack. See #1357. + import multiprocessing + + iterator = ifcopenshell.geom.iterator( + ifcopenshell.geom.settings(), ifc_file, multiprocessing.cpu_count(), include=elements + ) + valid_file = iterator.initialize() + if not valid_file: + return False + while True: + shape = iterator.get() + self.create_object(name, shape.guid, shape) + if not iterator.next(): + break def create_object(self, group_name, id, shape): obj = hppfcl.CollisionObject( self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data) ) - aabb = obj.getAABB() - c = aabb.center() - x = aabb.width() - y = aabb.height() - z = aabb.depth() - aabb = AABB([(c[0] - x / 2, c[0] + x / 2), (c[1] - y / 2, c[1] + y / 2), (c[2] - z / 2, c[2] + z / 2)]) - self.groups[group_name]["tree"].add(aabb, id) - self.groups[group_name]["objects"][id] = (aabb, obj) + self.groups[group_name]["objects"][id] = obj def collide_internal(self, name): - print('starting internal collision') - return self.collide_narrowphase(self.collide_broadphase(name, name)) + return self.collide_narrowphase(name, name, self.collide_broadphase(name, name)) def collide_group(self, name1, name2): - print('starting group collision') - return self.collide_narrowphase(self.collide_broadphase(name1, name2)) + return self.collide_narrowphase(name1, name2, self.collide_broadphase(name1, name2)) def collide_broadphase(self, name1, name2): - print('Begin broad phase') potential_collisions = [] checked_collisions = set() - i = 0 - for id, obj_data in self.groups[name1]["objects"].items(): - aabb, obj = obj_data - collision_stack = [self.groups[name2]["tree"]] + for id, element in self.groups[name1]["elements"].items(): checked_collisions.add(id) - i += 1 - while i % 1000 == 0: - print(i, '...') - while collision_stack: - node = collision_stack.pop() - if node.value == id or node.value in checked_collisions: - continue - if node.does_overlap(aabb): - if node.is_leaf: - potential_collisions.append( - { - "id1": id, - "obj1": obj, - "id2": node.value, - "obj2": self.groups[name2]["objects"][node.value][1], - } - ) - else: - collision_stack.append(node.left) - collision_stack.append(node.right) + 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) return potential_collisions - def collide_narrowphase(self, potential_collisions): - print('Begin narrow phase') + def collide_narrowphase(self, name1, name2, potential_collisions): collisions = [] for data in potential_collisions: result = hppfcl.CollisionResult() - hppfcl.collide(data["obj1"], data["obj2"], hppfcl.CollisionRequest(), result) + 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}) - print({"id1": data["id1"], "id2": data["id2"], "collision": result}) return collisions def create_transform(self, m): diff --git a/src/ifcclash/ifcclash/ifcclash.py b/src/ifcclash/ifcclash/ifcclash.py index 6e51fa7e2c..6dd16c12e6 100644 --- a/src/ifcclash/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash/ifcclash.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.util.selector -import multiprocessing import numpy as np import json import sys import argparse import logging +import multiprocessing +import ifcopenshell +import ifcopenshell.geom +import ifcopenshell.util.selector from . import collider class Clasher: def __init__(self, settings): self.settings = settings - self.geom_settings = ifcopenshell.geom.settings() + self.geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True) self.clash_sets = [] self.collider = collider.Collider() self.selector = ifcopenshell.util.selector.Selector() @@ -23,36 +23,45 @@ class Clasher: def clash(self): existing_limit = sys.getrecursionlimit() - sys.setrecursionlimit(100000) for clash_set in self.clash_sets: self.process_clash_set(clash_set) - sys.setrecursionlimit(existing_limit) def process_clash_set(self, clash_set): - print("proccessings", clash_set) self.collider.create_group("a") for source in clash_set["a"]: - self.add_collision_objects( - "a", self.load_ifc(source["file"]), source.get("mode", None), source.get("selector", None) - ) + 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") for source in clash_set["b"]: - self.add_collision_objects( - "b", self.load_ifc(source["file"]), source.get("mode", None), source.get("selector", None) - ) + 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") else: results = self.collider.collide_internal("a") + processed_results = {} for result in results: - print("*" * 10) - print("Is Collision:", result["collision"].isCollision()) - print(result["id1"], result["id2"]) - print("Number of contacts:", result["collision"].numContacts()) - for contact in result["collision"].getContacts(): - print(contact) + 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["1"], result["id2"]) + + contact = result["collision"].getContacts()[0] + processed_results[f"{result['id1']}-{result['id2']}"] = { + "a_global_id": result["id1"], + "b_global_id": result["id2"], + "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, + } + clash_set["clashes"] = processed_results def load_ifc(self, path): ifc = self.ifcs.get(path, None) @@ -62,37 +71,17 @@ class Clasher: return ifc def add_collision_objects(self, name, ifc_file, mode=None, selector=None): - print('adding collision objects', name) if not mode: - iterator = ifcopenshell.geom.iterator( - self.geom_settings, - ifc_file, - multiprocessing.cpu_count(), - exclude=(ifc_file.by_type("IfcSpatialStructureElement")), - ) + elements = ifc_file.by_type("IfcElement") elif mode == "e": - iterator = ifcopenshell.geom.iterator( - self.geom_settings, - ifc_file, - multiprocessing.cpu_count(), - exclude=selector.parse(ifc_file, selector), - ) + exclude = self.selector.parse(ifc_file, selector) + elements = [e for e in ifc_file.by_type("IfcElement") if e not in exclude] elif mode == "i": - iterator = ifcopenshell.geom.iterator( - self.geom_settings, - ifc_file, - multiprocessing.cpu_count(), - include=selector.parse(ifc_file, selector), - ) - valid_file = iterator.initialize() - if not valid_file: - return False - old_progress = -1 - while True: - shape = iterator.get() - self.collider.create_object(name, shape.guid, shape) - if not iterator.next(): - break + elements = self.selector.parse(ifc_file, selector) + iterator = ifcopenshell.geom.iterator( + self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements + ) + self.collider.create_objects(name, ifc_file, iterator, elements) def export(self): if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": @@ -101,23 +90,23 @@ class Clasher: def export_bcfxml(self): import bcf - import bcf.bcfxml + import bcf.v2.bcfxml for i, clash_set in enumerate(self.clash_sets): - bcfxml = bcf.bcfxml.BcfXml() + bcfxml = bcf.v2.bcfxml.BcfXml() bcfxml.new_project() bcfxml.project.name = clash_set["name"] bcfxml.edit_project() for key, clash in clash_set["clashes"].items(): - topic = bcf.data.Topic() + topic = bcf.v2.data.Topic() topic.title = "{}/{} and {}/{}".format( clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"] ) topic = bcfxml.add_topic(topic) - viewpoint = bcf.data.Viewpoint() - viewpoint.perspective_camera = bcf.data.PerspectiveCamera() + viewpoint = bcf.v2.data.Viewpoint() + viewpoint.perspective_camera = bcf.v2.data.PerspectiveCamera() position = np.array(clash["position"]) - point = position + np.array((5, 5, 5)) # Dumb, but works! + point = position + np.array((5, 5, 5)) # Dumb, but works (for now)! viewpoint.perspective_camera.camera_view_point.x = point[0] viewpoint.perspective_camera.camera_view_point.y = point[1] viewpoint.perspective_camera.camera_view_point.z = point[2] @@ -128,14 +117,14 @@ class Clasher: viewpoint.perspective_camera.camera_up_vector.x = mat[0][1] viewpoint.perspective_camera.camera_up_vector.y = mat[1][1] viewpoint.perspective_camera.camera_up_vector.z = mat[2][1] - viewpoint.components = bcf.data.Components() - c1 = bcf.data.Component() + viewpoint.components = bcf.v2.data.Components() + c1 = bcf.v2.data.Component() c1.ifc_guid = clash["a_global_id"] - c2 = bcf.data.Component() + c2 = bcf.v2.data.Component() c2.ifc_guid = clash["b_global_id"] viewpoint.components.selection.append(c1) viewpoint.components.selection.append(c2) - viewpoint.components.visibility = bcf.data.ComponentVisibility() + viewpoint.components.visibility = bcf.v2.data.ComponentVisibility() viewpoint.components.visibility.default_visibility = True viewpoint.snapshot = self.get_viewpoint_snapshot(viewpoint, mat) bcfxml.add_viewpoint(topic, viewpoint) @@ -168,8 +157,6 @@ class Clasher: def export_json(self): results = self.clash_sets.copy() for result in results: - del result["a_cm"] - del result["b_cm"] for ab in ["a", "b"]: for data in result[ab]: if "ifc" in data: @@ -177,10 +164,10 @@ class Clasher: with open(self.settings.output, "w", encoding="utf-8") as clashes_file: json.dump(results, clashes_file, indent=4) - def get_element(self, clash_group, global_id): - for data in clash_group: + def get_element(self, clash_set, global_id): + for source in clash_set: try: - element = data["ifc"].by_guid(global_id) + element = source["ifc"].by_guid(global_id) if element: return element except: