1+ import builtins
2+ from dataclasses import dataclass
3+ import itertools
4+ import numbers
15import time
26import sys
37import os
610import re
711
812from collections import defaultdict
13+ import types
14+ import typing
915
1016from lark import Lark , Transformer , Tree , Token
1117from lark .exceptions import UnexpectedToken , UnexpectedCharacters
@@ -236,6 +242,18 @@ def enumeration(self, s):
236242 STAR = str
237243
238244
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+
239257def create_step_entity (entity_tree ):
240258 entity = {}
241259 t = T (visit_tokens = True ).transform (entity_tree )
@@ -252,31 +270,38 @@ def traverse(fn, x):
252270
253271 lines = list (traverse (get_line_number , entity_tree ))
254272
255- id_tree = t .children [0 ].children [0 ]
256-
257273 entity_id = t .children [0 ].children [0 ]
258274 entity_type = t .children [0 ].children [1 ].children [0 ]
259275
260276 attributes_tree = t .children [0 ].children [1 ].children [1 ]
261277 attributes = list (attributes_tree )
262278
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+ )
269285
270286
271- def process_tree (filecontent , file_tree , with_progress ):
287+ def process_tree (filecontent , file_tree , with_progress , with_header = False ):
272288 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 ))
273298
274- n = len (file_tree . children [ 1 ] .children )
299+ n = len (data .children )
275300 if n :
276301 percentages = [i * 100.0 / n for i in range (n + 1 )]
277302 num_dots = [int (b ) - int (a ) for a , b in zip (percentages , percentages [1 :])]
278303
279- for idx , entity_tree in enumerate (file_tree . children [ 1 ] .children ):
304+ for idx , entity_tree in enumerate (data .children ):
280305 if with_progress :
281306 sys .stdout .write (num_dots [idx ] * "." )
282307 sys .stdout .flush ()
@@ -286,13 +311,16 @@ def process_tree(filecontent, file_tree, with_progress):
286311 raise DuplicateNameError (filecontent , ent ["id" ], ent ["lines" ])
287312 ents [id_ ].append (ent )
288313
289- return ents
314+ if with_header :
315+ return header , ents
316+ else :
317+ return ents
290318
291319
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 ):
293321 if filename :
294322 assert not filecontent
295- filecontent = open (filename , encoding = None ).read ()
323+ filecontent = builtins . open (filename , encoding = None ).read ()
296324
297325 # Match and remove the comments
298326 p = r"/\*[\s\S]*?\*/"
@@ -340,7 +368,7 @@ def replace_fn(match):
340368 raise SyntaxError (filecontent , e )
341369
342370 if with_tree :
343- return process_tree (filecontent , ast , with_progress )
371+ return process_tree (filecontent , ast , with_progress , with_header )
344372 else :
345373 # process_tree() would take care of duplicate identifiers,
346374 # but we need to do it ourselves now using our rudimentary
@@ -352,6 +380,78 @@ def replace_fn(match):
352380 seen .add (iden )
353381
354382
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+
355455if __name__ == "__main__" :
356456 args = [x for x in sys .argv [1 :] if not x .startswith ("-" )]
357457 flags = [x for x in sys .argv [1 :] if x .startswith ("-" )]
0 commit comments