1919# along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
2020
2121
22+ from __future__ import annotations
2223import json
2324import time
2425import numpy as np
2526import multiprocessing
2627import ifcopenshell
2728import ifcopenshell .geom
2829import 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
3180class 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
279337class ClashSettings :
280338 def __init__ (self ):
281- self .logger = None
339+ self .logger : Logger = None
282340 self .output = "clashes.json"
0 commit comments