Skip to content

Commit 3ad58ab

Browse files
committed
ifcclash typing
1 parent 9131804 commit 3ad58ab

4 files changed

Lines changed: 93 additions & 31 deletions

File tree

src/bonsai/bonsai/bim/module/clash/operator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ def get_viewpoint_snapshot(viewpoint):
284284
if extension == ".json":
285285
tool.Clash.load_clash_sets(self.filepath)
286286
tool.Clash.import_active_clashes()
287+
self.report({"INFO"}, "Finished IFC clash.")
287288
return {"FINISHED"}
288289

289290

src/bonsai/bonsai/bim/module/clash/prop.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ class BIMClashProperties(PropertyGroup):
8686
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
8787
blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty)
8888
clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet)
89-
should_create_clash_snapshots: BoolProperty(name="Create Snapshots", default=False)
89+
should_create_clash_snapshots: BoolProperty(
90+
name="Create Snapshots", description="Create bcf snapshots", default=False
91+
)
9092
clash_results_path: StringProperty(name="Clash Results Path")
9193
smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path")
9294
active_clash_set_index: IntProperty(name="Active Clash Set Index")

src/bonsai/bonsai/tool/clash.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@
2424
import bonsai.tool as tool
2525
from contextlib import contextmanager
2626
from mathutils import Vector
27+
from ifcclash import ifcclash
2728

2829

2930
class Clash(bonsai.core.tool.Clash):
3031

3132
@classmethod
32-
def export_clash_sets(cls):
33+
def export_clash_sets(cls) -> list[ifcclash.ClashSet]:
3334
clash_sets = []
3435
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets:
3536
a = []

src/ifcclash/ifcclash/ifcclash.py

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,79 @@
1919
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
2020

2121

22+
from __future__ import annotations
2223
import json
2324
import time
2425
import numpy as np
2526
import multiprocessing
2627
import ifcopenshell
2728
import ifcopenshell.geom
2829
import ifcopenshell.util.selector
30+
from logging import Logger
31+
from typing import Optional, Literal, TypedDict, NotRequired
32+
33+
34+
class ClashSource(TypedDict):
35+
file: str
36+
mode: NotRequired[Literal["a", "e", "i"]]
37+
selector: NotRequired[str]
38+
# Will be automatically added during clash.
39+
ifc: NotRequired[ifcopenshell.file]
40+
41+
42+
ClashType = Literal["protrusion", "pierce", "collision", "clearance"]
43+
44+
45+
class ClashResult(TypedDict):
46+
a_global_id: str
47+
b_global_id: str
48+
a_ifc_class: str
49+
b_ifc_class: str
50+
a_name: str
51+
b_name: str
52+
type: ClashType
53+
p1: list[float]
54+
p2: list[float]
55+
distance: float
56+
57+
58+
class ClashSet(TypedDict):
59+
name: str
60+
a: list[ClashSource]
61+
b: NotRequired[list[ClashSource]]
62+
mode: Literal["intersection", "collision", "clearance"]
63+
# Added during clash.
64+
clashes: NotRequired[dict[str, ClashResult]]
65+
# intersection, clearance modes.
66+
check_all: NotRequired[bool]
67+
# inseresection mode.
68+
tolerance: NotRequired[float]
69+
# collision mode.
70+
allow_touching: NotRequired[bool]
71+
# clearance mode.
72+
clearance: NotRequired[float]
73+
74+
75+
class ClashGroup(TypedDict):
76+
elements: dict
77+
objects: dict
2978

3079

3180
class Clasher:
32-
def __init__(self, settings):
81+
def __init__(self, settings: ClashSettings):
3382
self.settings = settings
3483
self.geom_settings = ifcopenshell.geom.settings()
35-
self.clash_sets = []
84+
self.clash_sets: list[ClashSet] = []
3685
self.logger = self.settings.logger
37-
self.groups = {}
38-
self.ifcs = {}
86+
self.groups: dict[str, ClashGroup] = {}
87+
self.ifcs: dict[str, ifcopenshell.file] = {}
3988
self.tree = None
4089

41-
def clash(self):
90+
def clash(self) -> None:
4291
for clash_set in self.clash_sets:
4392
self.process_clash_set(clash_set)
4493

45-
def process_clash_set(self, clash_set):
94+
def process_clash_set(self, clash_set: ClashSet) -> None:
4695
self.tree = ifcopenshell.geom.tree()
4796
self.create_group("a")
4897
for source in clash_set["a"]:
@@ -79,42 +128,51 @@ def process_clash_set(self, clash_set):
79128
clearance=clash_set["clearance"],
80129
check_all=clash_set["check_all"],
81130
)
131+
else:
132+
assert False, f"Unexpected mode '{mode}'."
82133

83-
processed_results = {}
134+
processed_results: dict[str, ClashResult] = {}
84135
for result in results:
85136
element1 = result.a
86137
element2 = result.b
87138

88-
processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = {
89-
"a_global_id": element1.get_argument(0),
90-
"b_global_id": element2.get_argument(0),
91-
"a_ifc_class": element1.is_a(),
92-
"b_ifc_class": element2.is_a(),
93-
"a_name": element1.get_argument(2),
94-
"b_name": element2.get_argument(2),
95-
"type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type],
96-
"p1": list(result.p1),
97-
"p2": list(result.p2),
98-
"distance": result.distance,
99-
}
139+
processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = ClashResult(
140+
a_global_id=element1.get_argument(0),
141+
b_global_id=element2.get_argument(0),
142+
a_ifc_class=element1.is_a(),
143+
b_ifc_class=element2.is_a(),
144+
a_name=element1.get_argument(2),
145+
b_name=element2.get_argument(2),
146+
type=["protrusion", "pierce", "collision", "clearance"][result.clash_type],
147+
p1=list(result.p1),
148+
p2=list(result.p2),
149+
distance=result.distance,
150+
)
100151
clash_set["clashes"] = processed_results
101152
self.logger.info(f"Found clashes: {len(processed_results.keys())}")
102153

103-
def create_group(self, name):
154+
def create_group(self, name: str) -> None:
104155
self.logger.info(f"Creating group {name}")
105156
self.groups[name] = {"elements": {}, "objects": {}}
106157

107-
def load_ifc(self, path):
158+
def load_ifc(self, path: str) -> ifcopenshell.file:
108159
start = time.time()
109160
self.settings.logger.info(f"Loading IFC {path}")
110161
ifc = self.ifcs.get(path, None)
111162
if not ifc:
112163
ifc = ifcopenshell.open(path)
164+
assert isinstance(ifc, ifcopenshell.file)
113165
self.ifcs[path] = ifc
114166
self.settings.logger.info(f"Loading finished {time.time() - start}")
115167
return ifc
116168

117-
def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
169+
def add_collision_objects(
170+
self,
171+
name: str,
172+
ifc_file: ifcopenshell.file,
173+
mode: Optional[Literal["a", "e", "i"]] = None,
174+
selector: Optional[str] = None,
175+
) -> None:
118176
start = time.time()
119177
self.settings.logger.info("Creating iterator")
120178
if not mode or mode == "a" or not selector:
@@ -132,7 +190,7 @@ def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
132190
self.settings.logger.info(f"Iterator creation finished {time.time() - start}")
133191

134192
start = time.time()
135-
self.logger.info(f"Adding objects {name}")
193+
self.logger.info(f"Adding objects {name} ({len(elements)} elements)")
136194
assert iterator.initialize()
137195
while True:
138196
self.tree.add_element(iterator.get())
@@ -145,12 +203,12 @@ def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
145203
self.logger.info(f"Element metadata finished {time.time() - start}")
146204
start = time.time()
147205

148-
def export(self):
206+
def export(self) -> None:
149207
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
150208
return self.export_bcfxml()
151209
self.export_json()
152210

153-
def export_bcfxml(self):
211+
def export_bcfxml(self) -> None:
154212
from bcf.v2.bcfxml import BcfXml
155213

156214
for i, clash_set in enumerate(self.clash_sets):
@@ -170,12 +228,12 @@ def export_bcfxml(self):
170228
suffix = f".{i}" if i else ""
171229
bcfxml.save_project(f"{self.settings.output}{suffix}")
172230

173-
def get_viewpoint_snapshot(self, viewpoint):
231+
def get_viewpoint_snapshot(self, viewpoint) -> None:
174232
# Possible to overload this function in a GUI application if used as a library.
175233
# Should return a tuple of (filename, bytes).
176234
return None
177235

178-
def export_json(self):
236+
def export_json(self) -> None:
179237
clash_sets = self.clash_sets.copy()
180238
for clash_set in clash_sets:
181239
for source in clash_set["a"]:
@@ -185,7 +243,7 @@ def export_json(self):
185243
with open(self.settings.output, "w", encoding="utf-8") as clashes_file:
186244
json.dump(clash_sets, clashes_file, indent=4)
187245

188-
def smart_group_clashes(self, clash_sets, max_clustering_distance):
246+
def smart_group_clashes(self, clash_sets: list[ClashSet], max_clustering_distance: float):
189247
from sklearn.cluster import OPTICS
190248
from collections import defaultdict
191249

@@ -278,5 +336,5 @@ def smart_group_clashes(self, clash_sets, max_clustering_distance):
278336

279337
class ClashSettings:
280338
def __init__(self):
281-
self.logger = None
339+
self.logger: Logger = None
282340
self.output = "clashes.json"

0 commit comments

Comments
 (0)