Skip to content

Commit bb39748

Browse files
committed
Rudimentary ifcopenshell-compatible file/instance interface
1 parent db8ec7c commit bb39748

File tree

1 file changed

+115
-15
lines changed

1 file changed

+115
-15
lines changed

main.py

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import builtins
2+
from dataclasses import dataclass
3+
import itertools
4+
import numbers
15
import time
26
import sys
37
import os
@@ -6,6 +10,8 @@
610
import re
711

812
from collections import defaultdict
13+
import types
14+
import typing
915

1016
from lark import Lark, Transformer, Tree, Token
1117
from 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+
239257
def 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+
355455
if __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

Comments
 (0)