mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
mvd info to module, add testing, setattr & getattr
This commit is contained in:
committed by
Thomas Krijnen
parent
9f11568aff
commit
cbe8fc1ebd
@@ -37,9 +37,7 @@ from typing import Any
|
||||
from . import ifcopenshell_wrapper
|
||||
from .entity_instance import entity_instance
|
||||
|
||||
from lark import Lark, Transformer
|
||||
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
|
||||
|
||||
from ifcopenshell.util.mvd_info import MvdInfo, LARK_AVAILABLE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ifcopenshell.util.schema
|
||||
@@ -211,106 +209,6 @@ 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
|
||||
|
||||
mvd_grammar = r'''
|
||||
start: entry+
|
||||
|
||||
entry: "ViewDefinition" "[" simple_value_list "]" -> view_definition
|
||||
| "Comment" "[" comment_text "]" -> comment
|
||||
| GENERIC_KEYWORD "[" value_list_set "]" -> dynamic_option
|
||||
|
||||
%declare COMMENT_TEXT // Ensure Lark treats it with higher priority
|
||||
|
||||
GENERIC_KEYWORD: /[A-Za-z0-9_]+/
|
||||
|
||||
simple_value_list: value ("," value)*
|
||||
|
||||
value_list_set: value_set (";" value_set)*
|
||||
|
||||
value_set: set_name ":" simple_value_list
|
||||
|
||||
set_name: /[A-Za-z0-9_]+/
|
||||
|
||||
value: /[A-Za-z0-9 _\.-]+/
|
||||
|
||||
comment_text: /[^\[\]]+/ // Free text inside COMMENT brackets
|
||||
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
'''
|
||||
|
||||
parser = Lark(mvd_grammar, parser='lalr')
|
||||
class DescriptionTransform(Transformer):
|
||||
def __init__(self):
|
||||
self.mvd = []
|
||||
self.keywords = set()
|
||||
self.comments = ""
|
||||
self.exchangerequirement = ""
|
||||
self.option = ''
|
||||
|
||||
|
||||
def view_definition(self, args):
|
||||
self.keywords.add('mvd')
|
||||
self.mvd.extend(args[0])
|
||||
|
||||
|
||||
def dynamic_option(self, args):
|
||||
"""
|
||||
e.g. in case of 'Remark' as optional keyword in the description
|
||||
The value can be retrieved through DescriptionTransform.remark
|
||||
"""
|
||||
key = str(args[0]).lower()
|
||||
attr_name = f"{key}"
|
||||
if attr_name not in self.keywords:
|
||||
setattr(self, attr_name, {})
|
||||
self.keywords.add(attr_name)
|
||||
dynamic_dict = getattr(self, attr_name)
|
||||
for value_set in args[1]:
|
||||
set_name, *values = value_set
|
||||
dynamic_dict[set_name] = values if len(values) > 1 else values[0]
|
||||
|
||||
|
||||
def comment(self, args):
|
||||
self.keywords.add('comment')
|
||||
self.comments = " ".join(str(child) for child in args[0].children).strip()
|
||||
|
||||
def simple_value_list(self, args):
|
||||
return [str(arg) for arg in args]
|
||||
|
||||
def value_list_set(self, args):
|
||||
return args
|
||||
|
||||
def value_set(self, args):
|
||||
return [str(args[0])] + args[1]
|
||||
|
||||
def value(self, args):
|
||||
return str(args[0])
|
||||
|
||||
def set_name(self, args):
|
||||
return str(args[0])
|
||||
|
||||
@property
|
||||
def other_keywords(self):
|
||||
""""
|
||||
The predefined keywords are 'ViewDefinition', 'Option', 'Comment', 'ExchangeRequirement' and 'Option'
|
||||
Keywords in the description not from this lists are returned
|
||||
"""
|
||||
return {k for k in self.keywords if k not in {'mvd', 'comment', 'exchangerequirement', 'option'}}
|
||||
|
||||
|
||||
def parse_mvd(description):
|
||||
text = ' '.join(description)
|
||||
parser = Lark(mvd_grammar, parser='lalr')
|
||||
parsed_description = DescriptionTransform()
|
||||
try:
|
||||
if not text:
|
||||
parsed_description.mvd = None
|
||||
return parsed_description
|
||||
parse_tree = parser.parse(text)
|
||||
parsed_description.transform(parse_tree)
|
||||
except (UnexpectedCharacters, UnexpectedEOF, UnexpectedToken) as e:
|
||||
parsed_description.mvd = None
|
||||
return parsed_description
|
||||
|
||||
class file:
|
||||
"""Base class for containing IFC files.
|
||||
|
||||
@@ -409,7 +307,6 @@ class file:
|
||||
import weakref
|
||||
|
||||
file_dict[self.file_pointer()] = weakref.ref(self)
|
||||
self.parsed_description = parse_mvd(self.wrapped_data.header.file_description.description)
|
||||
|
||||
def __del__(self) -> None:
|
||||
# Avoid infinite recursion if file is failed to initialize
|
||||
@@ -569,68 +466,16 @@ class file:
|
||||
number = re.search(prefix + r"(\d)", schema)
|
||||
version.append(int(number.group(1)) if number else 0)
|
||||
return tuple(version)
|
||||
|
||||
@property
|
||||
def mvd(self) -> str:
|
||||
"""
|
||||
View Definition supported by the exporting application that is reflected in the IFC file
|
||||
For example:
|
||||
“CoordinationView”
|
||||
| “PresentationView”
|
||||
| “StructuralAnalysisView”
|
||||
| “FMHandOverView”
|
||||
| “QuantityTakeOffAddOnView”
|
||||
| “SpaceBoundary1stLevelAddOnView”
|
||||
| “SpaceBoundary2ndLevelAddOnView”
|
||||
"""
|
||||
return ','.join(self.parsed_description.mvd)
|
||||
|
||||
@property
|
||||
def mvd_comments(self) -> str:
|
||||
"""
|
||||
For example; 'Comment [This
|
||||
export contains Boolean Operation geometry that may not be fully
|
||||
supported by other application]'
|
||||
|
||||
f.mvd_comments
|
||||
'This export contains Boolean Operation geometry that may not be fully supported by other application'
|
||||
"""
|
||||
return str(self.parsed_description.comments)
|
||||
|
||||
@property
|
||||
def mvd_exchange_requirements(self) -> str:
|
||||
return str(self.parsed_description.exchangerequirement)
|
||||
|
||||
@property
|
||||
def mvd_options(self) -> str:
|
||||
"""
|
||||
Option being used by the exporting application for e.g. quality control and debugging
|
||||
a. this is an optional field providing informal information, the values for the Option keyword
|
||||
reflect the export settings of the exporting software application, those settings may be
|
||||
specific to the exporting software
|
||||
For example:
|
||||
> "{'ExcludedObjects': [' Stair', ' Ramp', ' Space'], 'SplitLevel': ' On'}"
|
||||
"""
|
||||
return str(self.parsed_description.option)
|
||||
|
||||
@property
|
||||
def mvd_keywords(self) -> str:
|
||||
"""
|
||||
Returns all keywords from description, e.g.
|
||||
f.mvd_keywords
|
||||
"{'exchangerequirement', 'mvd', 'option', 'comment', 'remark'}"_
|
||||
"""
|
||||
return str(self.parsed_description.keywords)
|
||||
|
||||
@property
|
||||
def mvd_optional_keyword_fields(self) -> str:
|
||||
""""
|
||||
Returns fields from keywords other than 'ViewDefinition', 'Option', 'Comment' and 'ExchangeRequirement'.
|
||||
For example, in case of
|
||||
'REMARK [SomeKey: SomeValue; AnotherKey: AnotherValue]'
|
||||
file.mvd_optional_keywords_fields == "{'remark': {'SomeKey': ' SomeValue', 'AnotherKey': ' AnotherValue'}}"
|
||||
"""
|
||||
return str({kw: getattr(self.parsed_description, kw) for kw in self.parsed_description.other_keywords})
|
||||
def mvd(self):
|
||||
if not LARK_AVAILABLE:
|
||||
return None
|
||||
file_description = self.wrapped_data.header.file_description
|
||||
return MvdInfo(
|
||||
get_description=lambda: file_description.description,
|
||||
set_description=lambda d: setattr(file_description, "description", tuple(d))
|
||||
)
|
||||
|
||||
def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]:
|
||||
if attr[0:6] == "create":
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
try:
|
||||
from lark import Lark, Transformer
|
||||
from lark.exceptions import UnexpectedCharacters, UnexpectedEOF, UnexpectedToken
|
||||
LARK_AVAILABLE = True
|
||||
except ImportError:
|
||||
LARK_AVAILABLE = False
|
||||
|
||||
from typing import Callable
|
||||
import re
|
||||
|
||||
if LARK_AVAILABLE:
|
||||
mvd_grammar = r'''
|
||||
start: entry+
|
||||
|
||||
entry: "ViewDefinition" "[" simple_value_list "]" -> view_definition
|
||||
| "Comment" "[" simple_value_list "]" -> comment
|
||||
| "ExchangeRequirement" "[" other_keyword "]" -> exchangerequirement
|
||||
| "Option" "[" other_keyword "]" -> option
|
||||
| GENERIC_KEYWORD "[" dynamic_option_word "]" -> dynamic_option
|
||||
|
||||
GENERIC_KEYWORD: /[A-Za-z0-9_]+/
|
||||
|
||||
simple_value_list: value ("," value)*
|
||||
|
||||
value_list_set: value_set (";" value_set)*
|
||||
|
||||
value_set: set_name ":" simple_value_list
|
||||
|
||||
set_name: /[A-Za-z0-9_]+/
|
||||
|
||||
value: /[A-Za-z0-9 _\.-]+/
|
||||
|
||||
other_keyword: /[^\[\]]+/
|
||||
|
||||
dynamic_option_word: /[^\[\]]+/
|
||||
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
'''
|
||||
|
||||
parser = Lark(mvd_grammar, parser='lalr')
|
||||
|
||||
class DescriptionTransform(Transformer):
|
||||
def __init__(self):
|
||||
self.view_definitions = []
|
||||
self.keywords = set()
|
||||
self.comments = ""
|
||||
self.exchange_requirements = ""
|
||||
self.options = ""
|
||||
self._dynamic = {}
|
||||
|
||||
def view_definition(self, args):
|
||||
self.keywords.add('view_definitions')
|
||||
self.view_definitions.extend(args[0])
|
||||
|
||||
def store_text_attribute(self, args, keyword):
|
||||
self.keywords.add(keyword)
|
||||
setattr(self, keyword, " ".join(" ".join(str(child) for child in args[0].children).split()))
|
||||
|
||||
def comment(self, args):
|
||||
self.keywords.add("comments")
|
||||
self.comments = args[0] if len(args[0]) > 1 else args[0][0]
|
||||
|
||||
def exchangerequirement(self, args):
|
||||
self.store_text_attribute(args, "exchange_requirements")
|
||||
|
||||
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)
|
||||
else:
|
||||
self.store_text_attribute(args, "options")
|
||||
|
||||
def dynamic_option(self, args):
|
||||
try:
|
||||
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.keywords.add(key)
|
||||
setattr(self, key, parsed_value)
|
||||
except Exception:
|
||||
setattr(self, key, None)
|
||||
|
||||
def simple_value_list(self, args):
|
||||
return [str(arg) for arg in args]
|
||||
|
||||
def value_list_set(self, args):
|
||||
return args
|
||||
|
||||
def value_set(self, args):
|
||||
return [str(args[0])] + args[1]
|
||||
|
||||
def value(self, args):
|
||||
return str(args[0])
|
||||
|
||||
def set_name(self, args):
|
||||
return str(args[0])
|
||||
|
||||
def parse_mvd(description):
|
||||
text = ' '.join(description)
|
||||
parsed_description = DescriptionTransform()
|
||||
try:
|
||||
if not text:
|
||||
parsed_description.view_definitions = None
|
||||
return parsed_description
|
||||
parse_tree = parser.parse(text)
|
||||
parsed_description.transform(parse_tree)
|
||||
except (UnexpectedCharacters, UnexpectedEOF, UnexpectedToken):
|
||||
parsed_description.view_definitions = None
|
||||
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):
|
||||
return None
|
||||
result = {}
|
||||
try:
|
||||
pairs = text.split(';')
|
||||
for pair in pairs:
|
||||
if ':' in pair:
|
||||
key, value = pair.split(':', 1)
|
||||
key = key.strip()
|
||||
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
|
||||
|
||||
|
||||
class MvdInfo:
|
||||
def __init__(self, get_description: Callable[[], list[str]], set_description: Callable[[list[str]], None]):
|
||||
self._get_description = get_description
|
||||
self._set_description = set_description
|
||||
self._parsed = None
|
||||
|
||||
def _ensure_parsed(self):
|
||||
if not LARK_AVAILABLE:
|
||||
return
|
||||
if self._parsed is None:
|
||||
self._parsed = parse_mvd(self.description)
|
||||
|
||||
@property
|
||||
def description(self) -> list[str]:
|
||||
return self._get_description()
|
||||
|
||||
@description.setter
|
||||
def description(self, new_description: list[str]):
|
||||
self._set_description(new_description)
|
||||
self._parsed = None
|
||||
|
||||
@property
|
||||
def view_definitions(self):
|
||||
self._ensure_parsed()
|
||||
return ', '.join(' '.join(item.split()) for item in self._parsed.view_definitions) if self._parsed else None
|
||||
|
||||
@view_definitions.setter
|
||||
def view_definitions(self, new_value: str):
|
||||
self._update_keyword("ViewDefinition", new_value)
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
self._ensure_parsed()
|
||||
comments = self._parsed.comments
|
||||
comment_list = comments if isinstance(comments, list) else [comments] if comments else []
|
||||
return AutoCommitList(
|
||||
comment_list,
|
||||
callback=lambda val: self._update_keyword("Comment", val),
|
||||
formatter=lambda lst: ", ".join(str(i) for i in lst)
|
||||
)
|
||||
|
||||
@comments.setter
|
||||
def comments(self, new_value: str | list[str]):
|
||||
if isinstance(new_value, list):
|
||||
value = ", ".join(new_value)
|
||||
else:
|
||||
value = str(new_value)
|
||||
self._update_keyword("Comment", value)
|
||||
|
||||
@property
|
||||
def exchange_requirements(self):
|
||||
self._ensure_parsed()
|
||||
return self._parsed.exchange_requirements if self._parsed else None
|
||||
|
||||
@exchange_requirements.setter
|
||||
def exchange_requirements(self, new_value: str):
|
||||
self._update_keyword("ExchangeRequirement", new_value)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
self._ensure_parsed()
|
||||
if isinstance(self._parsed.options, dict):
|
||||
return DictionaryHandler(self._parsed.options, self, "Option")
|
||||
return self._parsed.options if self._parsed else None
|
||||
|
||||
@options.setter
|
||||
def options(self, new_value: str):
|
||||
self._update_keyword("Option", new_value)
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
self._ensure_parsed()
|
||||
return self._parsed.keywords if self._parsed else set()
|
||||
|
||||
def _update_keyword(self, keyword: str, new_value: str):
|
||||
updated = False
|
||||
new_line = f"{keyword} [{new_value}]"
|
||||
lines = []
|
||||
for line in self.description:
|
||||
if line.strip().startswith(f"{keyword} ["):
|
||||
lines.append(new_line)
|
||||
updated = True
|
||||
else:
|
||||
lines.append(line)
|
||||
if not updated:
|
||||
lines.append(new_line)
|
||||
self.description = lines
|
||||
|
||||
def __getattr__(self, name):
|
||||
self._ensure_parsed()
|
||||
if hasattr(self._parsed, '_dynamic'):
|
||||
name_lc = name.lower()
|
||||
if name_lc in self._parsed._dynamic:
|
||||
value, original_keyword = self._parsed._dynamic[name_lc]
|
||||
return DictionaryHandler(value, self, original_keyword)
|
||||
raise AttributeError(f"'MvdInfo' object has no attribute '{name}'")
|
||||
|
||||
def __dir__(self):
|
||||
base = super().__dir__()
|
||||
if self._parsed and hasattr(self._parsed, '_dynamic'):
|
||||
return base + [kw for _, kw in self._parsed._dynamic.values()]
|
||||
return base
|
||||
|
||||
|
||||
class DictionaryHandler(dict):
|
||||
def __init__(self, initial_data, mvdinfo, keyword):
|
||||
super().__init__()
|
||||
self._mvdinfo = mvdinfo
|
||||
self._keyword = keyword
|
||||
for k, v in initial_data.items():
|
||||
if isinstance(v, list):
|
||||
super().__setitem__(k, AutoCommitList(v, self._commit))
|
||||
else:
|
||||
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()
|
||||
)
|
||||
self._mvdinfo._update_keyword(self._keyword, new_value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if isinstance(value, list):
|
||||
value = AutoCommitList(value, self._commit)
|
||||
super().__setitem__(key, value)
|
||||
self._commit()
|
||||
|
||||
def __delitem__(self, key):
|
||||
super().__delitem__(key)
|
||||
self._commit()
|
||||
|
||||
|
||||
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
|
||||
|
||||
def _commit(self):
|
||||
if self._formatter:
|
||||
self._callback(self._formatter(self))
|
||||
else:
|
||||
self._callback()
|
||||
|
||||
def append(self, item):
|
||||
super().append(item)
|
||||
self._commit()
|
||||
|
||||
def extend(self, iterable):
|
||||
super().extend(iterable)
|
||||
self._commit()
|
||||
|
||||
def insert(self, index, item):
|
||||
super().insert(index, item)
|
||||
self._commit()
|
||||
|
||||
def remove(self, item):
|
||||
super().remove(item)
|
||||
self._commit()
|
||||
|
||||
def pop(self, index=-1):
|
||||
item = super().pop(index)
|
||||
self._commit()
|
||||
return item
|
||||
|
||||
def clear(self):
|
||||
super().clear()
|
||||
self._commit()
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
super().__setitem__(index, value)
|
||||
self._commit()
|
||||
|
||||
def __delitem__(self, index):
|
||||
super().__delitem__(index)
|
||||
self._commit()
|
||||
@@ -0,0 +1,30 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'Comment [Any]'),'2;1');
|
||||
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Trimble Inc. - SketchUp - 2025.0','IFC4 model');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPERSON($,$,'',$,$,$,$,$);
|
||||
#2=IFCORGANIZATION($,'',$,$,$);
|
||||
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
|
||||
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
|
||||
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
|
||||
#6=IFCDIRECTION((1.,0.,0.));
|
||||
#7=IFCDIRECTION((0.,0.,1.));
|
||||
#8=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
|
||||
#10=IFCDIRECTION((0.,1.));
|
||||
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
|
||||
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
|
||||
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
|
||||
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
|
||||
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
|
||||
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1');
|
||||
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Trimble Inc. - SketchUp - 2025.0','IFC4 model');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPERSON($,$,'',$,$,$,$,$);
|
||||
#2=IFCORGANIZATION($,'',$,$,$);
|
||||
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
|
||||
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
|
||||
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
|
||||
#6=IFCDIRECTION((1.,0.,0.));
|
||||
#7=IFCDIRECTION((0.,0.,1.));
|
||||
#8=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
|
||||
#10=IFCDIRECTION((0.,1.));
|
||||
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
|
||||
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
|
||||
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
|
||||
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
|
||||
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
|
||||
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -0,0 +1,30 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'Option [Any]'),'2;1');
|
||||
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Trimble Inc. - SketchUp - 2025.0','IFC4 model');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPERSON($,$,'',$,$,$,$,$);
|
||||
#2=IFCORGANIZATION($,'',$,$,$);
|
||||
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
|
||||
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
|
||||
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
|
||||
#6=IFCDIRECTION((1.,0.,0.));
|
||||
#7=IFCDIRECTION((0.,0.,1.));
|
||||
#8=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
|
||||
#10=IFCDIRECTION((0.,1.));
|
||||
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
|
||||
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
|
||||
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
|
||||
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
|
||||
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
|
||||
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -0,0 +1,30 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ViewDefinition [QuantityTakeOffAddOnView]', 'Option [ExcludedObjects: Stair, Ramp, Space; SplitLevel: On]', 'ExchangeRequirement [CustomRequirement: Value1, Value2]', 'Remark [SomeKey: SomeValue; AnotherKey: AnotherValue]', 'Comment [This is a free text comment, or a comma-separated list of items]'),'2;1');
|
||||
FILE_NAME('Header example2.ifc', '2022-09-16T10:35:07', ('Evandro Alfieri'), ('buildingSMART Int.'), 'IFC Motor 1.0', 'Company - Application - 26.0.0.0', 'none');
|
||||
FILE_SCHEMA(('IFC4X3_ADD2'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPERSON($,$,'',$,$,$,$,$);
|
||||
#2=IFCORGANIZATION($,'',$,$,$);
|
||||
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
|
||||
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
|
||||
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
|
||||
#6=IFCDIRECTION((1.,0.,0.));
|
||||
#7=IFCDIRECTION((0.,0.,1.));
|
||||
#8=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
|
||||
#10=IFCDIRECTION((0.,1.));
|
||||
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
|
||||
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
|
||||
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
|
||||
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
|
||||
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
|
||||
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -0,0 +1,30 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [Alignment-basedView]'),'2;1');
|
||||
FILE_NAME('Header example2.ifc', '2022-09-16T10:35:07', ('Evandro Alfieri'), ('buildingSMART Int.'), 'IFC Motor 1.0', 'Company - Application - 26.0.0.0', 'none');
|
||||
FILE_SCHEMA(('IFC4X3_ADD2'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPERSON($,$,'',$,$,$,$,$);
|
||||
#2=IFCORGANIZATION($,'',$,$,$);
|
||||
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
|
||||
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
|
||||
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
|
||||
#6=IFCDIRECTION((1.,0.,0.));
|
||||
#7=IFCDIRECTION((0.,0.,1.));
|
||||
#8=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
|
||||
#10=IFCDIRECTION((0.,1.));
|
||||
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
|
||||
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
|
||||
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
|
||||
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
|
||||
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
|
||||
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -0,0 +1,30 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [CoordinationView_V2.0]', 'ViewDefinition[SpaceBoundaryAddonView]'),'2;1');
|
||||
FILE_NAME('Header example2.ifc', '2022-09-16T10:35:07', ('Evandro Alfieri'), ('buildingSMART Int.'), 'IFC Motor 1.0', 'Company - Application - 26.0.0.0', 'none');
|
||||
FILE_SCHEMA(('IFC2X3'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPERSON($,$,'',$,$,$,$,$);
|
||||
#2=IFCORGANIZATION($,'',$,$,$);
|
||||
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
|
||||
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
|
||||
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
|
||||
#6=IFCDIRECTION((1.,0.,0.));
|
||||
#7=IFCDIRECTION((0.,0.,1.));
|
||||
#8=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
|
||||
#10=IFCDIRECTION((0.,1.));
|
||||
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
|
||||
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
|
||||
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
|
||||
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
|
||||
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
|
||||
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -0,0 +1,134 @@
|
||||
import os
|
||||
import pytest
|
||||
import ifcopenshell
|
||||
from ifcopenshell.util import mvd_info
|
||||
from ifcopenshell.util.mvd_info import parse_mvd
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def load_fixture():
|
||||
base_dir = os.path.join(os.path.dirname(__file__), "fixtures", "mvd_parsing")
|
||||
def _load(filename):
|
||||
return ifcopenshell.open(os.path.join(base_dir, filename))
|
||||
return _load
|
||||
|
||||
|
||||
class TestViewDefinition:
|
||||
def test_single_view(self, load_fixture):
|
||||
f = load_fixture("passing_header.ifc")
|
||||
assert f.mvd.view_definitions == "Alignment-basedView"
|
||||
|
||||
def test_multiple_views(self, load_fixture):
|
||||
f = load_fixture("two_views.ifc")
|
||||
assert f.mvd.view_definitions == 'CoordinationView_V2.0, SpaceBoundaryAddonView'
|
||||
|
||||
|
||||
class TestExchangeRequirements:
|
||||
def test_parsing(self, load_fixture):
|
||||
f = load_fixture("contains_exchange_requirement.ifc")
|
||||
parsed = parse_mvd(f.mvd.description)
|
||||
assert parsed.exchange_requirements == 'Any'
|
||||
|
||||
def test_access_and_modification(self, load_fixture):
|
||||
f = load_fixture("contains_exchange_requirement.ifc")
|
||||
f.header.file_description.description = (
|
||||
'ViewDefinition [Alignment-basedView]',
|
||||
'ExchangeRequirement [SomethingElse]'
|
||||
)
|
||||
assert f.mvd.exchange_requirements == 'SomethingElse'
|
||||
f.mvd.view_definitions = 'CoordinationView_V2.0'
|
||||
assert f.mvd.view_definitions == 'CoordinationView_V2.0'
|
||||
|
||||
|
||||
class TestComments:
|
||||
def test_read_and_append(self, load_fixture):
|
||||
f = load_fixture("contains_comment.ifc")
|
||||
assert f.mvd.comments == ['Any']
|
||||
f.mvd.comments = ['SomethingElse']
|
||||
assert f.mvd.comments == ['SomethingElse']
|
||||
f.mvd.comments.append('AnotherComment')
|
||||
assert f.mvd.comments == ['SomethingElse', ' AnotherComment']
|
||||
assert f.mvd.description[1] == 'Comment [SomethingElse, AnotherComment]'
|
||||
|
||||
def test_comment_list_modifications(self, load_fixture):
|
||||
f = load_fixture("contains_comment.ifc")
|
||||
f.mvd.comments = ''
|
||||
f.mvd.comments.append('OnlyOne')
|
||||
assert 'OnlyOne' in f.mvd.comments
|
||||
|
||||
f.mvd.comments.insert(0, 'FirstOne')
|
||||
f.mvd.comments[0] == 'FirstOne'
|
||||
|
||||
f.mvd.comments.pop()
|
||||
assert f.mvd.comments[0] == 'FirstOne'
|
||||
|
||||
del f.mvd.comments[0]
|
||||
assert not f.mvd.comments
|
||||
|
||||
|
||||
class TestOptions:
|
||||
def test_string_options(self, load_fixture):
|
||||
f = load_fixture("contains_options.ifc")
|
||||
assert f.mvd.options == 'Any'
|
||||
assert 'options' in f.mvd.keywords
|
||||
|
||||
|
||||
class TestDynamicFields:
|
||||
def test_options_modifications(self, load_fixture):
|
||||
f = load_fixture("dynamic_fields.ifc")
|
||||
|
||||
f.mvd.options['ExcludedObjects'].append('Chair')
|
||||
f.mvd.options['SplitLevel'] = 'Off'
|
||||
f.mvd.options['OtherAttr'] = 'SomeValue'
|
||||
|
||||
assert f.mvd.description[2].startswith('Option [')
|
||||
assert 'OtherAttr: SomeValue' in f.mvd.description[2]
|
||||
assert f.mvd.description == f.header.file_description.description
|
||||
|
||||
def test_remark_editing(self, load_fixture):
|
||||
f = load_fixture("dynamic_fields.ifc")
|
||||
assert f.mvd.remark == {'SomeKey': 'SomeValue', 'AnotherKey': 'AnotherValue'}
|
||||
f.mvd.remark['AnotherKey'] = 'SometingElse'
|
||||
f.mvd.remark['IncludedObjects'] = ['Floor', 'Roof']
|
||||
assert f.mvd.remark['AnotherKey'] == 'SometingElse'
|
||||
assert f.mvd.remark['IncludedObjects'] == ['Floor', 'Roof']
|
||||
assert 'remark' in f.mvd.keywords
|
||||
|
||||
def test_custom_dict_behavior(self, load_fixture):
|
||||
f = load_fixture("dynamic_fields.ifc")
|
||||
|
||||
# delete
|
||||
del f.mvd.options['SplitLevel']
|
||||
assert not f.mvd.options.get('SplitLevel')
|
||||
|
||||
# keys, values, items
|
||||
assert set(f.mvd.remark.keys()) == {'SomeKey', 'AnotherKey'}
|
||||
assert 'SomeValue' in f.mvd.remark.values()
|
||||
assert ('SomeKey', 'SomeValue') in f.mvd.remark.items()
|
||||
|
||||
# containment
|
||||
assert 'SomeKey' in f.mvd.remark
|
||||
assert 'MissingKey' not in f.mvd.remark
|
||||
|
||||
|
||||
class TestKeywords:
|
||||
@pytest.mark.parametrize("filename, expected_keywords", [
|
||||
("contains_comment.ifc", {"view_definitions", "comments"}),
|
||||
("contains_exchange_requirement.ifc", {"view_definitions", "exchange_requirements"}),
|
||||
("contains_options.ifc", {"view_definitions", "options"}),
|
||||
("dynamic_fields.ifc", {"view_definitions", "exchange_requirements", "comments", "remark"})
|
||||
])
|
||||
def test_keywords_present(self, load_fixture, filename, expected_keywords):
|
||||
f = load_fixture(filename)
|
||||
assert f.mvd.keywords == expected_keywords
|
||||
|
||||
|
||||
class TestFallbackBehavior:
|
||||
def test_parse_mvd_fallback(self, monkeypatch):
|
||||
monkeypatch.setattr(mvd_info, "LARK_AVAILABLE", False)
|
||||
mvd = mvd_info.MvdInfo(
|
||||
get_description=lambda: ["ViewDefinition [ShouldNotParse]"],
|
||||
set_description=lambda d: None
|
||||
)
|
||||
assert mvd.view_definitions is None
|
||||
assert mvd.keywords == set()
|
||||
Reference in New Issue
Block a user