From 3ad58aba7a18388b1542ba1f566f9f719fb99ae7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 30 Sep 2024 14:53:09 +0500 Subject: [PATCH] ifcclash typing --- .../bonsai/bim/module/clash/operator.py | 1 + src/bonsai/bonsai/bim/module/clash/prop.py | 4 +- src/bonsai/bonsai/tool/clash.py | 3 +- src/ifcclash/ifcclash/ifcclash.py | 116 +++++++++++++----- 4 files changed, 93 insertions(+), 31 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 00e7e75f0a..409cb480b0 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -284,6 +284,7 @@ class ExecuteIfcClash(bpy.types.Operator): if extension == ".json": tool.Clash.load_clash_sets(self.filepath) tool.Clash.import_active_clashes() + self.report({"INFO"}, "Finished IFC clash.") return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/clash/prop.py b/src/bonsai/bonsai/bim/module/clash/prop.py index 3902eb28bc..e9337f1810 100644 --- a/src/bonsai/bonsai/bim/module/clash/prop.py +++ b/src/bonsai/bonsai/bim/module/clash/prop.py @@ -86,7 +86,9 @@ class BIMClashProperties(PropertyGroup): blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty) clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet) - should_create_clash_snapshots: BoolProperty(name="Create Snapshots", default=False) + should_create_clash_snapshots: BoolProperty( + name="Create Snapshots", description="Create bcf snapshots", default=False + ) clash_results_path: StringProperty(name="Clash Results Path") smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path") active_clash_set_index: IntProperty(name="Active Clash Set Index") diff --git a/src/bonsai/bonsai/tool/clash.py b/src/bonsai/bonsai/tool/clash.py index 893a282f4b..57b7ffd4a8 100644 --- a/src/bonsai/bonsai/tool/clash.py +++ b/src/bonsai/bonsai/tool/clash.py @@ -24,12 +24,13 @@ import bonsai.core.tool import bonsai.tool as tool from contextlib import contextmanager from mathutils import Vector +from ifcclash import ifcclash class Clash(bonsai.core.tool.Clash): @classmethod - def export_clash_sets(cls): + def export_clash_sets(cls) -> list[ifcclash.ClashSet]: clash_sets = [] for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: a = [] diff --git a/src/ifcclash/ifcclash/ifcclash.py b/src/ifcclash/ifcclash/ifcclash.py index a967be3ce3..b746ad2055 100644 --- a/src/ifcclash/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash/ifcclash.py @@ -19,6 +19,7 @@ # along with IfcClash. If not, see . +from __future__ import annotations import json import time import numpy as np @@ -26,23 +27,71 @@ import multiprocessing import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.selector +from logging import Logger +from typing import Optional, Literal, TypedDict, NotRequired + + +class ClashSource(TypedDict): + file: str + mode: NotRequired[Literal["a", "e", "i"]] + selector: NotRequired[str] + # Will be automatically added during clash. + ifc: NotRequired[ifcopenshell.file] + + +ClashType = Literal["protrusion", "pierce", "collision", "clearance"] + + +class ClashResult(TypedDict): + a_global_id: str + b_global_id: str + a_ifc_class: str + b_ifc_class: str + a_name: str + b_name: str + type: ClashType + p1: list[float] + p2: list[float] + distance: float + + +class ClashSet(TypedDict): + name: str + a: list[ClashSource] + b: NotRequired[list[ClashSource]] + mode: Literal["intersection", "collision", "clearance"] + # Added during clash. + clashes: NotRequired[dict[str, ClashResult]] + # intersection, clearance modes. + check_all: NotRequired[bool] + # inseresection mode. + tolerance: NotRequired[float] + # collision mode. + allow_touching: NotRequired[bool] + # clearance mode. + clearance: NotRequired[float] + + +class ClashGroup(TypedDict): + elements: dict + objects: dict class Clasher: - def __init__(self, settings): + def __init__(self, settings: ClashSettings): self.settings = settings self.geom_settings = ifcopenshell.geom.settings() - self.clash_sets = [] + self.clash_sets: list[ClashSet] = [] self.logger = self.settings.logger - self.groups = {} - self.ifcs = {} + self.groups: dict[str, ClashGroup] = {} + self.ifcs: dict[str, ifcopenshell.file] = {} self.tree = None - def clash(self): + def clash(self) -> None: for clash_set in self.clash_sets: self.process_clash_set(clash_set) - def process_clash_set(self, clash_set): + def process_clash_set(self, clash_set: ClashSet) -> None: self.tree = ifcopenshell.geom.tree() self.create_group("a") for source in clash_set["a"]: @@ -79,42 +128,51 @@ class Clasher: clearance=clash_set["clearance"], check_all=clash_set["check_all"], ) + else: + assert False, f"Unexpected mode '{mode}'." - processed_results = {} + processed_results: dict[str, ClashResult] = {} for result in results: element1 = result.a element2 = result.b - processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = { - "a_global_id": element1.get_argument(0), - "b_global_id": element2.get_argument(0), - "a_ifc_class": element1.is_a(), - "b_ifc_class": element2.is_a(), - "a_name": element1.get_argument(2), - "b_name": element2.get_argument(2), - "type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type], - "p1": list(result.p1), - "p2": list(result.p2), - "distance": result.distance, - } + processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = ClashResult( + a_global_id=element1.get_argument(0), + b_global_id=element2.get_argument(0), + a_ifc_class=element1.is_a(), + b_ifc_class=element2.is_a(), + a_name=element1.get_argument(2), + b_name=element2.get_argument(2), + type=["protrusion", "pierce", "collision", "clearance"][result.clash_type], + p1=list(result.p1), + p2=list(result.p2), + distance=result.distance, + ) clash_set["clashes"] = processed_results self.logger.info(f"Found clashes: {len(processed_results.keys())}") - def create_group(self, name): + def create_group(self, name: str) -> None: self.logger.info(f"Creating group {name}") self.groups[name] = {"elements": {}, "objects": {}} - def load_ifc(self, path): + def load_ifc(self, path: str) -> ifcopenshell.file: start = time.time() self.settings.logger.info(f"Loading IFC {path}") ifc = self.ifcs.get(path, None) if not ifc: ifc = ifcopenshell.open(path) + assert isinstance(ifc, ifcopenshell.file) self.ifcs[path] = ifc self.settings.logger.info(f"Loading finished {time.time() - start}") return ifc - def add_collision_objects(self, name, ifc_file, mode=None, selector=None): + def add_collision_objects( + self, + name: str, + ifc_file: ifcopenshell.file, + mode: Optional[Literal["a", "e", "i"]] = None, + selector: Optional[str] = None, + ) -> None: start = time.time() self.settings.logger.info("Creating iterator") if not mode or mode == "a" or not selector: @@ -132,7 +190,7 @@ class Clasher: self.settings.logger.info(f"Iterator creation finished {time.time() - start}") start = time.time() - self.logger.info(f"Adding objects {name}") + self.logger.info(f"Adding objects {name} ({len(elements)} elements)") assert iterator.initialize() while True: self.tree.add_element(iterator.get()) @@ -145,12 +203,12 @@ class Clasher: self.logger.info(f"Element metadata finished {time.time() - start}") start = time.time() - def export(self): + def export(self) -> None: if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": return self.export_bcfxml() self.export_json() - def export_bcfxml(self): + def export_bcfxml(self) -> None: from bcf.v2.bcfxml import BcfXml for i, clash_set in enumerate(self.clash_sets): @@ -170,12 +228,12 @@ class Clasher: suffix = f".{i}" if i else "" bcfxml.save_project(f"{self.settings.output}{suffix}") - def get_viewpoint_snapshot(self, viewpoint): + def get_viewpoint_snapshot(self, viewpoint) -> None: # Possible to overload this function in a GUI application if used as a library. # Should return a tuple of (filename, bytes). return None - def export_json(self): + def export_json(self) -> None: clash_sets = self.clash_sets.copy() for clash_set in clash_sets: for source in clash_set["a"]: @@ -185,7 +243,7 @@ class Clasher: with open(self.settings.output, "w", encoding="utf-8") as clashes_file: json.dump(clash_sets, clashes_file, indent=4) - def smart_group_clashes(self, clash_sets, max_clustering_distance): + def smart_group_clashes(self, clash_sets: list[ClashSet], max_clustering_distance: float): from sklearn.cluster import OPTICS from collections import defaultdict @@ -278,5 +336,5 @@ class Clasher: class ClashSettings: def __init__(self): - self.logger = None + self.logger: Logger = None self.output = "clashes.json"