Merge remote-tracking branch 'origin/v0.8.0' into datamodel-v1.0

This commit is contained in:
Thomas Krijnen
2026-04-18 20:15:28 +02:00
1655 changed files with 105534 additions and 46934 deletions
@@ -17,16 +17,20 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import re
import sys
from collections.abc import Iterable
from decimal import Decimal
from types import EllipsisType
from typing import Any, Optional, Union
import lark
import numpy as np
import ifcopenshell.api.pset
import ifcopenshell.api.geometry
import ifcopenshell.api.pset
import ifcopenshell.util
import ifcopenshell.util.attribute
import ifcopenshell.util.classification
import ifcopenshell.util.element
import ifcopenshell.util.fm
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.pset
@@ -34,18 +38,8 @@ import ifcopenshell.util.schema
import ifcopenshell.util.shape
import ifcopenshell.util.system
import ifcopenshell.util.unit
from decimal import Decimal
from typing import Optional, Any, Union
from collections.abc import Iterable
if sys.version_info >= (3, 10):
from types import EllipsisType
else:
EllipsisType = type(...)
filter_elements_grammar = lark.Lark(
"""start: filter_group
filter_elements_grammar = lark.Lark("""start: filter_group
filter_group: facet_list ("+" facet_list)*
facet_list: facet ("," facet)*
@@ -116,11 +110,9 @@ filter_elements_grammar = lark.Lark(
NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text
"""
)
""")
get_element_grammar = lark.Lark(
"""start: keys
get_element_grammar = lark.Lark("""start: keys
keys: key ("." key)*
key: quoted_string | regex_string | unquoted_string
@@ -135,25 +127,37 @@ get_element_grammar = lark.Lark(
WS: /[ \\t\\f\\r\\n]/+
%ignore WS // Disregard spaces in text
"""
)
""")
format_grammar = lark.Lark(
"""start: function
format_grammar = lark.Lark("""start: expression
function: round | number | int | format_length | lower | upper | title | concat | substr | ESCAPED_STRING | NUMBER
?expression: add_sub
?add_sub: mul_div
| add_sub "+" mul_div -> add
| add_sub "-" mul_div -> subtract
?mul_div: function
| mul_div "*" function -> multiply
| mul_div "/" function -> divide
function: round | number | int | format_length | lower | upper | title | concat | substr | sort | reverse | join | variable | ESCAPED_STRING | SIGNED_NUMBER | "(" expression ")"
round: "round(" function "," NUMBER ")"
number: "number(" function ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")"
int: "int(" function ")"
variable: "{{" query_path "}}"
query_path: /[^}]+/
round: "round(" expression "," NUMBER ")"
number: "number(" expression ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")"
int: "int(" expression ")"
format_length: metric_length | imperial_length
metric_length: "metric_length(" function "," NUMBER "," NUMBER ")"
imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
lower: "lower(" function ")"
upper: "upper(" function ")"
title: "title(" function ")"
concat: "concat(" function ("," function)* ")"
substr: "substr(" function "," SIGNED_INT ["," SIGNED_INT] ")"
metric_length: "metric_length(" expression "," NUMBER "," NUMBER ")"
imperial_length: "imperial_length(" expression "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING ["," boolean]] ")"
lower: "lower(" expression ")"
upper: "upper(" expression ")"
title: "title(" expression ")"
concat: "concat(" expression ("," expression)* ")"
substr: "substr(" expression "," SIGNED_INT ["," SIGNED_INT] ")"
sort: "sort(" expression ")"
reverse: "reverse(" expression ")"
join: "join(" ESCAPED_STRING "," expression ")"
boolean: TRUE | FALSE
TRUE: "true" | "True" | "TRUE"
@@ -184,14 +188,82 @@ format_grammar = lark.Lark(
NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text
"""
)
""")
class FormatTransformer(lark.Transformer):
def __init__(self, element=None):
"""Initialize transformer with optional element for variable substitution"""
super().__init__()
self.element = element
def start(self, args):
if isinstance(args[0], (list, tuple)):
return ", ".join(args[0])
return args[0]
def expression(self, args):
return args[0]
def variable(self, args):
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
if self.element:
try:
return get_element_value(self.element, args[0])
except:
pass
def query_path(self, args):
"""Extract the query path from variable"""
return str(args[0]).strip()
def add(self, args):
"""Handle addition operation"""
left, right = args
try:
left_val = float(left) if left != "None" and left is not None else 0.0
right_val = float(right) if right != "None" and right is not None else 0.0
result = left_val + right_val
# Return integer if result has no decimal part
if result % 1 == 0:
return str(int(result))
return str(result)
except (ValueError, TypeError):
# If can't convert to numbers, concatenate as strings
return str(left) + str(right)
def subtract(self, args):
"""Handle subtraction operation"""
left, right = args
left_val = float(left) if left != "None" and left is not None else 0.0
right_val = float(right) if right != "None" and right is not None else 0.0
result = left_val - right_val
if result % 1 == 0:
return str(int(result))
return str(result)
def multiply(self, args):
"""Handle multiplication operation"""
left, right = args
left_val = float(left) if left != "None" and left is not None else 0.0
right_val = float(right) if right != "None" and right is not None else 0.0
result = left_val * right_val
if result % 1 == 0:
return str(int(result))
return str(result)
def divide(self, args):
"""Handle division operation"""
left, right = args
left_val = float(left) if left != "None" and left is not None else 0.0
right_val = float(right) if right != "None" and right is not None else 1.0
if right_val == 0:
return "inf" # or raise an error, or return "0"
result = left_val / right_val
if result % 1 == 0:
return str(int(result))
return str(result)
def function(self, args):
return args[0]
@@ -211,7 +283,7 @@ class FormatTransformer(lark.Transformer):
return str(args[0]).title()
def concat(self, args):
return "".join(args)
return "".join(str(arg) for arg in args)
def substr(self, args):
if len(args) == 3:
@@ -221,6 +293,15 @@ class FormatTransformer(lark.Transformer):
elif len(args) == 2:
return str(args[0])[int(args[1]) :]
def sort(self, args):
return sorted(args[0])
def reverse(self, args):
return list(reversed(args[0]))
def join(self, args):
return args[0].join(args[1])
def boolean(self, args):
if not args:
return True
@@ -241,13 +322,14 @@ class FormatTransformer(lark.Transformer):
return str(result)
def number(self, args):
if isinstance(args[0], str):
args[0] = float(args[0]) if "." in args[0] else int(args[0])
arg_val = args[0]
if isinstance(arg_val, str):
arg_val = float(arg_val) if "." in arg_val else int(arg_val)
if len(args) >= 3 and args[2]:
return "{:,}".format(args[0]).replace(".", "*").replace(",", args[2]).replace("*", args[1])
return "{:,}".format(arg_val).replace(".", "*").replace(",", args[2]).replace("*", args[1])
elif len(args) >= 2 and args[1]:
return "{}".format(args[0]).replace(".", args[1])
return "{:,}".format(args[0])
return "{}".format(arg_val).replace(".", args[1])
return "{:,}".format(arg_val)
def format_length(self, args):
return args[0]
@@ -287,7 +369,8 @@ class FormatTransformer(lark.Transformer):
)
def int(self, args: list[str]) -> str:
return str(int(float(args[0])))
value = 0.0 if args[0] == "None" else args[0] or 0.0
return str(int(float(value)))
class GetElementTransformer(lark.Transformer):
@@ -313,8 +396,18 @@ class GetElementTransformer(lark.Transformer):
return args[1:-1].replace("\\", "")
def format(query: str) -> str:
return FormatTransformer().transform(format_grammar.parse(query))
def format(query: str, element: Optional[ifcopenshell.entity_instance] = None) -> str:
"""Format a query string with optional element context for variable substitution.
:param query: Format query string (can include {{variable}} placeholders)
:param element: Optional IFC element for variable substitution
:return: Formatted string
Example:
format("{{z}} / 2", element) # Substitutes element's z value
format("imperial_length({{z}} / 2, 4)", element) # Uses z in calculation
"""
return FormatTransformer(element).transform(format_grammar.parse(query))
def get_element_value(element: ifcopenshell.entity_instance, query: str) -> Any:
@@ -347,13 +440,13 @@ def _get_element_value(element: ifcopenshell.entity_instance, keys: list[str]) -
elif key == "container":
value = ifcopenshell.util.element.get_container(value)
elif key == "space":
value = ifcopenshell.util.element.get_container(value, ifc_class="IfcSpace")
value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcSpace")
elif key == "storey":
value = ifcopenshell.util.element.get_container(value, ifc_class="IfcBuildingStorey")
value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcBuildingStorey")
elif key == "building":
value = ifcopenshell.util.element.get_container(value, ifc_class="IfcBuilding")
value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcBuilding")
elif key == "site":
value = ifcopenshell.util.element.get_container(value, ifc_class="IfcSite")
value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcSite")
elif key == "parent":
value = ifcopenshell.util.element.get_parent(value)
elif key in ("types", "occurrences"):