mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-25 09:32:37 +00:00
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:
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -1,77 +1,73 @@
|
|||||||
import hppfcl
|
import hppfcl
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from aabbtree import AABB
|
import ifcopenshell
|
||||||
from aabbtree import AABBTree
|
|
||||||
|
|
||||||
|
|
||||||
class Collider:
|
class Collider:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.groups = {}
|
self.groups = {}
|
||||||
|
self.tree = ifcopenshell.geom.tree()
|
||||||
|
|
||||||
def create_group(self, name):
|
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):
|
def create_object(self, group_name, id, shape):
|
||||||
obj = hppfcl.CollisionObject(
|
obj = hppfcl.CollisionObject(
|
||||||
self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data)
|
self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data)
|
||||||
)
|
)
|
||||||
aabb = obj.getAABB()
|
self.groups[group_name]["objects"][id] = obj
|
||||||
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)
|
|
||||||
|
|
||||||
def collide_internal(self, name):
|
def collide_internal(self, name):
|
||||||
print('starting internal collision')
|
return self.collide_narrowphase(name, name, self.collide_broadphase(name, name))
|
||||||
return self.collide_narrowphase(self.collide_broadphase(name, name))
|
|
||||||
|
|
||||||
def collide_group(self, name1, name2):
|
def collide_group(self, name1, name2):
|
||||||
print('starting group collision')
|
return self.collide_narrowphase(name1, name2, self.collide_broadphase(name1, name2))
|
||||||
return self.collide_narrowphase(self.collide_broadphase(name1, name2))
|
|
||||||
|
|
||||||
def collide_broadphase(self, name1, name2):
|
def collide_broadphase(self, name1, name2):
|
||||||
print('Begin broad phase')
|
|
||||||
potential_collisions = []
|
potential_collisions = []
|
||||||
checked_collisions = set()
|
checked_collisions = set()
|
||||||
i = 0
|
for id, element in self.groups[name1]["elements"].items():
|
||||||
for id, obj_data in self.groups[name1]["objects"].items():
|
|
||||||
aabb, obj = obj_data
|
|
||||||
collision_stack = [self.groups[name2]["tree"]]
|
|
||||||
checked_collisions.add(id)
|
checked_collisions.add(id)
|
||||||
i += 1
|
box_filter = self.tree.select_box(element)
|
||||||
while i % 1000 == 0:
|
pairs = [
|
||||||
print(i, '...')
|
{"id1": id, "id2": e.GlobalId}
|
||||||
while collision_stack:
|
for e in box_filter
|
||||||
node = collision_stack.pop()
|
if e.GlobalId not in checked_collisions and e.GlobalId in self.groups[name2]["elements"]
|
||||||
if node.value == id or node.value in checked_collisions:
|
]
|
||||||
continue
|
potential_collisions.extend(pairs)
|
||||||
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)
|
|
||||||
return potential_collisions
|
return potential_collisions
|
||||||
|
|
||||||
def collide_narrowphase(self, potential_collisions):
|
def collide_narrowphase(self, name1, name2, potential_collisions):
|
||||||
print('Begin narrow phase')
|
|
||||||
collisions = []
|
collisions = []
|
||||||
for data in potential_collisions:
|
for data in potential_collisions:
|
||||||
result = hppfcl.CollisionResult()
|
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():
|
if result.isCollision():
|
||||||
collisions.append({"id1": data["id1"], "id2": data["id2"], "collision": result})
|
collisions.append({"id1": data["id1"], "id2": data["id2"], "collision": result})
|
||||||
print({"id1": data["id1"], "id2": data["id2"], "collision": result})
|
|
||||||
return collisions
|
return collisions
|
||||||
|
|
||||||
def create_transform(self, m):
|
def create_transform(self, m):
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import ifcopenshell
|
|
||||||
import ifcopenshell.geom
|
|
||||||
import ifcopenshell.util.selector
|
|
||||||
import multiprocessing
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import ifcopenshell
|
||||||
|
import ifcopenshell.geom
|
||||||
|
import ifcopenshell.util.selector
|
||||||
from . import collider
|
from . import collider
|
||||||
|
|
||||||
|
|
||||||
class Clasher:
|
class Clasher:
|
||||||
def __init__(self, settings):
|
def __init__(self, settings):
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.geom_settings = ifcopenshell.geom.settings()
|
self.geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True)
|
||||||
self.clash_sets = []
|
self.clash_sets = []
|
||||||
self.collider = collider.Collider()
|
self.collider = collider.Collider()
|
||||||
self.selector = ifcopenshell.util.selector.Selector()
|
self.selector = ifcopenshell.util.selector.Selector()
|
||||||
@@ -23,36 +23,45 @@ class Clasher:
|
|||||||
|
|
||||||
def clash(self):
|
def clash(self):
|
||||||
existing_limit = sys.getrecursionlimit()
|
existing_limit = sys.getrecursionlimit()
|
||||||
sys.setrecursionlimit(100000)
|
|
||||||
for clash_set in self.clash_sets:
|
for clash_set in self.clash_sets:
|
||||||
self.process_clash_set(clash_set)
|
self.process_clash_set(clash_set)
|
||||||
sys.setrecursionlimit(existing_limit)
|
|
||||||
|
|
||||||
def process_clash_set(self, clash_set):
|
def process_clash_set(self, clash_set):
|
||||||
print("proccessings", clash_set)
|
|
||||||
self.collider.create_group("a")
|
self.collider.create_group("a")
|
||||||
for source in clash_set["a"]:
|
for source in clash_set["a"]:
|
||||||
self.add_collision_objects(
|
source["ifc"] = self.load_ifc(source["file"])
|
||||||
"a", self.load_ifc(source["file"]), source.get("mode", None), source.get("selector", None)
|
self.add_collision_objects("a", source["ifc"], source.get("mode", None), source.get("selector", None))
|
||||||
)
|
|
||||||
|
|
||||||
if "b" in clash_set:
|
if "b" in clash_set:
|
||||||
self.collider.create_group("b")
|
self.collider.create_group("b")
|
||||||
for source in clash_set["b"]:
|
for source in clash_set["b"]:
|
||||||
self.add_collision_objects(
|
source["ifc"] = self.load_ifc(source["file"])
|
||||||
"b", self.load_ifc(source["file"]), source.get("mode", None), source.get("selector", None)
|
self.add_collision_objects("b", source["ifc"], source.get("mode", None), source.get("selector", None))
|
||||||
)
|
|
||||||
results = self.collider.collide_group("a", "b")
|
results = self.collider.collide_group("a", "b")
|
||||||
else:
|
else:
|
||||||
results = self.collider.collide_internal("a")
|
results = self.collider.collide_internal("a")
|
||||||
|
|
||||||
|
processed_results = {}
|
||||||
for result in results:
|
for result in results:
|
||||||
print("*" * 10)
|
element1 = self.get_element(clash_set["a"], result["id1"])
|
||||||
print("Is Collision:", result["collision"].isCollision())
|
if "b" in clash_set:
|
||||||
print(result["id1"], result["id2"])
|
element2 = self.get_element(clash_set["b"], result["id2"])
|
||||||
print("Number of contacts:", result["collision"].numContacts())
|
else:
|
||||||
for contact in result["collision"].getContacts():
|
element2 = self.get_element(clash_set["1"], result["id2"])
|
||||||
print(contact)
|
|
||||||
|
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):
|
def load_ifc(self, path):
|
||||||
ifc = self.ifcs.get(path, None)
|
ifc = self.ifcs.get(path, None)
|
||||||
@@ -62,37 +71,17 @@ class Clasher:
|
|||||||
return ifc
|
return ifc
|
||||||
|
|
||||||
def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
|
def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
|
||||||
print('adding collision objects', name)
|
|
||||||
if not mode:
|
if not mode:
|
||||||
iterator = ifcopenshell.geom.iterator(
|
elements = ifc_file.by_type("IfcElement")
|
||||||
self.geom_settings,
|
|
||||||
ifc_file,
|
|
||||||
multiprocessing.cpu_count(),
|
|
||||||
exclude=(ifc_file.by_type("IfcSpatialStructureElement")),
|
|
||||||
)
|
|
||||||
elif mode == "e":
|
elif mode == "e":
|
||||||
iterator = ifcopenshell.geom.iterator(
|
exclude = self.selector.parse(ifc_file, selector)
|
||||||
self.geom_settings,
|
elements = [e for e in ifc_file.by_type("IfcElement") if e not in exclude]
|
||||||
ifc_file,
|
|
||||||
multiprocessing.cpu_count(),
|
|
||||||
exclude=selector.parse(ifc_file, selector),
|
|
||||||
)
|
|
||||||
elif mode == "i":
|
elif mode == "i":
|
||||||
iterator = ifcopenshell.geom.iterator(
|
elements = self.selector.parse(ifc_file, selector)
|
||||||
self.geom_settings,
|
iterator = ifcopenshell.geom.iterator(
|
||||||
ifc_file,
|
self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements
|
||||||
multiprocessing.cpu_count(),
|
)
|
||||||
include=selector.parse(ifc_file, selector),
|
self.collider.create_objects(name, ifc_file, iterator, elements)
|
||||||
)
|
|
||||||
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
|
|
||||||
|
|
||||||
def export(self):
|
def export(self):
|
||||||
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
|
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
|
||||||
@@ -101,23 +90,23 @@ class Clasher:
|
|||||||
|
|
||||||
def export_bcfxml(self):
|
def export_bcfxml(self):
|
||||||
import bcf
|
import bcf
|
||||||
import bcf.bcfxml
|
import bcf.v2.bcfxml
|
||||||
|
|
||||||
for i, clash_set in enumerate(self.clash_sets):
|
for i, clash_set in enumerate(self.clash_sets):
|
||||||
bcfxml = bcf.bcfxml.BcfXml()
|
bcfxml = bcf.v2.bcfxml.BcfXml()
|
||||||
bcfxml.new_project()
|
bcfxml.new_project()
|
||||||
bcfxml.project.name = clash_set["name"]
|
bcfxml.project.name = clash_set["name"]
|
||||||
bcfxml.edit_project()
|
bcfxml.edit_project()
|
||||||
for key, clash in clash_set["clashes"].items():
|
for key, clash in clash_set["clashes"].items():
|
||||||
topic = bcf.data.Topic()
|
topic = bcf.v2.data.Topic()
|
||||||
topic.title = "{}/{} and {}/{}".format(
|
topic.title = "{}/{} and {}/{}".format(
|
||||||
clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"]
|
clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"]
|
||||||
)
|
)
|
||||||
topic = bcfxml.add_topic(topic)
|
topic = bcfxml.add_topic(topic)
|
||||||
viewpoint = bcf.data.Viewpoint()
|
viewpoint = bcf.v2.data.Viewpoint()
|
||||||
viewpoint.perspective_camera = bcf.data.PerspectiveCamera()
|
viewpoint.perspective_camera = bcf.v2.data.PerspectiveCamera()
|
||||||
position = np.array(clash["position"])
|
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.x = point[0]
|
||||||
viewpoint.perspective_camera.camera_view_point.y = point[1]
|
viewpoint.perspective_camera.camera_view_point.y = point[1]
|
||||||
viewpoint.perspective_camera.camera_view_point.z = point[2]
|
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.x = mat[0][1]
|
||||||
viewpoint.perspective_camera.camera_up_vector.y = mat[1][1]
|
viewpoint.perspective_camera.camera_up_vector.y = mat[1][1]
|
||||||
viewpoint.perspective_camera.camera_up_vector.z = mat[2][1]
|
viewpoint.perspective_camera.camera_up_vector.z = mat[2][1]
|
||||||
viewpoint.components = bcf.data.Components()
|
viewpoint.components = bcf.v2.data.Components()
|
||||||
c1 = bcf.data.Component()
|
c1 = bcf.v2.data.Component()
|
||||||
c1.ifc_guid = clash["a_global_id"]
|
c1.ifc_guid = clash["a_global_id"]
|
||||||
c2 = bcf.data.Component()
|
c2 = bcf.v2.data.Component()
|
||||||
c2.ifc_guid = clash["b_global_id"]
|
c2.ifc_guid = clash["b_global_id"]
|
||||||
viewpoint.components.selection.append(c1)
|
viewpoint.components.selection.append(c1)
|
||||||
viewpoint.components.selection.append(c2)
|
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.components.visibility.default_visibility = True
|
||||||
viewpoint.snapshot = self.get_viewpoint_snapshot(viewpoint, mat)
|
viewpoint.snapshot = self.get_viewpoint_snapshot(viewpoint, mat)
|
||||||
bcfxml.add_viewpoint(topic, viewpoint)
|
bcfxml.add_viewpoint(topic, viewpoint)
|
||||||
@@ -168,8 +157,6 @@ class Clasher:
|
|||||||
def export_json(self):
|
def export_json(self):
|
||||||
results = self.clash_sets.copy()
|
results = self.clash_sets.copy()
|
||||||
for result in results:
|
for result in results:
|
||||||
del result["a_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]:
|
||||||
if "ifc" in data:
|
if "ifc" in data:
|
||||||
@@ -177,10 +164,10 @@ class Clasher:
|
|||||||
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)
|
||||||
|
|
||||||
def get_element(self, clash_group, global_id):
|
def get_element(self, clash_set, global_id):
|
||||||
for data in clash_group:
|
for source in clash_set:
|
||||||
try:
|
try:
|
||||||
element = data["ifc"].by_guid(global_id)
|
element = source["ifc"].by_guid(global_id)
|
||||||
if element:
|
if element:
|
||||||
return element
|
return element
|
||||||
except:
|
except:
|
||||||
|
|||||||
Reference in New Issue
Block a user