Upgrade IfcClash to use new clash capabilities (currently unmerged from feature-clash2)

This commit is contained in:
Dion Moult
2024-03-26 15:22:56 +11:00
parent 71013a48d4
commit 18c38b312d
2 changed files with 75 additions and 166 deletions
-121
View File
@@ -1,121 +0,0 @@
# IfcClash - IFC-based clash detection.
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcClash.
#
# IfcClash is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcClash is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
import hppfcl
import numpy as np
import ifcopenshell
class Collider:
def __init__(self, logger):
self.logger = logger
self.groups = {}
self.tree = ifcopenshell.geom.tree()
def create_group(self, name):
self.logger.info(f"Creating group {name}")
self.groups[name] = {"elements": {}, "objects": {}}
def create_objects(self, name, ifc_file, iterator, elements):
import time
start = time.time()
self.logger.info(f"Adding objects {name}")
assert iterator.initialize()
while True:
self.tree.add_element(iterator.get_native())
shape = iterator.get()
self.create_object(name, shape.guid, shape)
if not iterator.next():
break
self.logger.info(f"Tree finished {time.time() - start}")
start = time.time()
self.groups[name]["elements"].update({e.GlobalId: e for e in elements})
self.logger.info(f"Element metadata finished {time.time() - start}")
start = time.time()
def create_object(self, group_name, id, shape):
obj = hppfcl.CollisionObject(
self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data)
)
self.groups[group_name]["objects"][id] = obj
def collide_internal(self, name):
return self.collide_narrowphase(name, name, self.collide_broadphase(name, name))
def collide_group(self, name1, name2):
return self.collide_narrowphase(name1, name2, self.collide_broadphase(name1, name2))
def collide_broadphase(self, name1, name2):
import time
start = time.time()
self.logger.info("Starting broadphase")
potential_collisions = []
checked_collisions = set()
for id, element in self.groups[name1]["elements"].items():
checked_collisions.add(id)
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)
self.logger.info(f"Finished broadphase {time.time() - start}")
return potential_collisions
def collide_narrowphase(self, name1, name2, potential_collisions):
import time
start = time.time()
self.logger.info("Starting narrowphase")
collisions = []
for data in potential_collisions:
result = hppfcl.CollisionResult()
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})
self.logger.info(f"Finished narrowphase {time.time() - start}")
return collisions
def create_transform(self, m):
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()
return hppfcl.Transform3f(mat[:3, :3], mat[:3, 3])
def create_bvh(self, mesh):
v = mesh.verts
f = mesh.faces
mesh_verts = np.array([[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)])
mesh_faces = [(int(f[i]), int(f[i + 1]), int(f[i + 2])) for i in range(0, len(f), 3)]
bvh = hppfcl.BVHModelOBB()
bvh.beginModel(len(mesh_faces), len(mesh_verts))
vertices = hppfcl.StdVec_Vec3f()
[vertices.append(v) for v in mesh_verts]
triangles = hppfcl.StdVec_Triangle()
[triangles.append(hppfcl.Triangle(f[0], f[1], f[2])) for f in mesh_faces]
bvh.addSubModel(vertices, triangles)
bvh.endModel()
return bvh
+75 -45
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# IfcClash - IFC-based clash detection. # IfcClash - IFC-based clash detection.
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com> # Copyright (C) 2020-2024 Dion Moult <dion@thinkmoult.com>
# #
# This file is part of IfcClash. # This file is part of IfcClash.
# #
@@ -19,14 +19,13 @@
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>. # along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import json import json
import sys import time
import numpy as np
import multiprocessing import multiprocessing
import ifcopenshell import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
import ifcopenshell.util.selector import ifcopenshell.util.selector
from . import collider
class Clasher: class Clasher:
@@ -34,55 +33,78 @@ class Clasher:
self.settings = settings self.settings = settings
self.geom_settings = ifcopenshell.geom.settings() self.geom_settings = ifcopenshell.geom.settings()
self.clash_sets = [] self.clash_sets = []
self.collider = collider.Collider(self.settings.logger) self.logger = self.settings.logger
self.selector = ifcopenshell.util.selector.Selector() self.groups = {}
self.ifcs = {} self.ifcs = {}
self.tree = None
def clash(self): def clash(self):
existing_limit = sys.getrecursionlimit()
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)
def process_clash_set(self, clash_set): def process_clash_set(self, clash_set):
self.collider.create_group("a") self.tree = ifcopenshell.geom.tree()
self.create_group("a")
for source in clash_set["a"]: for source in clash_set["a"]:
source["ifc"] = self.load_ifc(source["file"]) source["ifc"] = self.load_ifc(source["file"])
self.add_collision_objects("a", source["ifc"], 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 and clash_set["b"]:
self.collider.create_group("b") self.create_group("b")
for source in clash_set["b"]: for source in clash_set["b"]:
source["ifc"] = self.load_ifc(source["file"]) source["ifc"] = self.load_ifc(source["file"])
self.add_collision_objects("b", source["ifc"], 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") b = "b"
else: else:
results = self.collider.collide_internal("a") b = "a"
mode = clash_set["mode"]
if mode == "intersection":
results = self.tree.clash_intersection_many(
list(self.groups["a"]["elements"].values()),
list(self.groups[b]["elements"].values()),
tolerance=clash_set["tolerance"],
check_all=clash_set["check_all"],
)
elif mode == "collision":
results = self.tree.clash_collision_many(
list(self.groups["a"]["elements"].values()),
list(self.groups[b]["elements"].values()),
allow_touching=clash_set["allow_touching"],
)
elif mode == "clearance":
results = self.tree.clash_clearance_many(
list(self.groups["a"]["elements"].values()),
list(self.groups[b]["elements"].values()),
clearance=clash_set["clearance"],
check_all=clash_set["check_all"],
)
processed_results = {} processed_results = {}
for result in results: for result in results:
element1 = self.get_element(clash_set["a"], result["id1"]) element1 = result.a
if "b" in clash_set: element2 = result.b
element2 = self.get_element(clash_set["b"], result["id2"])
else:
element2 = self.get_element(clash_set["a"], result["id2"])
contact = result["collision"].getContacts()[0] processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = {
processed_results[f"{result['id1']}-{result['id2']}"] = { "a_global_id": element1.get_argument(0),
"a_global_id": result["id1"], "b_global_id": element2.get_argument(0),
"b_global_id": result["id2"],
"a_ifc_class": element1.is_a(), "a_ifc_class": element1.is_a(),
"b_ifc_class": element2.is_a(), "b_ifc_class": element2.is_a(),
"a_name": element1.Name, "a_name": element1.get_argument(2),
"b_name": element2.Name, "b_name": element2.get_argument(2),
"normal": list(contact.normal), "type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type],
"position": list(contact.pos), "p1": list(result.p1),
"penetration_depth": contact.penetration_depth, "p2": list(result.p2),
"distance": result.distance,
} }
clash_set["clashes"] = processed_results clash_set["clashes"] = processed_results
self.logger.info(f"Found clashes: {len(processed_results.keys())}")
def create_group(self, name):
self.logger.info(f"Creating group {name}")
self.groups[name] = {"elements": {}, "objects": {}}
def load_ifc(self, path): def load_ifc(self, path):
import time
start = time.time() start = time.time()
self.settings.logger.info(f"Loading IFC {path}") self.settings.logger.info(f"Loading IFC {path}")
ifc = self.ifcs.get(path, None) ifc = self.ifcs.get(path, None)
@@ -93,21 +115,36 @@ 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):
import time
start = time.time() start = time.time()
self.settings.logger.info("Creating iterator") self.settings.logger.info("Creating iterator")
if not mode: if not mode or not selector:
elements = ifc_file.by_type("IfcElement") elements = set(ifc_file.by_type("IfcElement"))
elements -= set(ifc_file.by_type("IfcFeatureElement"))
elif mode == "e": elif mode == "e":
elements = set(ifc_file.by_type("IfcElement")) - set(self.selector.parse(ifc_file, selector)) elements = set(ifc_file.by_type("IfcElement"))
elements -= set(ifc_file.by_type("IfcFeatureElement"))
elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, selector))
elif mode == "i": elif mode == "i":
elements = self.selector.parse(ifc_file, selector) elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, selector))
iterator = ifcopenshell.geom.iterator( iterator = ifcopenshell.geom.iterator(
self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements
) )
self.settings.logger.info(f"Iterator creation finished {time.time() - start}") self.settings.logger.info(f"Iterator creation finished {time.time() - start}")
self.collider.create_objects(name, ifc_file, iterator, elements)
start = time.time()
self.logger.info(f"Adding objects {name}")
assert iterator.initialize()
while True:
self.tree.add_element(iterator.get_native(), should_triangulate=True)
# self.tree.add_element(iterator.get())
shape = iterator.get()
if not iterator.next():
break
self.logger.info(f"Tree finished {time.time() - start}")
start = time.time()
self.groups[name]["elements"].update({e.GlobalId: e for e in elements})
self.logger.info(f"Element metadata finished {time.time() - start}")
start = time.time()
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":
@@ -123,7 +160,9 @@ class Clasher:
title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}' title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}'
topic = bcfxml.add_topic(title, title, "IfcClash") topic = bcfxml.add_topic(title, title, "IfcClash")
viewpoint = topic.add_viewpoint_from_point_and_guids( viewpoint = topic.add_viewpoint_from_point_and_guids(
np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"], np.array(clash["position"]),
clash["a_global_id"],
clash["b_global_id"],
) )
snapshot = self.get_viewpoint_snapshot(viewpoint) snapshot = self.get_viewpoint_snapshot(viewpoint)
if snapshot: if snapshot:
@@ -147,15 +186,6 @@ 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(clash_sets, clashes_file, indent=4) json.dump(clash_sets, clashes_file, indent=4)
def get_element(self, clash_set, global_id):
for source in clash_set:
try:
element = source["ifc"].by_guid(global_id)
if element:
return element
except:
pass
def smart_group_clashes(self, clash_sets, max_clustering_distance): def smart_group_clashes(self, clash_sets, max_clustering_distance):
from sklearn.cluster import OPTICS from sklearn.cluster import OPTICS
from collections import defaultdict from collections import defaultdict