IFC clash now optimistically skips clashes that are likely to be merely coincident, thus resulting in less false positives

This commit is contained in:
Dion Moult
2020-10-21 23:13:43 +11:00
parent 95d6685597
commit 44d6060924
Executable → Regular
+81 -8
View File
@@ -22,7 +22,8 @@ class IfcClasher:
self.settings = settings self.settings = settings
self.geom_settings = ifcopenshell.geom.settings() self.geom_settings = ifcopenshell.geom.settings()
self.clash_sets = [] self.clash_sets = []
#self.tolerance = 0.01 self.clash_data = {'meshes': {}}
self.global_data = {'meshes': {}, 'matrices': {}}
def clash(self): def clash(self):
for clash_set in self.clash_sets: for clash_set in self.clash_sets:
@@ -62,10 +63,62 @@ class IfcClasher:
b = self.get_element(clash_set['a'], b_global_id) b = self.get_element(clash_set['a'], b_global_id)
if contact.raw.penetration_depth < tolerance: if contact.raw.penetration_depth < tolerance:
continue 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}' key = f'{a_global_id}-{b_global_id}'
if key in clash_set['clashes'] \ if key in clash_set['clashes'] \
and clash_set['clashes'][key]['penetration_depth'] > contact.raw.penetration_depth: and clash_set['clashes'][key]['penetration_depth'] > contact.raw.penetration_depth:
continue continue
clash_set['clashes'][key] = { clash_set['clashes'][key] = {
'a_global_id': a_global_id, 'a_global_id': a_global_id,
'b_global_id': b_global_id, 'b_global_id': b_global_id,
@@ -78,6 +131,24 @@ class IfcClasher:
'penetration_depth': contact.raw.penetration_depth '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): def export(self):
results = self.clash_sets.copy() results = self.clash_sets.copy()
for result in results: for result in results:
@@ -85,9 +156,8 @@ class IfcClasher:
del result['b_cm'] del result['b_cm']
for ab in ['a', 'b']: for ab in ['a', 'b']:
for data in result[ab]: for data in result[ab]:
for key in ['ifc', 'meshes']: if 'ifc' in data:
if key in data: del data['ifc']
del data[key]
with open(self.settings.output, 'w', encoding='utf-8') as clashes_file: with open(self.settings.output, 'w', encoding='utf-8') as clashes_file:
json.dump(results, clashes_file, indent=4) json.dump(results, clashes_file, indent=4)
@@ -119,7 +189,7 @@ class IfcClasher:
data['ifc'].remove(element) data['ifc'].remove(element)
def add_collision_objects(self, data, cm): 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()) iterator = ifcopenshell.geom.iterator(self.geom_settings, data['ifc'], multiprocessing.cpu_count())
valid_file = iterator.initialize() valid_file = iterator.initialize()
if not valid_file: if not valid_file:
@@ -140,11 +210,12 @@ class IfcClasher:
element = data['ifc'].by_id(shape.guid) element = data['ifc'].by_id(shape.guid)
self.settings.logger.info('Creating object {}'.format(element)) self.settings.logger.info('Creating object {}'.format(element))
mesh_name = f'mesh-{shape.geometry.id}' mesh_name = f'mesh-{shape.geometry.id}'
if mesh_name in data['meshes']: if mesh_name in self.clash_data['meshes']:
mesh = data['meshes'][mesh_name] mesh = self.clash_data['meshes'][mesh_name]
else: else:
mesh = self.create_mesh(shape) 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 m = shape.transformation.matrix.data
mat = np.array( mat = np.array(
@@ -155,7 +226,9 @@ class IfcClasher:
[0, 0, 0, 1] [0, 0, 0, 1]
] ]
) )
mat.transpose() mat.transpose()
self.global_data['matrices'][shape.guid] = mat
cm.add_object(shape.guid, mesh, mat) cm.add_object(shape.guid, mesh, mat)
def create_mesh(self, shape): def create_mesh(self, shape):