diff --git a/src/ifcclash/collision.py b/src/ifcclash/collision.py index 192c06e4c2..b5dcf620f5 100644 --- a/src/ifcclash/collision.py +++ b/src/ifcclash/collision.py @@ -29,10 +29,7 @@ class ContactData(object): The contact in question. """ self.names = names - self._inds = { - names[0]: contact.b1, - names[1]: contact.b2 - } + self._inds = {names[0]: contact.b1, names[1]: contact.b2} self._point = contact.pos self.raw = contact @@ -83,14 +80,8 @@ class DistanceData(object): 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._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 @@ -149,7 +140,7 @@ class CollisionManager(object): Initialize a mesh-mesh collision manager. """ if fcl is None: - raise ValueError('No FCL Available!') + raise ValueError("No FCL Available!") # {name: {geom:, obj}} self._objs = {} # {id(bvh) : str, name} @@ -162,10 +153,7 @@ class CollisionManager(object): self._manager = fcl.DynamicAABBTreeCollisionManager() self._manager.setup() - def add_object(self, - name, - mesh, - transform=None): + def add_object(self, name, mesh, transform=None): """ Add an object to the collision manager. @@ -187,7 +175,7 @@ class CollisionManager(object): transform = np.eye(4) transform = np.asanyarray(transform, dtype=np.float64) if transform.shape != (4, 4): - raise ValueError('transform must be (4,4)!') + raise ValueError("transform must be (4,4)!") # create or recall from cache BVH bvh = self._get_BVH(mesh) @@ -198,8 +186,7 @@ class CollisionManager(object): # Add collision object to set if name in self._objs: self._manager.unregisterObject(self._objs[name]) - self._objs[name] = {'obj': o, - 'geom': bvh} + self._objs[name] = {"obj": o, "geom": bvh} # store the name of the geometry self._names[id(bvh)] = name @@ -217,14 +204,14 @@ class CollisionManager(object): The identifier for the object """ if name in self._objs: - self._manager.unregisterObject(self._objs[name]['obj']) - self._manager.update(self._objs[name]['obj']) + 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']) + geom_id = id(self._objs.pop(name)["geom"]) # remove names self._names.pop(geom_id) else: - raise ValueError('{} not in collision manager!'.format(name)) + raise ValueError("{} not in collision manager!".format(name)) def set_transform(self, name, transform): """ @@ -239,18 +226,14 @@ class CollisionManager(object): A new homogeneous transform matrix for the object """ if name in self._objs: - o = self._objs[name]['obj'] + 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)) + raise ValueError("{} not in collision manager!".format(name)) - def in_collision_single(self, - mesh, - transform=None, - return_names=False, - return_data=False): + 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. @@ -288,9 +271,7 @@ class CollisionManager(object): # 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)) + 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 @@ -305,7 +286,7 @@ class CollisionManager(object): cg = contact.o2 name = self._extract_name(cg) - names = (name, '__external') + names = (name, "__external") if cg == contact.o2: names = reversed(names) @@ -349,8 +330,7 @@ class CollisionManager(object): """ cdata = fcl.CollisionData() if return_names or return_data: - cdata = fcl.CollisionData(request=fcl.CollisionRequest( - num_max_contacts=1000000, enable_contact=True)) + cdata = fcl.CollisionData(request=fcl.CollisionRequest(num_max_contacts=1000000, enable_contact=True)) self._manager.collide(cdata, fcl.defaultCollisionCallback) @@ -360,8 +340,7 @@ class CollisionManager(object): 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)) + names = (self._extract_name(contact.o1), self._extract_name(contact.o2)) if return_names: objs_in_collision.add(tuple(sorted(names))) @@ -377,8 +356,7 @@ class CollisionManager(object): else: return result - def in_collision_other(self, other_manager, - return_names=False, return_data=False): + 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. @@ -408,13 +386,8 @@ class CollisionManager(object): """ 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) + 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() @@ -422,11 +395,9 @@ class CollisionManager(object): 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)) + 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)) + names = (self._extract_name(contact.o2), other_manager._extract_name(contact.o1)) reverse = True if return_names: @@ -445,11 +416,7 @@ class CollisionManager(object): else: return result - def min_distance_single(self, - mesh, - transform=None, - return_name=False, - return_data=False): + 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. @@ -486,10 +453,7 @@ class CollisionManager(object): # Collide with manager's objects ddata = fcl.DistanceData() if return_data: - ddata = fcl.DistanceData( - fcl.DistanceRequest(enable_nearest_points=True), - fcl.DistanceResult() - ) + ddata = fcl.DistanceData(fcl.DistanceRequest(enable_nearest_points=True), fcl.DistanceResult()) self._manager.distance(o, ddata, fcl.defaultDistanceCallback) @@ -504,7 +468,7 @@ class CollisionManager(object): name = self._extract_name(cg) - names = (name, '__external') + names = (name, "__external") if cg == ddata.result.o2: names = reversed(names) data = DistanceData(names, ddata.result) @@ -541,10 +505,7 @@ class CollisionManager(object): """ ddata = fcl.DistanceData() if return_data: - ddata = fcl.DistanceData( - fcl.DistanceRequest(enable_nearest_points=True), - fcl.DistanceResult() - ) + ddata = fcl.DistanceData(fcl.DistanceRequest(enable_nearest_points=True), fcl.DistanceResult()) self._manager.distance(ddata, fcl.defaultDistanceCallback) @@ -552,8 +513,7 @@ class CollisionManager(object): names, data = None, None if return_names or return_data: - names = (self._extract_name(ddata.result.o1), - self._extract_name(ddata.result.o2)) + names = (self._extract_name(ddata.result.o1), self._extract_name(ddata.result.o2)) data = DistanceData(names, ddata.result) names = tuple(sorted(names)) @@ -566,8 +526,7 @@ class CollisionManager(object): else: return distance - def min_distance_other(self, other_manager, - return_names=False, return_data=False): + 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. @@ -596,26 +555,19 @@ class CollisionManager(object): """ ddata = fcl.DistanceData() if return_data: - ddata = fcl.DistanceData( - fcl.DistanceRequest(enable_nearest_points=True), - fcl.DistanceResult() - ) + ddata = fcl.DistanceData(fcl.DistanceRequest(enable_nearest_points=True), fcl.DistanceResult()) - self._manager.distance(other_manager._manager, - ddata, - fcl.defaultDistanceCallback) + 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)) + 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)) + names = (self._extract_name(ddata.result.o2), other_manager._extract_name(ddata.result.o1)) dnames = tuple(names) if reverse: @@ -681,10 +633,8 @@ def mesh_to_BVH(mesh): 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.beginModel(num_tris_=len(mesh.faces), num_vertices_=len(mesh.vertices)) + bvh.addSubModel(verts=mesh.vertices, triangles=mesh.faces) bvh.endModel() return bvh @@ -709,7 +659,5 @@ def scene_to_collision(scene): 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) + objects[node] = manager.add_object(name=node, mesh=scene.geometry[geometry], transform=T) return manager, objects diff --git a/src/ifcclash/ifcclash.py b/src/ifcclash/ifcclash.py index 9a10a650b5..320a9ef5d8 100644 --- a/src/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash.py @@ -22,43 +22,43 @@ class IfcClasher: self.settings = settings self.geom_settings = ifcopenshell.geom.settings() self.clash_sets = [] - self.clash_data = {'meshes': {}} - self.global_data = {'meshes': {}, 'matrices': {}} + 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 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']) + 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) + 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) + 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'] = {} + 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) + 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) + b = self.get_element(clash_set["a"], b_global_id) if contact.raw.penetration_depth < tolerance: continue @@ -72,7 +72,7 @@ class IfcClasher: # 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 + 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 @@ -82,17 +82,21 @@ class IfcClasher: # 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]] + 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)) + 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)) + 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 @@ -111,75 +115,89 @@ class IfcClasher: # This is probably two triangles which just touch continue - key = f'{a_global_id}-{b_global_id}' + 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: + 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 + 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) + 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) + 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) + 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) + 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): results = self.clash_sets.copy() for result in results: - del result['a_cm'] - del result['b_cm'] - for ab in ['a', 'b']: + 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: + 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) + element = data["ifc"].by_guid(global_id) if element: return element except: pass def add_collision_objects(self, data, cm): - self.clash_data['meshes'] = {} + 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'])) + 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 @@ -196,46 +214,37 @@ class IfcClasher: 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] + 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 + 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 = 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 + 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)]) + 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') + 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') + buildings = self.find_decomposed_ifc_class(project, "IfcBuilding") for building in buildings: self.patch_placement_to_origin(building) @@ -252,43 +261,36 @@ class IfcClasher: return results def patch_placement_to_origin(self, element): - element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0., 0., 0.) + element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, 0.0) if element.ObjectPlacement.RelativePlacement.Axis: - element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0., 0., 1.) + element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0.0, 0.0, 1.0) if element.ObjectPlacement.RelativePlacement.RefDirection: - element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1., 0., 0.) + element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1.0, 0.0, 0.0) class IfcClashSettings: def __init__(self): self.logger = None - self.output = 'clashes.json' + self.output = "clashes.json" -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description='Clashes geometry between two IFC files') +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( - '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') + "-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 = 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: + 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()