ifcclash typing

This commit is contained in:
Andrej730
2024-09-30 14:53:09 +05:00
parent 9131804e4e
commit 3ad58aba7a
4 changed files with 93 additions and 31 deletions
@@ -284,6 +284,7 @@ class ExecuteIfcClash(bpy.types.Operator):
if extension == ".json": if extension == ".json":
tool.Clash.load_clash_sets(self.filepath) tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes() tool.Clash.import_active_clashes()
self.report({"INFO"}, "Finished IFC clash.")
return {"FINISHED"} return {"FINISHED"}
+3 -1
View File
@@ -86,7 +86,9 @@ class BIMClashProperties(PropertyGroup):
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty) blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty)
clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet) 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") clash_results_path: StringProperty(name="Clash Results Path")
smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path") smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path")
active_clash_set_index: IntProperty(name="Active Clash Set Index") active_clash_set_index: IntProperty(name="Active Clash Set Index")
+2 -1
View File
@@ -24,12 +24,13 @@ import bonsai.core.tool
import bonsai.tool as tool import bonsai.tool as tool
from contextlib import contextmanager from contextlib import contextmanager
from mathutils import Vector from mathutils import Vector
from ifcclash import ifcclash
class Clash(bonsai.core.tool.Clash): class Clash(bonsai.core.tool.Clash):
@classmethod @classmethod
def export_clash_sets(cls): def export_clash_sets(cls) -> list[ifcclash.ClashSet]:
clash_sets = [] clash_sets = []
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: for clash_set in bpy.context.scene.BIMClashProperties.clash_sets:
a = [] a = []
+87 -29
View File
@@ -19,6 +19,7 @@
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>. # along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import json import json
import time import time
import numpy as np import numpy as np
@@ -26,23 +27,71 @@ import multiprocessing
import ifcopenshell import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
import ifcopenshell.util.selector 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: class Clasher:
def __init__(self, settings): def __init__(self, settings: ClashSettings):
self.settings = settings self.settings = settings
self.geom_settings = ifcopenshell.geom.settings() self.geom_settings = ifcopenshell.geom.settings()
self.clash_sets = [] self.clash_sets: list[ClashSet] = []
self.logger = self.settings.logger self.logger = self.settings.logger
self.groups = {} self.groups: dict[str, ClashGroup] = {}
self.ifcs = {} self.ifcs: dict[str, ifcopenshell.file] = {}
self.tree = None self.tree = None
def clash(self): def clash(self) -> None:
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: ClashSet) -> None:
self.tree = ifcopenshell.geom.tree() self.tree = ifcopenshell.geom.tree()
self.create_group("a") self.create_group("a")
for source in clash_set["a"]: for source in clash_set["a"]:
@@ -79,42 +128,51 @@ class Clasher:
clearance=clash_set["clearance"], clearance=clash_set["clearance"],
check_all=clash_set["check_all"], check_all=clash_set["check_all"],
) )
else:
assert False, f"Unexpected mode '{mode}'."
processed_results = {} processed_results: dict[str, ClashResult] = {}
for result in results: for result in results:
element1 = result.a element1 = result.a
element2 = result.b element2 = result.b
processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = { processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = ClashResult(
"a_global_id": element1.get_argument(0), a_global_id=element1.get_argument(0),
"b_global_id": element2.get_argument(0), b_global_id=element2.get_argument(0),
"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.get_argument(2), a_name=element1.get_argument(2),
"b_name": element2.get_argument(2), b_name=element2.get_argument(2),
"type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type], type=["protrusion", "pierce", "collision", "clearance"][result.clash_type],
"p1": list(result.p1), p1=list(result.p1),
"p2": list(result.p2), p2=list(result.p2),
"distance": result.distance, distance=result.distance,
} )
clash_set["clashes"] = processed_results clash_set["clashes"] = processed_results
self.logger.info(f"Found clashes: {len(processed_results.keys())}") 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.logger.info(f"Creating group {name}")
self.groups[name] = {"elements": {}, "objects": {}} self.groups[name] = {"elements": {}, "objects": {}}
def load_ifc(self, path): def load_ifc(self, path: str) -> ifcopenshell.file:
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)
if not ifc: if not ifc:
ifc = ifcopenshell.open(path) ifc = ifcopenshell.open(path)
assert isinstance(ifc, ifcopenshell.file)
self.ifcs[path] = ifc self.ifcs[path] = ifc
self.settings.logger.info(f"Loading finished {time.time() - start}") self.settings.logger.info(f"Loading finished {time.time() - start}")
return ifc 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() start = time.time()
self.settings.logger.info("Creating iterator") self.settings.logger.info("Creating iterator")
if not mode or mode == "a" or not selector: 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}") self.settings.logger.info(f"Iterator creation finished {time.time() - start}")
start = time.time() start = time.time()
self.logger.info(f"Adding objects {name}") self.logger.info(f"Adding objects {name} ({len(elements)} elements)")
assert iterator.initialize() assert iterator.initialize()
while True: while True:
self.tree.add_element(iterator.get()) self.tree.add_element(iterator.get())
@@ -145,12 +203,12 @@ class Clasher:
self.logger.info(f"Element metadata finished {time.time() - start}") self.logger.info(f"Element metadata finished {time.time() - start}")
start = time.time() start = time.time()
def export(self): def export(self) -> None:
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
return self.export_bcfxml() return self.export_bcfxml()
self.export_json() self.export_json()
def export_bcfxml(self): def export_bcfxml(self) -> None:
from bcf.v2.bcfxml import BcfXml from bcf.v2.bcfxml import BcfXml
for i, clash_set in enumerate(self.clash_sets): for i, clash_set in enumerate(self.clash_sets):
@@ -170,12 +228,12 @@ class Clasher:
suffix = f".{i}" if i else "" suffix = f".{i}" if i else ""
bcfxml.save_project(f"{self.settings.output}{suffix}") 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. # Possible to overload this function in a GUI application if used as a library.
# Should return a tuple of (filename, bytes). # Should return a tuple of (filename, bytes).
return None return None
def export_json(self): def export_json(self) -> None:
clash_sets = self.clash_sets.copy() clash_sets = self.clash_sets.copy()
for clash_set in clash_sets: for clash_set in clash_sets:
for source in clash_set["a"]: for source in clash_set["a"]:
@@ -185,7 +243,7 @@ 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 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 sklearn.cluster import OPTICS
from collections import defaultdict from collections import defaultdict
@@ -278,5 +336,5 @@ class Clasher:
class ClashSettings: class ClashSettings:
def __init__(self): def __init__(self):
self.logger = None self.logger: Logger = None
self.output = "clashes.json" self.output = "clashes.json"