From 44d6060924005a1fcfc285019a402be82b578595 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 21 Oct 2020 23:13:43 +1100 Subject: [PATCH] IFC clash now optimistically skips clashes that are likely to be merely coincident, thus resulting in less false positives --- src/ifcclash/ifcclash.py | 89 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 8 deletions(-) mode change 100755 => 100644 src/ifcclash/ifcclash.py diff --git a/src/ifcclash/ifcclash.py b/src/ifcclash/ifcclash.py old mode 100755 new mode 100644 index 359811bf59..9b9ea0a242 --- a/src/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash.py @@ -22,7 +22,8 @@ class IfcClasher: self.settings = settings self.geom_settings = ifcopenshell.geom.settings() self.clash_sets = [] - #self.tolerance = 0.01 + self.clash_data = {'meshes': {}} + self.global_data = {'meshes': {}, 'matrices': {}} def clash(self): for clash_set in self.clash_sets: @@ -62,10 +63,62 @@ class IfcClasher: 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, @@ -78,6 +131,24 @@ class IfcClasher: '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): results = self.clash_sets.copy() for result in results: @@ -85,9 +156,8 @@ class IfcClasher: del result['b_cm'] for ab in ['a', 'b']: for data in result[ab]: - for key in ['ifc', 'meshes']: - if key in data: - del data[key] + 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) @@ -119,7 +189,7 @@ class IfcClasher: data['ifc'].remove(element) def add_collision_objects(self, data, cm): - data['meshes'] = {} + self.clash_data['meshes'] = {} iterator = ifcopenshell.geom.iterator(self.geom_settings, data['ifc'], multiprocessing.cpu_count()) valid_file = iterator.initialize() if not valid_file: @@ -140,11 +210,12 @@ class IfcClasher: 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 data['meshes']: - mesh = data['meshes'][mesh_name] + if mesh_name in self.clash_data['meshes']: + mesh = self.clash_data['meshes'][mesh_name] else: mesh = self.create_mesh(shape) - data['meshes'][mesh_name] = mesh + self.clash_data['meshes'][mesh_name] = mesh + self.global_data['meshes'][shape.guid] = mesh m = shape.transformation.matrix.data mat = np.array( @@ -155,7 +226,9 @@ class IfcClasher: [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):