This commit is contained in:
Andrej730
2025-05-06 11:11:02 +05:00
parent c6c852c7f6
commit b0f2785e99
4 changed files with 100 additions and 92 deletions
+4 -5
View File
@@ -209,6 +209,7 @@ NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
class file:
"""Base class for containing IFC files.
@@ -466,14 +467,12 @@ class file:
number = re.search(prefix + r"(\d)", schema)
version.append(int(number.group(1)) if number else 0)
return tuple(version)
@property
@property
def mvd(self):
if not LARK_AVAILABLE:
return None
return MvdInfo(
self.header
)
return MvdInfo(self.header)
def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]:
if attr[0:6] == "create":
@@ -1,6 +1,7 @@
try:
from lark import Lark, Transformer
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
LARK_AVAILABLE = True
except ImportError:
LARK_AVAILABLE = False
@@ -9,7 +10,7 @@ from typing import Callable
import re
if LARK_AVAILABLE:
mvd_grammar = r'''
mvd_grammar = r"""
start: entry+
entry: "ViewDefinition" "[" simple_value_list "]" -> view_definition
@@ -36,9 +37,9 @@ if LARK_AVAILABLE:
%import common.WS
%ignore WS
'''
"""
parser = Lark(mvd_grammar, parser='lalr')
parser = Lark(mvd_grammar, parser="lalr")
class DescriptionTransform(Transformer):
def __init__(self):
@@ -50,7 +51,7 @@ if LARK_AVAILABLE:
self._dynamic = {}
def view_definition(self, args):
self.keywords.add('view_definitions')
self.keywords.add("view_definitions")
self.view_definitions.extend(args[0])
def store_text_attribute(self, args, keyword):
@@ -66,17 +67,17 @@ if LARK_AVAILABLE:
def option(self, args):
if v := parse_semicolon_separated_kv(" ".join(" ".join(str(child) for child in args[0].children).split())):
setattr(self, 'options', v)
setattr(self, "options", v)
else:
self.store_text_attribute(args, "options")
def dynamic_option(self, args):
try:
original_keyword = str(args[0])
key = original_keyword.lower()
original_keyword = str(args[0])
key = original_keyword.lower()
raw_text = args[1].children[0].value
parsed_value = parse_semicolon_separated_kv(raw_text)
self._dynamic[key] = (parsed_value, original_keyword)
self._dynamic[key] = (parsed_value, original_keyword)
self.keywords.add(key)
setattr(self, key, parsed_value)
except Exception:
@@ -98,7 +99,7 @@ if LARK_AVAILABLE:
return str(args[0])
def parse_mvd(description):
text = ' '.join(description)
text = " ".join(description)
parsed_description = DescriptionTransform()
try:
if not text:
@@ -111,21 +112,23 @@ if LARK_AVAILABLE:
return parsed_description
def parse_semicolon_separated_kv(text: str) -> dict[str, str | list[str]] | None:
if not re.search(r'\w+\s*:\s*[^:]+', text):
if not re.search(r"\w+\s*:\s*[^:]+", text):
return None
result = {}
try:
pairs = text.split(';')
pairs = text.split(";")
for pair in pairs:
if ':' in pair:
key, value = pair.split(':', 1)
if ":" in pair:
key, value = pair.split(":", 1)
key = key.strip()
values = [v.strip() for v in value.split(',')]
values = [v.strip() for v in value.split(",")]
result[key] = values[0] if len(values) == 1 else values
return result
except Exception:
return None
else:
def parse_mvd(description):
return None
@@ -161,7 +164,7 @@ class MvdInfo:
return AutoCommitList(
vd_list,
callback=lambda val: (self._update_keyword("ViewDefinition", val), setattr(self, "_parsed", None)),
formatter=lambda lst: ",".join(str(i) for i in lst)
formatter=lambda lst: ",".join(str(i) for i in lst),
)
@view_definitions.setter
@@ -180,7 +183,7 @@ class MvdInfo:
return AutoCommitList(
comment_list,
callback=lambda val: self._update_keyword("Comment", val),
formatter=lambda lst: ", ".join(str(i) for i in lst)
formatter=lambda lst: ", ".join(str(i) for i in lst),
)
@comments.setter
@@ -232,7 +235,7 @@ class MvdInfo:
def __getattr__(self, name):
self._ensure_parsed()
if hasattr(self._parsed, '_dynamic'):
if hasattr(self._parsed, "_dynamic"):
name_lc = name.lower()
if name_lc in self._parsed._dynamic:
value, original_keyword = self._parsed._dynamic[name_lc]
@@ -241,7 +244,7 @@ class MvdInfo:
def __dir__(self):
base = super().__dir__()
if self._parsed and hasattr(self._parsed, '_dynamic'):
if self._parsed and hasattr(self._parsed, "_dynamic"):
return base + [kw for _, kw in self._parsed._dynamic.values()]
return base
@@ -258,10 +261,7 @@ class DictionaryHandler(dict):
super().__setitem__(k, v)
def _commit(self):
new_value = "; ".join(
f"{k}: {', '.join(v) if isinstance(v, list) else v}"
for k, v in self.items()
)
new_value = "; ".join(f"{k}: {', '.join(v) if isinstance(v, list) else v}" for k, v in self.items())
self._mvdinfo._update_keyword(self._keyword, new_value)
def __setitem__(self, key, value):
@@ -277,10 +277,11 @@ class DictionaryHandler(dict):
class AutoCommitList(list):
"ensures keyword attributes are written back to ifcopenshell.file.header"
def __init__(self, iterable, callback, formatter=None):
super().__init__(iterable)
self._callback = callback
self._formatter = formatter
self._formatter = formatter
def _commit(self):
if self._formatter:
@@ -319,4 +320,4 @@ class AutoCommitList(list):
def __delitem__(self, index):
super().__delitem__(index)
self._commit()
self._commit()
@@ -597,24 +597,28 @@ def validate_guid(guid: str) -> Union[str, None]:
return "Couldn't decompress guid, it's not base64 encoded."
return None
def to_string_header_entity(header_entity):
"""Recreate IFC header string representation, like FILE_NAME(...)"""
# Prefer native .toString() if available (native IfcOpenShell wrapper)
if isinstance(header_entity, W.HeaderEntity):
return header_entity.toString()
elif hasattr(header_entity, '_fields'):
elif hasattr(header_entity, "_fields"):
values = [repr(getattr(header_entity, f)) for f in header_entity._fields]
return f"{type(header_entity).__name__.upper()}({','.join(values)})"
else:
raise TypeError(f"Cannot stringify header_entity of type {type(header_entity)}")
def validate_ifc_header(f: Union[ifcopenshell.file, ifcopenshell.simple_spf.file], logger: Logger) -> None:
header: Union[W.IfcSpfHeader, types.SimpleNamespace] = f.header
AGGREGATE_TYPE = "LIST [ 1 : ? ] OF STRING (256)"
STRING_TYPE = "STRING (256)"
def log_error(header_entity: Union[W.HeaderEntity, tuple], name: str, index: int, expected_type: str, provided_type: str) -> None:
def log_error(
header_entity: Union[W.HeaderEntity, tuple], name: str, index: int, expected_type: str, provided_type: str
) -> None:
logger.error(
(
"For instance:\n %s\n %s\n"