New ifcopenshell.geom.tree based broadphase collision for IfcClash instead of aabbtree. Also, upgrades for new bcf.v2 namespace implemented. See #1357.

This commit is contained in:
Dion Moult
2021-08-02 20:32:16 +10:00
parent 2137b34063
commit 557f8e6d69
4 changed files with 90 additions and 1224 deletions
-663
View File
@@ -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
-454
View File
@@ -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()
+40 -44
View File
@@ -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):
+50 -63
View File
@@ -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: