|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import ifcopenshell |
| 4 | +import ifcopenshell.geom |
| 5 | +import ifcopenshell.util.selector |
| 6 | +import multiprocessing |
| 7 | +import numpy as np |
| 8 | +import json |
| 9 | +import sys |
| 10 | +import argparse |
| 11 | +import logging |
| 12 | +from . import collider |
| 13 | + |
| 14 | + |
| 15 | +class Clasher: |
| 16 | + def __init__(self, settings): |
| 17 | + self.settings = settings |
| 18 | + self.geom_settings = ifcopenshell.geom.settings() |
| 19 | + self.clash_sets = [] |
| 20 | + self.collider = collider.Collider() |
| 21 | + self.selector = ifcopenshell.util.selector.Selector() |
| 22 | + self.ifcs = {} |
| 23 | + |
| 24 | + def clash(self): |
| 25 | + existing_limit = sys.getrecursionlimit() |
| 26 | + sys.setrecursionlimit(100000) |
| 27 | + for clash_set in self.clash_sets: |
| 28 | + self.process_clash_set(clash_set) |
| 29 | + sys.setrecursionlimit(existing_limit) |
| 30 | + |
| 31 | + def process_clash_set(self, clash_set): |
| 32 | + print("proccessings", clash_set) |
| 33 | + self.collider.create_group("a") |
| 34 | + for source in clash_set["a"]: |
| 35 | + self.add_collision_objects( |
| 36 | + "a", self.load_ifc(source["file"]), source.get("mode", None), source.get("selector", None) |
| 37 | + ) |
| 38 | + |
| 39 | + if "b" in clash_set: |
| 40 | + self.collider.create_group("b") |
| 41 | + for source in clash_set["b"]: |
| 42 | + self.add_collision_objects( |
| 43 | + "b", self.load_ifc(source["file"]), source.get("mode", None), source.get("selector", None) |
| 44 | + ) |
| 45 | + results = self.collider.collide_group("a", "b") |
| 46 | + else: |
| 47 | + results = self.collider.collide_internal("a") |
| 48 | + |
| 49 | + for result in results: |
| 50 | + print("*" * 10) |
| 51 | + print("Is Collision:", result["collision"].isCollision()) |
| 52 | + print(result["id1"], result["id2"]) |
| 53 | + print("Number of contacts:", result["collision"].numContacts()) |
| 54 | + for contact in result["collision"].getContacts(): |
| 55 | + print(contact) |
| 56 | + |
| 57 | + def load_ifc(self, path): |
| 58 | + ifc = self.ifcs.get(path, None) |
| 59 | + if not ifc: |
| 60 | + ifc = ifcopenshell.open(path) |
| 61 | + self.ifcs[path] = ifc |
| 62 | + return ifc |
| 63 | + |
| 64 | + def add_collision_objects(self, name, ifc_file, mode=None, selector=None): |
| 65 | + print('adding collision objects', name) |
| 66 | + if not mode: |
| 67 | + iterator = ifcopenshell.geom.iterator( |
| 68 | + self.geom_settings, |
| 69 | + ifc_file, |
| 70 | + multiprocessing.cpu_count(), |
| 71 | + exclude=(ifc_file.by_type("IfcSpatialStructureElement")), |
| 72 | + ) |
| 73 | + elif mode == "e": |
| 74 | + iterator = ifcopenshell.geom.iterator( |
| 75 | + self.geom_settings, |
| 76 | + ifc_file, |
| 77 | + multiprocessing.cpu_count(), |
| 78 | + exclude=selector.parse(ifc_file, selector), |
| 79 | + ) |
| 80 | + elif mode == "i": |
| 81 | + iterator = ifcopenshell.geom.iterator( |
| 82 | + self.geom_settings, |
| 83 | + ifc_file, |
| 84 | + multiprocessing.cpu_count(), |
| 85 | + include=selector.parse(ifc_file, selector), |
| 86 | + ) |
| 87 | + valid_file = iterator.initialize() |
| 88 | + if not valid_file: |
| 89 | + return False |
| 90 | + old_progress = -1 |
| 91 | + while True: |
| 92 | + shape = iterator.get() |
| 93 | + self.collider.create_object(name, shape.guid, shape) |
| 94 | + if not iterator.next(): |
| 95 | + break |
| 96 | + |
| 97 | + def export(self): |
| 98 | + if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": |
| 99 | + return self.export_bcfxml() |
| 100 | + self.export_json() |
| 101 | + |
| 102 | + def export_bcfxml(self): |
| 103 | + import bcf |
| 104 | + import bcf.bcfxml |
| 105 | + |
| 106 | + for i, clash_set in enumerate(self.clash_sets): |
| 107 | + bcfxml = bcf.bcfxml.BcfXml() |
| 108 | + bcfxml.new_project() |
| 109 | + bcfxml.project.name = clash_set["name"] |
| 110 | + bcfxml.edit_project() |
| 111 | + for key, clash in clash_set["clashes"].items(): |
| 112 | + topic = bcf.data.Topic() |
| 113 | + topic.title = "{}/{} and {}/{}".format( |
| 114 | + clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"] |
| 115 | + ) |
| 116 | + topic = bcfxml.add_topic(topic) |
| 117 | + viewpoint = bcf.data.Viewpoint() |
| 118 | + viewpoint.perspective_camera = bcf.data.PerspectiveCamera() |
| 119 | + position = np.array(clash["position"]) |
| 120 | + point = position + np.array((5, 5, 5)) # Dumb, but works! |
| 121 | + viewpoint.perspective_camera.camera_view_point.x = point[0] |
| 122 | + viewpoint.perspective_camera.camera_view_point.y = point[1] |
| 123 | + viewpoint.perspective_camera.camera_view_point.z = point[2] |
| 124 | + mat = self.get_track_to_matrix(point, position) |
| 125 | + viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1 |
| 126 | + viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1 |
| 127 | + viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1 |
| 128 | + viewpoint.perspective_camera.camera_up_vector.x = mat[0][1] |
| 129 | + viewpoint.perspective_camera.camera_up_vector.y = mat[1][1] |
| 130 | + viewpoint.perspective_camera.camera_up_vector.z = mat[2][1] |
| 131 | + viewpoint.components = bcf.data.Components() |
| 132 | + c1 = bcf.data.Component() |
| 133 | + c1.ifc_guid = clash["a_global_id"] |
| 134 | + c2 = bcf.data.Component() |
| 135 | + c2.ifc_guid = clash["b_global_id"] |
| 136 | + viewpoint.components.selection.append(c1) |
| 137 | + viewpoint.components.selection.append(c2) |
| 138 | + viewpoint.components.visibility = bcf.data.ComponentVisibility() |
| 139 | + viewpoint.components.visibility.default_visibility = True |
| 140 | + viewpoint.snapshot = self.get_viewpoint_snapshot(viewpoint, mat) |
| 141 | + bcfxml.add_viewpoint(topic, viewpoint) |
| 142 | + if i == 0: |
| 143 | + bcfxml.save_project(self.settings.output) |
| 144 | + else: |
| 145 | + bcfxml.save_project(self.settings.output + f".{i}") |
| 146 | + |
| 147 | + def get_viewpoint_snapshot(self, viewpoint, mat): |
| 148 | + return None # Possible to overload this function in a GUI application if used as a library |
| 149 | + |
| 150 | + # https://blender.stackexchange.com/questions/68834/recreate-to-track-quat-with-two-vectors-using-python/141706#141706 |
| 151 | + def get_track_to_matrix(self, camera_position, target_position): |
| 152 | + camera_direction = camera_position - target_position |
| 153 | + camera_direction = camera_direction / np.linalg.norm(camera_direction) |
| 154 | + camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction) |
| 155 | + camera_right = camera_right / np.linalg.norm(camera_right) |
| 156 | + camera_up = np.cross(camera_direction, camera_right) |
| 157 | + camera_up = camera_up / np.linalg.norm(camera_up) |
| 158 | + rotation_transform = np.zeros((4, 4)) |
| 159 | + rotation_transform[0, :3] = camera_right |
| 160 | + rotation_transform[1, :3] = camera_up |
| 161 | + rotation_transform[2, :3] = camera_direction |
| 162 | + rotation_transform[-1, -1] = 1 |
| 163 | + translation_transform = np.eye(4) |
| 164 | + translation_transform[:3, -1] = -camera_position |
| 165 | + look_at_transform = np.matmul(rotation_transform, translation_transform) |
| 166 | + return np.linalg.inv(look_at_transform) |
| 167 | + |
| 168 | + def export_json(self): |
| 169 | + results = self.clash_sets.copy() |
| 170 | + for result in results: |
| 171 | + del result["a_cm"] |
| 172 | + del result["b_cm"] |
| 173 | + for ab in ["a", "b"]: |
| 174 | + for data in result[ab]: |
| 175 | + if "ifc" in data: |
| 176 | + del data["ifc"] |
| 177 | + with open(self.settings.output, "w", encoding="utf-8") as clashes_file: |
| 178 | + json.dump(results, clashes_file, indent=4) |
| 179 | + |
| 180 | + def get_element(self, clash_group, global_id): |
| 181 | + for data in clash_group: |
| 182 | + try: |
| 183 | + element = data["ifc"].by_guid(global_id) |
| 184 | + if element: |
| 185 | + return element |
| 186 | + except: |
| 187 | + pass |
| 188 | + |
| 189 | + def smart_group_clashes(self, clash_sets, max_clustering_distance): |
| 190 | + from sklearn.cluster import OPTICS |
| 191 | + from collections import defaultdict |
| 192 | + |
| 193 | + count_of_input_clashes = 0 |
| 194 | + count_of_clash_sets = 0 |
| 195 | + count_of_smart_groups = 0 |
| 196 | + count_of_final_clash_sets = 0 |
| 197 | + |
| 198 | + count_of_clash_sets = len(clash_sets) |
| 199 | + |
| 200 | + for clash_set in clash_sets: |
| 201 | + if not "clashes" in clash_set.keys(): |
| 202 | + print(f"Skipping clash set [{clash_set['name']}] since it contains no clash results.") |
| 203 | + continue |
| 204 | + clashes = clash_set["clashes"] |
| 205 | + if len(clashes) == 0: |
| 206 | + print(f"Skipping clash set [{clash_set['name']}] since it contains no clash results.") |
| 207 | + continue |
| 208 | + |
| 209 | + count_of_input_clashes += len(clashes) |
| 210 | + |
| 211 | + positions = [] |
| 212 | + for clash in clashes.values(): |
| 213 | + positions.append(clash["position"]) |
| 214 | + |
| 215 | + data = np.array(positions) |
| 216 | + |
| 217 | + # INPUTS |
| 218 | + # set the desired maximum distance between the grouped points |
| 219 | + if max_clustering_distance > 0: |
| 220 | + max_distance_between_grouped_points = max_clustering_distance |
| 221 | + else: |
| 222 | + max_distance_between_grouped_points = 3 |
| 223 | + |
| 224 | + model = OPTICS(min_samples=2, max_eps=max_distance_between_grouped_points) |
| 225 | + model.fit_predict(data) |
| 226 | + pred = model.fit_predict(data) |
| 227 | + |
| 228 | + # Insert the smart groups into the clashes |
| 229 | + if len(pred) == len(clashes.values()): |
| 230 | + i = 0 |
| 231 | + for clash in clashes.values(): |
| 232 | + int_prediction = int(pred[i]) |
| 233 | + if int_prediction == -1: |
| 234 | + # ungroup this clash since it's a single clash that we were not able to group. |
| 235 | + new_clash_group_number = np.amax(pred).item() + 1 + i |
| 236 | + clash["smart_group"] = new_clash_group_number |
| 237 | + else: |
| 238 | + clash["smart_group"] = int_prediction |
| 239 | + i += 1 |
| 240 | + |
| 241 | + # Create JSON with smart_groups that contain GlobalIDs |
| 242 | + output_clash_sets = defaultdict(list) |
| 243 | + for clash_set in clash_sets: |
| 244 | + if not "clashes" in clash_set.keys(): |
| 245 | + continue |
| 246 | + smart_groups = defaultdict(list) |
| 247 | + for clash_id, content in clash_set["clashes"].items(): |
| 248 | + if "smart_group" in content: |
| 249 | + object_id_list = list() |
| 250 | + # Clash has been grouped, let's extract it. |
| 251 | + object_id_list.append(content["a_global_id"]) |
| 252 | + object_id_list.append(content["b_global_id"]) |
| 253 | + smart_groups[content["smart_group"]].append(object_id_list) |
| 254 | + count_of_smart_groups += len(smart_groups) |
| 255 | + output_clash_sets[clash_set["name"]].append(smart_groups) |
| 256 | + |
| 257 | + # Rename the clash groups to something more sensible |
| 258 | + for clash_set, smart_groups in output_clash_sets.items(): |
| 259 | + clash_set_name = clash_set |
| 260 | + # Only select the clashes that correspond to the actively selected IFC Clash Set |
| 261 | + i = 1 |
| 262 | + new_smart_group_name = "" |
| 263 | + for smart_group, global_id_pairs in list(smart_groups[0].items()): |
| 264 | + new_smart_group_name = f"{clash_set_name} - {i}" |
| 265 | + smart_groups[0][new_smart_group_name] = smart_groups[0].pop(smart_group) |
| 266 | + i += 1 |
| 267 | + |
| 268 | + count_of_final_clash_sets = len(output_clash_sets) |
| 269 | + print( |
| 270 | + f"Took {count_of_input_clashes} clashes in {count_of_clash_sets} clash sets and turned", |
| 271 | + f"them into {count_of_smart_groups} smart groups in {count_of_final_clash_sets} clash sets", |
| 272 | + ) |
| 273 | + |
| 274 | + return output_clash_sets |
| 275 | + |
| 276 | + |
| 277 | +class ClashSettings: |
| 278 | + def __init__(self): |
| 279 | + self.logger = None |
| 280 | + self.output = "clashes.json" |
| 281 | + |
| 282 | + |
| 283 | +if __name__ == "__main__": |
| 284 | + parser = argparse.ArgumentParser(description="Clashes geometry between two IFC files") |
| 285 | + parser.add_argument("input", type=str, help="A JSON dataset describing a series of clashsets") |
| 286 | + parser.add_argument( |
| 287 | + "-o", "--output", type=str, help="The JSON diff file to output. Defaults to output.json", default="output.json" |
| 288 | + ) |
| 289 | + args = parser.parse_args() |
| 290 | + |
| 291 | + settings = ClashSettings() |
| 292 | + settings.output = args.output |
| 293 | + settings.logger = logging.getLogger("Clash") |
| 294 | + settings.logger.setLevel(logging.DEBUG) |
| 295 | + handler = logging.StreamHandler(sys.stdout) |
| 296 | + handler.setLevel(logging.DEBUG) |
| 297 | + settings.logger.addHandler(handler) |
| 298 | + ifc_clasher = Clasher(settings) |
| 299 | + with open(args.input, "r") as clash_sets_file: |
| 300 | + ifc_clasher.clash_sets = json.loads(clash_sets_file.read()) |
| 301 | + ifc_clasher.clash() |
| 302 | + ifc_clasher.export() |
0 commit comments