1
+ import builtins
2
+ from dataclasses import dataclass
3
+ import itertools
4
+ import numbers
1
5
import time
2
6
import sys
3
7
import os
6
10
import re
7
11
8
12
from collections import defaultdict
13
+ import types
14
+ import typing
9
15
10
16
from lark import Lark , Transformer , Tree , Token
11
17
from lark .exceptions import UnexpectedToken , UnexpectedCharacters
@@ -236,6 +242,18 @@ def enumeration(self, s):
236
242
STAR = str
237
243
238
244
245
+ @dataclass
246
+ class entity_instance :
247
+ id : int
248
+ type : str
249
+ attributes : tuple
250
+ lines : tuple
251
+ def __getitem__ (self , k ):
252
+ # compatibility with dict
253
+ return getattr (self , k )
254
+ def __repr__ (self ):
255
+ return f'#{ self .id } ={ self .type } ({ "," .join (map (str , self .attributes ))} )'
256
+
239
257
def create_step_entity (entity_tree ):
240
258
entity = {}
241
259
t = T (visit_tokens = True ).transform (entity_tree )
@@ -252,31 +270,38 @@ def traverse(fn, x):
252
270
253
271
lines = list (traverse (get_line_number , entity_tree ))
254
272
255
- id_tree = t .children [0 ].children [0 ]
256
-
257
273
entity_id = t .children [0 ].children [0 ]
258
274
entity_type = t .children [0 ].children [1 ].children [0 ]
259
275
260
276
attributes_tree = t .children [0 ].children [1 ].children [1 ]
261
277
attributes = list (attributes_tree )
262
278
263
- return {
264
- "id" : entity_id ,
265
- "type" : entity_type ,
266
- "attributes" : attributes ,
267
- "lines" : (min (lines ), max (lines )),
268
- }
279
+ return entity_instance (
280
+ entity_id ,
281
+ entity_type ,
282
+ attributes ,
283
+ (min (lines ), max (lines )),
284
+ )
269
285
270
286
271
- def process_tree (filecontent , file_tree , with_progress ):
287
+ def process_tree (filecontent , file_tree , with_progress , with_header = False ):
272
288
ents = defaultdict (list )
289
+ header , data = file_tree .children
290
+
291
+ def make_header_ent (ast ):
292
+ kw , param_list = ast .children
293
+ kw = kw .children [0 ].value
294
+ return kw , T (visit_tokens = True ).transform (param_list )
295
+
296
+ if with_header :
297
+ header = dict (map (make_header_ent , header .children [0 ].children ))
273
298
274
- n = len (file_tree . children [ 1 ] .children )
299
+ n = len (data .children )
275
300
if n :
276
301
percentages = [i * 100.0 / n for i in range (n + 1 )]
277
302
num_dots = [int (b ) - int (a ) for a , b in zip (percentages , percentages [1 :])]
278
303
279
- for idx , entity_tree in enumerate (file_tree . children [ 1 ] .children ):
304
+ for idx , entity_tree in enumerate (data .children ):
280
305
if with_progress :
281
306
sys .stdout .write (num_dots [idx ] * "." )
282
307
sys .stdout .flush ()
@@ -286,13 +311,16 @@ def process_tree(filecontent, file_tree, with_progress):
286
311
raise DuplicateNameError (filecontent , ent ["id" ], ent ["lines" ])
287
312
ents [id_ ].append (ent )
288
313
289
- return ents
314
+ if with_header :
315
+ return header , ents
316
+ else :
317
+ return ents
290
318
291
319
292
- def parse (* , filename = None , filecontent = None , with_progress = False , with_tree = True ):
320
+ def parse (* , filename = None , filecontent = None , with_progress = False , with_tree = True , with_header = False ):
293
321
if filename :
294
322
assert not filecontent
295
- filecontent = open (filename , encoding = None ).read ()
323
+ filecontent = builtins . open (filename , encoding = None ).read ()
296
324
297
325
# Match and remove the comments
298
326
p = r"/\*[\s\S]*?\*/"
@@ -340,7 +368,7 @@ def replace_fn(match):
340
368
raise SyntaxError (filecontent , e )
341
369
342
370
if with_tree :
343
- return process_tree (filecontent , ast , with_progress )
371
+ return process_tree (filecontent , ast , with_progress , with_header )
344
372
else :
345
373
# process_tree() would take care of duplicate identifiers,
346
374
# but we need to do it ourselves now using our rudimentary
@@ -352,6 +380,78 @@ def replace_fn(match):
352
380
seen .add (iden )
353
381
354
382
383
+ class file :
384
+ """
385
+ A somewhat compatible interface (but very limited) to ifcopenshell.file
386
+ """
387
+ def __init__ (self , parse_outcomes ):
388
+ self .header_ , self .data_ = parse_outcomes
389
+
390
+ @property
391
+ def schema_identifier (self ) -> str :
392
+ return self .header_ ['FILE_SCHEMA' ][0 ][0 ]
393
+
394
+ @property
395
+ def schema (self ) -> str :
396
+ """General IFC schema version: IFC2X3, IFC4, IFC4X3."""
397
+ prefixes = ("IFC" , "X" , "_ADD" , "_TC" )
398
+ reg = "" .join (f"(?P<{ s } >{ s } \\ d+)?" for s in prefixes )
399
+ match = re .match (reg , self .schema_identifier )
400
+ version_tuple = tuple (
401
+ map (
402
+ lambda pp : int (pp [1 ][len (pp [0 ]) :]) if pp [1 ] else None ,
403
+ ((p , match .group (p )) for p in prefixes ),
404
+ )
405
+ )
406
+ return "" .join ("" .join (map (str , t )) if t [1 ] else "" for t in zip (prefixes , version_tuple [0 :2 ]))
407
+
408
+ @property
409
+ def schema_version (self ) -> tuple [int , int , int , int ]:
410
+ """Numeric representation of the full IFC schema version.
411
+
412
+ E.g. IFC4X3_ADD2 is represented as (4, 3, 2, 0).
413
+ """
414
+ schema = self .wrapped_data .schema
415
+ version = []
416
+ for prefix in ("IFC" , "X" , "_ADD" , "_TC" ):
417
+ number = re .search (prefix + r"(\d)" , schema )
418
+ version .append (int (number .group (1 )) if number else 0 )
419
+ return tuple (version )
420
+
421
+ @property
422
+ def header (self ):
423
+ return types .SimpleNamespace (** {k .lower (): v for k , v in self .header_ .items ()})
424
+
425
+ def __getitem__ (self , key : numbers .Integral ) -> entity_instance :
426
+ return self .by_id (key )
427
+
428
+ def by_id (self , id : int ) -> entity_instance :
429
+ """Return an IFC entity instance filtered by IFC ID.
430
+
431
+ :param id: STEP numerical identifier
432
+ :type id: int
433
+
434
+ :raises RuntimeError: If `id` is not found or multiple definitions exist for `id`.
435
+
436
+ :rtype: entity_instance
437
+ """
438
+ ns = self .data_ .get (id , [])
439
+ if len (ns ) == 0 :
440
+ raise RuntimeError (f"Instance with id { id } not found" )
441
+ elif len (ns ) > 1 :
442
+ raise RuntimeError (f"Duplicate definition for id { id } " )
443
+ return entity_instance (ns , ns [0 ])
444
+
445
+ def by_type (self , type : str ) -> list [entity_instance ]:
446
+ """Return IFC objects filtered by IFC Type and wrapped with the entity_instance class.
447
+ :rtype: list[entity_instance]
448
+ """
449
+ type_lc = type .lower ()
450
+ return list (filter (lambda ent : ent .type .lower () == type_lc , itertools .chain .from_iterable (self .data_ .values ())))
451
+
452
+ def open (fn ) -> file :
453
+ return file (parse (filename = fn , with_tree = True , with_header = True ))
454
+
355
455
if __name__ == "__main__" :
356
456
args = [x for x in sys .argv [1 :] if not x .startswith ("-" )]
357
457
flags = [x for x in sys .argv [1 :] if x .startswith ("-" )]
0 commit comments