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,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import math
import ifcopenshell
import ifcopenshell.util.unit
@@ -16,8 +16,9 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Literal, Union
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from typing import Union, Literal
PrimitiveType = Literal["entity", "string", "float", "integer", "boolean", "enum", "binary"]
ComplexPrimitiveType = Literal["list", "array", "set"]
@@ -16,14 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import os
import json
import os
from typing import Union
import ifcopenshell
import ifcopenshell.util.classification
import ifcopenshell.util.element
import ifcopenshell.util.system
from typing import Union
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -16,9 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.element
from typing import Optional
import ifcopenshell.util.element
def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]:
"""Gets classification references associated with the element
@@ -18,9 +18,10 @@
#
#
import ifcopenshell
from typing import Union
import ifcopenshell
def get_constraints(product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""
@@ -16,10 +16,12 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import lark
import ifcopenshell
from typing import Optional, Union, Literal, Any
from collections.abc import Generator
from typing import Any, Literal, Optional, Union
import lark
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.attribute
import ifcopenshell.util.element
@@ -350,8 +352,7 @@ def get_cost_rate(
class CostValueUnserialiser:
def parse(self, formula: str):
l = lark.Lark(
"""start: formula
l = lark.Lark("""start: formula
formula: operand (operator operand)*
operand: value | category "(" formula ")"
value: NUMBER?
@@ -388,8 +389,7 @@ class CostValueUnserialiser:
NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text
"""
)
""")
start = l.parse(formula)
return self.get_formula(start.children[0])
@@ -17,10 +17,13 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import numpy as np
import ifcopenshell
from typing import Any, Union
from dataclasses import dataclass
from typing import Any, Union
import numpy as np
import ifcopenshell
from ifcopenshell.util.shape_builder import ShapeBuilder
@@ -16,12 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import datetime
import isodate
from re import findall
from typing import Any, Literal, Union, overload
import isodate
from dateutil import parser
from typing import Literal, Union, Any, overload
import ifcopenshell
def timedelta2duration(timedelta):
@@ -16,27 +16,30 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import copy
import json
from pathlib import Path
import copy
from typing import Optional, TypedDict, Union
from typing_extensions import NotRequired
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.attribute
import ifcopenshell.util.schema
from typing import Optional, Literal, Any, Union, TypedDict
from typing_extensions import NotRequired
try:
import glob
import warnings
import requests
import urllib.parse
from markdown import markdown
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
import zipfile
from lxml import etree
import re
import shutil
import urllib.parse
import warnings
import zipfile
import requests
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
from lxml import etree
from markdown import markdown
except:
pass # Only necessary if you're using it to generate the docs database
@@ -16,14 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from collections import namedtuple
from collections.abc import Callable, Generator, Sequence
from typing import Any, Literal, Optional, Union, overload
import ifcopenshell
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.representation
from typing import Any, Callable, Optional, Union, Literal, overload
from collections.abc import Generator, Sequence
from collections import deque, namedtuple
MATERIAL_TYPE = Literal[
"IfcMaterial",
@@ -469,7 +469,7 @@ def get_properties(
del data["HasProperties"]
results[prop_name] = data
if verbose:
results[prop_name] = {"id": data["id"], "class": data["class"], "value": results[prop_name]}
results[prop_name] = {"id": data["id"], "class": data["type"], "value": results[prop_name]}
return results
@@ -1234,7 +1234,9 @@ def get_controls(element: ifcopenshell.entity_instance) -> Generator[ifcopenshel
yield rel.RelatingControl
def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
def get_parent(
element: ifcopenshell.entity_instance, ifc_class: Optional[str] = None
) -> Union[ifcopenshell.entity_instance, None]:
"""Get the parent in the spatial heirarchy
IFC features a spatial hierarchy tree of all objects. Each spatial element
@@ -1251,6 +1253,8 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti
- Voiding: the opening voids another physical element, such as a hole in a wall
:param element: Any physical or spatial element in the tree
:param ifc_class: Optionally filter the type of parent you're after. For
example, you may be after the storey, not a space.
:return: Its parent. This must exist for any valid file, or None if we've reached the IfcProject.
Example:
@@ -1260,7 +1264,7 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti
element = file.by_type("IfcWall")[0]
parent = ifcopenshell.util.element.get_parent(element)
"""
return (
parent = (
get_container(element, should_get_direct=True)
or get_aggregate(element)
or get_nest(element)
@@ -1268,6 +1272,16 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti
or get_voided_element(element)
)
if not ifc_class:
return parent
while parent:
if parent.is_a(ifc_class):
return parent
parent = get_parent(parent)
return None
def get_filled_void(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""If the element is filling a void, get the void
@@ -17,7 +17,8 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import zipfile
from typing import IO, Union, TypedDict
from typing import IO, TypedDict, Union
from typing_extensions import NotRequired
@@ -16,11 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Literal
from typing_extensions import assert_never
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.attribute
from typing import Literal
from typing_extensions import assert_never
# COBie actually uses an exclusion list, but this inclusion list is equivalent.
cobie_type_classes = [
@@ -18,22 +18,22 @@
RUN_FROM_DEV_REPO = False
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.api.unit
import ifcopenshell.api.project
import ifcopenshell.guid
import ifcopenshell.util.attribute
import glob
import sys
from pathlib import Path
from lxml import etree
from itertools import chain
from pathlib import Path
from typing import cast
from lxml import etree
import ifcopenshell.api.project
import ifcopenshell.api.unit
import ifcopenshell.guid
import ifcopenshell.ifcopenshell_wrapper as W
if not RUN_FROM_DEV_REPO:
import zipfile
import shutil
import zipfile
BASE_MODULE_PATH = Path(__file__).parent
@@ -17,12 +17,14 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import math
from decimal import ROUND_HALF_UP, Decimal
from typing import Any, NamedTuple, Optional, Union
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.placement
from typing import NamedTuple, Optional, Union
from decimal import Decimal, ROUND_HALF_UP
MatrixType = ifcopenshell.util.placement.MatrixType
@@ -266,6 +268,15 @@ def get_helmert_transformation_parameters(ifc_file: ifcopenshell.file) -> Option
return HelmertTransformation(e, n, h, xaa, xao, scale, factor_x, factor_y, factor_z)
def get_crs(ifc_file: ifcopenshell.file) -> dict[str, Any]:
"""Get CRS information from an IFC file."""
if ifc_file.schema == "IFC2X3":
return ifcopenshell.util.element.get_pset(ifc_file.by_type("IfcProject")[0], "ePSet_ProjectedCRS")
for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if operation := context.HasCoordinateOperation:
return operation[0].TargetCRS.get_info()
def auto_z2e(ifc_file: ifcopenshell.file, z: float, should_return_in_map_units: bool = True) -> float:
"""Convert a Z coordinate to an elevation using model georeferencing data
@@ -17,7 +17,12 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
try:
from server import get_resource_path, resource_documentation_builder, process_markdown, R
from server import (
R,
get_resource_path,
process_markdown,
resource_documentation_builder,
)
except ModuleNotFoundError as e:
print(
"ERROR. Failed to import `server.py`.\n"
@@ -26,13 +31,15 @@ except ModuleNotFoundError as e:
raise e
import itertools
import operator
import json
import ifcopenshell
import operator
from collections import Counter
from bs4 import BeautifulSoup
from typing import Any, Union
from bs4 import BeautifulSoup
import ifcopenshell
# Hacky modified functions from server.py to make parser work
def get_definition_from_md(resource: str, mdc: str) -> str:
@@ -26,8 +26,8 @@ try:
except ImportError:
LARK_AVAILABLE = False
from typing import Callable, Union
import re
from typing import Union
if LARK_AVAILABLE:
mvd_grammar = r"""
@@ -51,9 +51,9 @@ if LARK_AVAILABLE:
value: /[A-Za-z0-9 _\.-]+/
other_keyword: /[^\[\]]+/
dynamic_option_word: /[^\[\]]+/
other_keyword: /[^\[\]]+/
dynamic_option_word: /[^\[\]]+/
%import common.WS
%ignore WS
@@ -16,11 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from collections.abc import Iterable
from typing import Literal, Optional
import numpy as np
import numpy.typing as npt
import ifcopenshell
from typing import Literal, Optional
from collections.abc import Iterable
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
@@ -16,15 +16,16 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import re
import pathlib
import re
from functools import lru_cache
from typing import Literal, NamedTuple, Optional, Union
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.schema
import ifcopenshell.util.type
from ifcopenshell import entity_instance
from functools import lru_cache
from typing import Optional, Literal, NamedTuple, Union
from ifcopenshell.entity_instance import entity_instance
templates: dict[ifcopenshell.util.schema.IFC_SCHEMA, "PsetQto"] = {}
@@ -16,15 +16,16 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from collections.abc import Generator, Sequence
from typing import Literal, Optional, TypedDict, Union
import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.util.representation
import ifcopenshell.util.placement
import ifcopenshell.util.shape
from typing import Optional, Union, TypedDict, Literal
from collections.abc import Generator, Sequence
import ifcopenshell
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
CONTEXT_TYPE = Literal["Model", "Plan", "NotDefined"]
REPRESENTATION_IDENTIFIER = Literal[
@@ -16,11 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.cost
import ifcopenshell.util.element
import ifcopenshell.util.date
from typing import Union, Any
from typing import Any, Union
import ifcopenshell.util.cost
import ifcopenshell.util.date
import ifcopenshell.util.element
PRODUCTIVITY_PSET_DATA = Union[dict[str, Any], None]
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H
@@ -16,13 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import os
import json
import os
import time
from typing import Any, Literal, Union
import ifcopenshell
import ifcopenshell.util.attribute
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from typing import Union, Any, Literal
import ifcopenshell.util.attribute
# This is highly experimental and incomplete, however, it may work for simple datasets.
@@ -5,6 +5,16 @@
"predefined_type": "NOTDEFINED"
}
],
"IfcAirTerminal": [
{
"name": "Commercial Kitchen Hood"
}
],
"IfcAirTerminalType": [
{
"name": "Commercial Kitchen Hood"
}
],
"IfcAirTerminalBox": [
{
"name": "VAV Box"
@@ -51,6 +61,16 @@
"predefined_type": "DISTRIBUTIONBOARD"
}
],
"IfcFireSuppressionTerminal": [
{
"name": "Fire Extinguisher"
}
],
"IfcFireSuppressionTerminalType": [
{
"name": "Fire Extinguisher"
}
],
"IfcFurniture": [
{
"name": "Casework"
@@ -25,11 +25,11 @@ Things we do check:
- class hierarchy
"""
import ast
import difflib
from pathlib import Path
from typing import Union
from typing_extensions import assert_never
@@ -57,11 +57,28 @@ def get_function_node_name(node: ast.FunctionDef) -> Union[SubnameType, None]:
:return: Function node name as ``SubnameType`` or ``None``, if function wasn't processed and can be skipped.
"""
node_name = node.name
if node_name.startswith("_") and node_name not in ("_is",):
is_init = node_name == "__init__"
if node_name.startswith("_") and node_name not in ("_is",) and not is_init:
return None
arg_nodes = node.args.args
defaults = [None] * (len(arg_nodes) - len(node.args.defaults)) + node.args.defaults
args: list[str] = []
for arg, default in zip(arg_nodes, defaults):
if default is None:
args.append(arg.arg)
else:
args.append(f"{arg.arg}={ast.unparse(default)}")
if arg := node.args.vararg:
args.append(f"*{arg.arg}")
if arg := node.args.kwarg:
args.append(f"**{arg.arg}")
# Skip non-informative constructors.
if is_init and args == ["self"]:
return None
args = [a.arg for a in node.args.args]
if node.args.vararg:
args.append("*args")
node_name = f"def {node.name}"
node_name = f"{node_name}({', '.join(args)}): ..."
@@ -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"):
@@ -17,13 +17,13 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import datetime
from collections.abc import Generator
from functools import cache
from math import floor
from typing import Literal, Optional, Union
import ifcopenshell.util.date
import ifcopenshell.util.element
from math import floor
from functools import cache
from typing import Union, Literal, Optional
from collections.abc import Generator
DURATION_TYPE = Literal["ELAPSEDTIME", "WORKTIME", "NOTDEFINED"]
RECURRENCE_TYPE = Literal[
@@ -16,26 +16,36 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import shapely
import shapely.ops
from __future__ import annotations
from math import cos, radians
from typing import TYPE_CHECKING, Literal, Optional, Union
import numpy as np
import numpy.typing as npt
import ifcopenshell.ifcopenshell_wrapper as W
import shapely
import shapely.ops
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
from ifcopenshell.util.shape_builder import VectorType
from math import radians, cos
from ifcopenshell.geom import ShapeElementType
from typing import Optional, Literal, Union
tol = 1e-6
AXIS_LITERAL = Literal["X", "Y", "Z"]
VECTOR_3D = tuple[float, float, float]
if TYPE_CHECKING:
import ifcopenshell.ifcopenshell_wrapper as W
from ifcopenshell.geom import ShapeElementType
from ifcopenshell.util.shape_builder import VectorType
AXIS_LITERAL = Literal["X", "Y", "Z"]
VECTOR_3D = tuple[float, float, float]
# Used only for typing, but reused by `shape.py` users.
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
tol = 1e-6
# NOTE: See IfcGeomRepresentation.h for W.Triangulation buffer types.
# NOTE: For functions that return a single scalar ensure to use .item() to
@@ -17,20 +17,21 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import collections.abc
from collections.abc import Sequence
from itertools import chain
from math import atan, cos, degrees, pi, radians, sin, sqrt, tan
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import numpy as np
import numpy.typing as npt
import collections
import collections.abc
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
from math import cos, sin, pi, tan, radians, degrees, atan, sqrt
from typing import Union, Optional, Literal, Any, TYPE_CHECKING
from collections.abc import Sequence
from itertools import chain
PRECISION = 1.0e-5
@@ -39,7 +40,7 @@ if TYPE_CHECKING:
# NOTE: mathutils is never used at runtime in ifcopenshell,
# only for type checking to ensure methods are compatible with
# Blender vectors.
from mathutils import Vector
from mathutils import Vector # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
# Support both numpy arrays and python sequences as inputs.
VectorType = Union[Sequence[float], Vector, np.ndarray]
@@ -312,7 +313,7 @@ class ShapeBuilder:
Generate an IfcIndexedPolyCurve based on the provided points.
:param points: List of 2d or 3d points
:param closed: Whether polyline should be closed. Default is `False`
:param closed: Whether polyline should be closed.
:param position_offset: offset to be applied to all points
:param arc_points: Indices of the middle points for arcs. For creating an arc segment,
provide 3 points: `arc_start`, `arc_middle` and `arc_end` to `points` and add the `arc_middle`
@@ -415,8 +416,9 @@ class ShapeBuilder:
3 2
0 1
:param size: rectangle size, could be either 2d or 3d, defaults to `(1,1)`
:param position: rectangle position, default to `None`.
:param size: rectangle size, could be either 2d or 3d.
Use 0 for one of 3d dimensions to create 2d rectangle in 3d space.
:param position: rectangle position.
if `position` not specified zero-vector will be used
:return: list of rectangle coords
"""
@@ -441,9 +443,11 @@ class ShapeBuilder:
"""
Generate a rectangle polyline.
:param size: rectangle size, could be either 2d or 3d, defaults to `(1,1)`
:param position: rectangle position, default to `None`.
if `position` not specified zero-vector will be used
:param size: rectangle.
:param position: rectangle position.
See ``get_rectangle_coords`` for more information.
:return: IfcIndexedPolyCurve
"""
return self.polyline(self.get_rectangle_coords(size, position), closed=True)
@@ -513,11 +517,18 @@ class ShapeBuilder:
trim_points_mask: Sequence[int],
position_offset: Optional[VectorType] = None,
) -> np.ndarray:
"""Handy way to get edge points of the ellipse like shape of a given radiuses.
"""Get cardinal-point coordinates of an ellipse by index mask.
Mask points are numerated from 0 to 3 ccw starting from (x_axis_radius/2; 0).
The four cardinal points are numbered 03 counter-clockwise starting from the
positive X axis: 0 ``(x, 0)``, 1 ``(0, y)``, 2 ``(-x, 0)``, 3 ``(0, -y)``.
Example: mask (0, 1, 2, 3) will return points (x, 0), (0, y), (-x, 0), (0, -y)
Example: mask ``(0, 1, 2, 3)`` returns all four points in order.
:param x_axis_radius: Radius (semi-axis length) along the X axis.
:param y_axis_radius: Radius (semi-axis length) along the Y axis.
:param trim_points_mask: Sequence of cardinal-point indices (03) to select.
:param position_offset: Optional 2D offset added to all returned points.
:return: Numpy array of the selected 2D points.
"""
points = np.array(
(
@@ -542,15 +553,23 @@ class ShapeBuilder:
ref_x_direction: VectorType = (1.0, 0.0),
trim_points_mask: Sequence[int] = (),
) -> ifcopenshell.entity_instance:
"""
Ellipse trimming points should be specified in counter clockwise order.
"""Create an IfcEllipse, optionally trimmed to an arc.
For example, if you need to get the part of the ellipse ABOVE y-axis, you need to use mask (0,2). Below y-axis - (2,0)
If neither ``trim_points`` nor ``trim_points_mask`` is provided, a full IfcEllipse is returned.
Trimming points must be given in counter-clockwise order. For example, to get the arc
above the Y-axis use mask ``(0, 2)``; below the Y-axis use ``(2, 0)``.
For more information about trim_points_mask check builder.get_trim_points_from_mask
A trimmed result (IfcTrimmedCurve) includes a closing segment between the trim points,
making it suitable for use as a profile in :meth:`extrude`.
Notion: trimmed ellipse also contains polyline between trim points, meaning IfcTrimmedCurve could be used
for further extrusion.
:param x_axis_radius: Semi-axis length along the local X axis.
:param y_axis_radius: Semi-axis length along the local Y axis.
:param position: 2D centre of the ellipse.
:param trim_points: Explicit pair of 2D trim points. Takes precedence over ``trim_points_mask``.
:param ref_x_direction: Direction of the local X axis.
:param trim_points_mask: Pair of cardinal-point indices (03) used when ``trim_points`` is empty.
See :meth:`get_trim_points_from_mask` for index definitions.
:return: IfcEllipse (untrimmed) or IfcTrimmedCurve (trimmed).
"""
ifc_position = self.create_axis2_placement_2d(position, ref_x_direction)
ifc_ellipse = self.file.createIfcEllipse(
@@ -681,6 +700,14 @@ class ShapeBuilder:
pivot_point: VectorType = (0.0, 0.0),
counter_clockwise: bool = False,
) -> np.ndarray:
"""Rotate a single 2D point around a pivot.
:param point_2d: The 2D point to rotate.
:param angle: Rotation angle, in degrees. Defaults to 90.
:param pivot_point: The point to rotate around.
:param counter_clockwise: If True, rotate counter-clockwise. Defaults to clockwise.
:return: Rotated 2D point as a numpy array.
"""
angle_rad = radians(angle) * (1 if counter_clockwise else -1)
relative_point = np.array(point_2d) - pivot_point
relative_point = np_rotation_matrix(angle_rad, 2) @ relative_point
@@ -748,7 +775,16 @@ class ShapeBuilder:
mirror_axes: VectorType = (1.0, 1.0),
mirror_point: VectorType = (0.0, 0.0),
) -> np.ndarray:
"""mirror_axes - along which axes mirror will be applied"""
"""Mirror a single 2D point across the specified axes.
:param point_2d: The 2D point to mirror.
:param mirror_axes: Indicates which axes to mirror across. A positive value in a
component means that axis is mirrored (negated relative to ``mirror_point``).
Example: ``(1, 0)`` mirrors across the Y-axis (negates X only),
``(1, 1)`` mirrors across both axes.
:param mirror_point: Origin of the mirror operation.
:return: Mirrored 2D point as a numpy array.
"""
mirror_axes: np.ndarray = np.where(np.array(mirror_axes) > 0, -1, 1)
mirror_point: np.ndarray = np.array(mirror_point)
relative_point = point_2d - mirror_point
@@ -784,7 +820,7 @@ class ShapeBuilder:
"""
Create IfcAxis2Placement3D from numpy matrix.
:param matrix: 4x4 transformation matrix, defaults to `np.eye(4)`
:param matrix: 4x4 transformation matrix, defaults to ``np.eye(4)``
:return: IfcAxis2Placement3D
"""
if matrix is None:
@@ -794,7 +830,13 @@ class ShapeBuilder:
def create_axis2_placement_2d(
self, position: VectorType = (0.0, 0.0), x_direction: Optional[VectorType] = None
) -> ifcopenshell.entity_instance:
"""Create IfcAxis2Placement2D."""
"""Create IfcAxis2Placement2D.
:param position: 2D origin of the placement.
:param x_direction: Direction of the local X axis. If not provided, defaults to
the global X axis ``(1, 0)``.
:return: IfcAxis2Placement2D
"""
ref_direction = (
self.file.create_entity("IfcDirection", ifc_safe_vector_type(x_direction)) if x_direction else None
)
@@ -965,8 +1007,8 @@ class ShapeBuilder:
def sphere(self, radius: float = 1.0, center: VectorType = (0.0, 0.0, 0.0)) -> ifcopenshell.entity_instance:
"""
:param radius: radius of the sphere, defaults to 1.0
:param center: sphere position, defaults to `(0.0, 0.0, 0.0)`
:param radius: radius of the sphere.
:param center: sphere position.
:return: IfcSphere
"""
@@ -996,7 +1038,7 @@ class ShapeBuilder:
) -> ifcopenshell.entity_instance:
"""
:param plane: The IfcPlane representing the half space.
:param agreement_flag: False if +Z represents the void
:param agreement_flag: If False (default), the plane normal points toward the **removed** material (the void). The kept region is on the opposite side from the normal.
:return: IfcHalfSpaceSolid
"""
return self.file.createIfcHalfSpaceSolid(plane, AgreementFlag=agreement_flag)
@@ -1049,7 +1091,14 @@ class ShapeBuilder:
def create_swept_disk_solid(
self, path_curve: ifcopenshell.entity_instance, radius: float
) -> ifcopenshell.entity_instance:
"""Create IfcSweptDiskSolid from `path_curve` (must be 3D) and `radius`"""
"""Create an IfcSweptDiskSolid — a circular cross-section swept along a 3D path.
Useful for modelling round pipes, conduits, and cables.
:param path_curve: A 3D curve entity defining the centreline path. Must have ``Dim == 3``.
:param radius: Radius of the circular disk cross-section.
:return: IfcSweptDiskSolid
"""
if path_curve.Dim != 3:
raise Exception(
f"Path curve for IfcSweptDiskSolid should be 3D to be valid, currently it has {path_curve.Dim} dimensions.\n"
@@ -1067,10 +1116,22 @@ class ShapeBuilder:
) -> ifcopenshell.entity_instance:
"""Create IFC representation for the specified context and items.
**All items must belong to the same geometry category.** IFC prohibits
mixing incompatible item types in one representation (e.g.
``IfcExtrudedAreaSolid`` with ``IfcBlock``, or solids with curves).
When ``representation_type`` is omitted the type is inferred via
:func:`ifcopenshell.util.representation.guess_type`; if the items are
heterogeneous ``guess_type`` returns ``None`` and the representation is
written with no ``RepresentationType``, which fails IFC validation.
Avoid mixing swept-solid primitives (``IfcExtrudedAreaSolid``,
``IfcRevolvedAreaSolid``) with CSG primitives (``IfcBlock``,
``IfcSphere``, etc.) or any other category in a single call.
:param context: IfcGeometricRepresentationSubContext
:param items: could be a list or single curve/IfcExtrudedAreaSolid
:param representation_type: Explicitly specified RepresentationType, defaults to `None`.
If not provided it will be guessed from the items types
:param items: A single item or list of items, all of the same geometry
category (e.g. all ``IfcExtrudedAreaSolid``, all ``IfcIndexedPolyCurve``)
:param representation_type: Explicitly specified RepresentationType.
If not provided it will be guessed from the items types.
:return: IfcShapeRepresentation
"""
if not isinstance(items, collections.abc.Iterable):
@@ -1092,18 +1153,26 @@ class ShapeBuilder:
)
def deep_copy(self, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Create a deep copy of an IFC element and all its referenced entities.
:param element: The IFC entity to copy.
:return: A new independent copy of the element.
"""
return ifcopenshell.util.element.copy_deep(self.file, element)
# UTILITIES
def extrude_kwargs(self, axis: Literal["Y", "X", "Z"]) -> dict[str, tuple[float, float, float]]:
"""Shortcut to get kwargs for `ShapeBuilder.extrude` to extrude by some axis.
"""Shortcut to get kwargs for :meth:`extrude` to extrude along a principal axis.
It assumes you have 2D profile in:
XZ plane for Y axis extrusion, \n
YZ plane for X axis extrusion, \n
XY plane for Z axis extrusion, \n
Assumes the 2D profile lies in the plane perpendicular to the extrusion axis:
XZ plane for Y-axis extrusion, YZ plane for X-axis extrusion, XY plane for Z-axis extrusion.
Extruding by X/Y using other kwargs might break ValidExtrusionDirection."""
Extruding along X or Y with other kwargs may violate the IFC ValidExtrusionDirection constraint.
:param axis: The extrusion axis: ``'X'``, ``'Y'``, or ``'Z'``.
:return: A dict with keys ``position_x_axis``, ``position_z_axis``, and ``extrusion_vector``
suitable for passing as ``**kwargs`` to :meth:`extrude`.
"""
if axis == "Y":
return {
@@ -1127,13 +1196,16 @@ class ShapeBuilder:
def rotate_extrusion_kwargs_by_z(
self, kwargs: dict[str, Any], angle: float, counter_clockwise: bool = False
) -> dict[str, VectorType]:
"""shortcut to rotate extrusion kwargs by z axis
"""Rotate extrusion kwargs around the Z axis.
`kwargs` expected to have `position_x_axis` and `position_z_axis` keys
A shortcut to rotate the ``position_x_axis`` and ``position_z_axis`` values returned by
:meth:`extrude_kwargs` around the Z axis before passing them to :meth:`extrude`.
`angle` is a rotation value in radians
by default rotation is clockwise, to make it counter clockwise use `counter_clockwise` flag
:param kwargs: A dict with ``position_x_axis`` and ``position_z_axis`` keys,
as returned by :meth:`extrude_kwargs`. The original dict is not mutated.
:param angle: Rotation angle, in radians.
:param counter_clockwise: If True, rotate counter-clockwise. Defaults to clockwise.
:return: A new dict with ``position_x_axis`` and ``position_z_axis`` rotated around Z.
"""
rot = np_rotation_matrix(-angle, 3, "Z")
kwargs = kwargs.copy() # prevent mutation of original kwargs
@@ -1142,7 +1214,11 @@ class ShapeBuilder:
return kwargs
def get_polyline_coords(self, polyline: ifcopenshell.entity_instance) -> np.ndarray:
"""polyline should be either `IfcIndexedPolyCurve` or `IfcPolyline`"""
"""Extract the coordinate array from a polyline entity.
:param polyline: An ``IfcIndexedPolyCurve`` or ``IfcPolyline`` entity.
:return: Numpy array of the polyline's point coordinates.
"""
coords = None
if polyline.is_a("IfcIndexedPolyCurve"):
coords = np.array(polyline.Points.CoordList)
@@ -1153,7 +1229,12 @@ class ShapeBuilder:
return coords
def set_polyline_coords(self, polyline: ifcopenshell.entity_instance, coords: SequenceOfVectors) -> None:
"""polyline should be either `IfcIndexedPolyCurve` or `IfcPolyline`"""
"""Update the coordinates of a polyline entity in-place.
:param polyline: An ``IfcIndexedPolyCurve`` or ``IfcPolyline`` entity.
:param coords: New sequence of point coordinates. Must contain the same number of
points as the original polyline.
"""
if polyline.is_a("IfcIndexedPolyCurve"):
polyline.Points.CoordList = ifc_safe_vector_type(coords)
elif polyline.is_a("IfcPolyline"):
@@ -1180,8 +1261,8 @@ class ShapeBuilder:
:param fillets: list of points from `coords` to base fillet on. Example: (1,)
:param fillet_radius: list of fillet radius for each of corresponding point form `fillets`.
Example: (5.,) Note: `fillet_radius` could be just 1 float value if it's the same for all fillets.
:param closed: boolean whether curve should be closed (whether last point connected to first one). Default: True
:param create_ifc_curve: create IfcIndexedPolyCurve or just return the data. Default: False
:param closed: boolean whether curve should be closed (whether last point connected to first one).
:param create_ifc_curve: create IfcIndexedPolyCurve or just return the data.
:return: (points, segments, ifc_curve) for the created simple curve
if both points in e are equally far from pt, then v1 is returned.
@@ -1292,6 +1373,18 @@ class ShapeBuilder:
WallThickness: float,
FilletRadius: float,
) -> ifcopenshell.entity_instance:
"""Create a Z-profile (cold-formed steel section) outline curve with lips and fillets.
All dimensions are in the IFC project's length units.
:param FirstFlangeWidth: Width of the first (top) flange, measured from the web centreline.
:param SecondFlangeWidth: Width of the second (bottom) flange, measured from the web centreline.
:param Depth: Total depth of the section (web height).
:param Girth: Length of the return lips on each flange.
:param WallThickness: Uniform material thickness.
:param FilletRadius: Inner bend radius at each corner.
:return: IfcIndexedPolyCurve representing the closed Z-profile outline.
"""
x1 = FirstFlangeWidth
x2 = SecondFlangeWidth
y = Depth / 2
@@ -1333,10 +1426,17 @@ class ShapeBuilder:
def create_transition_arc_ifc(
self, width: float, height: float, create_ifc_curve: bool = False
) -> tuple[SequenceOfVectors, list[list[int]], Union[ifcopenshell.entity_instance, None]]:
"""Create an arc in the rectangle with specified width and height.
"""Create an arc fitting inside a rectangle of the given width and height.
If it's not possible to make a complete arc, create an arc with longest radius possible
and straight segment in the middle.
If a single arc cannot span the full width, the longest possible radius is used and
a straight segment is inserted in the middle.
:param width: Width of the bounding rectangle.
:param height: Height of the bounding rectangle (also the maximum arc radius).
:param create_ifc_curve: If True, also create and return an ``IfcIndexedPolyCurve``.
If False, only return the raw point and segment data.
:return: A tuple ``(points, segments, ifc_curve)`` where ``ifc_curve`` is an
``IfcIndexedPolyCurve`` when ``create_ifc_curve=True``, otherwise ``None``.
"""
fillet_size = (width / 2) / height
if fillet_size <= 1:
@@ -1366,6 +1466,14 @@ class ShapeBuilder:
return points, segments, transition_arc
def mesh(self, points: SequenceOfVectors, faces: Sequence[Sequence[int]]) -> ifcopenshell.entity_instance:
"""Create a tessellated mesh from points and face indices.
Delegates to :meth:`faceted_brep` for IFC2X3, or :meth:`polygonal_face_set` for IFC4 and later.
:param points: List of 3D coordinates.
:param faces: List of faces, each face a sequence of zero-based point indices.
:return: IfcFacetedBrep (IFC2X3) or IfcPolygonalFaceSet (IFC4+).
"""
if self.file.schema == "IFC2X3":
return self.faceted_brep(points, faces)
return self.polygonal_face_set(points, faces)
@@ -1460,10 +1568,10 @@ class ShapeBuilder:
:param points: list of points, assuming they form consecutive closed polyline.
:param magnitude: extrusion magnitude
:param extrusion_vector: extrusion direction, by default it's extruding by Z+ axis
:param extrusion_vector: extrusion direction.
:param offset: offset from the points
:param start_cap: if True, create start cap, by default it's True
:param end_cap: if True, create end cap, by default it's True
:param start_cap: if True, create start cap.
:param end_cap: if True, create end cap.
:return: IfcPolygonalFaceSet
"""
@@ -1719,11 +1827,20 @@ class ShapeBuilder:
angle: float,
profile_offset: VectorType = (0.0, 0.0),
verbose: bool = True,
):
"""get the final transition length for two profiles dimensions, angle and XY offset between them,
) -> Optional[float]:
"""Get the transition length for two profile half-dimensions, an angle, and an XY offset.
the difference from `calculate_transition` - `get_transition_length` is making sure
that length will fit both sides of the transition
Unlike :meth:`mep_transition_calculate`, this method checks that the resulting length
satisfies the angle constraint from both the start and end profile perspectives.
:param start_half_dim: Half-dimensions of the start profile as a 3-element array
``[half_x, half_y, depth]``. For circular profiles ``half_x == half_y == radius``.
:param end_half_dim: Half-dimensions of the end profile in the same format.
:param angle: Maximum allowed transition angle, in degrees.
:param profile_offset: 2D XY offset between the centrelines of the start and end profiles.
:param verbose: If True, print diagnostic values during calculation.
:return: Transition length in project length units, or ``None`` if no valid length exists
for the given angle and offset.
"""
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None
np_X, np_Y = 0, 1
@@ -1784,9 +1901,23 @@ class ShapeBuilder:
angle: Optional[float] = None,
verbose: bool = True,
) -> Union[float, None]:
"""will return transition length based on the profile dimension differences and offset.
"""Calculate MEP transition length from angle, or transition angle from length.
If `length` is provided will return transition angle"""
Low-level calculation kernel used by :meth:`mep_transition_length`. Provide either
``angle`` or ``length`` (not both); the other value is computed and returned.
:param start_half_dim: Half-dimensions of the start profile ``[half_x, half_y, depth]``.
:param end_half_dim: Half-dimensions of the end profile ``[half_x, half_y, depth]``.
:param offset: 2D XY offset between profile centrelines.
:param diff: Pre-computed absolute difference of start and end half-dimensions (XY only).
Computed from ``start_half_dim`` and ``end_half_dim`` if not provided.
:param end_profile: If True, swap X and Y axes to compute from the end-profile perspective.
:param length: Known transition length. If provided, the corresponding angle is returned.
:param angle: Known transition angle, in degrees. If provided, the corresponding length is returned.
:param verbose: If True, print diagnostic values during calculation.
:return: Transition length (if ``angle`` was given) or transition angle in degrees
(if ``length`` was given), or ``None`` if the geometry is not feasible.
"""
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None
@@ -17,8 +17,9 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Literal, Optional, Union
import ifcopenshell.util.system
from typing import Optional, Union, Literal
group_types: dict[str, tuple[str, ...]] = {
"IfcZone": ("IfcZone", "IfcSpace", "IfcSpatialZone"),
@@ -16,8 +16,9 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import os
import json
import os
import ifcopenshell.util.schema
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -16,14 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from collections.abc import Generator
from fractions import Fraction
from math import pi
from typing import Literal, Optional, Union
from collections.abc import Generator
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.api.unit
prefixes = {
"EXA": 1e18,
@@ -209,6 +208,7 @@ si_conversions = {
"pound": 0.454,
"ton UK": 1016.0469088,
"ton US": 907.18474,
"tonne": 1000.0,
"lbf": 4.4482216153,
"kip": 4448.2216153,
"psi": 6894.7572932,
@@ -253,6 +253,7 @@ imperial_types = {
"pound": "MASSUNIT",
"ton UK": "MASSUNIT",
"ton US": "MASSUNIT",
"tonne": "MASSUNIT",
"lbf": "FORCEUNIT",
"kip": "FORCEUNIT",
"psi": "PRESSUREUNIT",
@@ -323,6 +324,7 @@ unit_symbols = {
"pound": "lb",
"ton UK": "ton",
"ton US": "ton",
"tonne": "t",
"lbf": "lbf",
"kip": "kip",
"psi": "psi",
@@ -866,10 +868,10 @@ def iter_element_and_attributes_per_type(ifc_file: ifcopenshell.file, attr_type_
def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "METER") -> ifcopenshell.file:
"""Converts all units in an IFC file to the specified target units. Returns a new file."""
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.api.georeference
import ifcopenshell.api.unit
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
prefix = get_prefix(target_units)
si_unit = get_unit_name(target_units)