mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Upgrade IfcClash to use new clash capabilities (currently unmerged from feature-clash2)
This commit is contained in:
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user