Merge branch 'v0.8.0' into saikei

This commit is contained in:
Dion Moult
2026-02-19 10:22:31 +11:00
committed by GitHub
684 changed files with 32439 additions and 30707 deletions
@@ -28,7 +28,7 @@ def _create_offset_curve_representation(
file: ifcopenshell.file, alignment: entity_instance, offsets: Sequence[entity_instance]
) -> None:
"""
Create geometric representation for the alignment based on an IfcPolyline
Create geometric representation for the alignment based on an IfcOffsetByDistances curve
:param alignment: The alignment for which the representation is being created
:return: None
@@ -36,6 +36,11 @@ def _create_offset_curve_representation(
expected_type = "IfcAlignment"
if not alignment.is_a(expected_type):
raise TypeError(f"Expected {expected_type} but got {alignment.is_a()}")
expected_type = "IfcPointByDistanceExpression"
for offset in offsets:
if not offset.is_a(expected_type):
raise TypeError(f"Expected {expected_type} but got {offset.is_a()}")
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
@@ -39,7 +39,7 @@ def create_as_offset_curve(
:param file:
:param name: name assigned to IfcAlignment.Name
:param offsets: offsets from the basis curve that defines the offset curve, expected to be IfcOffsetCurveByDistances.
:param offsets: offsets from the basis curve that defines the offset curve, expected to be IfcPointByDistanceExpression.
:param start_station: station value at the start of the alignment
:return: Returns an IfcAlignment
"""
@@ -100,15 +100,10 @@ class Usecase:
size = self.convert_si_to_unit(1)
points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0))
if self.polyline:
# Only scale polyline if we have actual slope
if self.x_angle and abs(self.x_angle) > 1e-6:
points = [
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
for p in self.polyline
]
else:
points = [(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1])) for p in self.polyline]
points = [
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
for p in self.polyline
]
if self.file.schema == "IFC2X3":
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
else:
@@ -119,23 +114,21 @@ class Usecase:
else:
direction_ratios = (0.0, 0.0, 1.0)
offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative
extrusion_direction = self.file.createIfcDirection(direction_ratios)
if self.direction_sense == "NEGATIVE":
direction_ratios = tuple(-n for n in direction_ratios)
extrusion_direction = self.file.createIfcDirection(direction_ratios)
# Calculate depth based on extrusion angle
extrusion_angle = abs(self.x_angle) if self.x_angle else 0
if extrusion_angle > 1e-6:
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(extrusion_angle))
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(extrusion_angle))
else:
perpendicular_depth = self.convert_si_to_unit(self.depth)
perpendicular_offset = self.convert_si_to_unit(self.offset)
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(self.x_angle))
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(self.x_angle))
position = None
# default position for IFC2X3 where .Position is not optional
if self.file.schema == "IFC2X3" or self.offset != 0:
position_vector = (
direction_ratios[0] * perpendicular_offset,
direction_ratios[1] * perpendicular_offset,
direction_ratios[2] * perpendicular_offset,
offset_direction[0] * perpendicular_offset,
offset_direction[1] * perpendicular_offset,
offset_direction[2] * perpendicular_offset,
)
position = self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint(position_vector),
@@ -85,6 +85,7 @@ class Usecase:
def create_item(self) -> ifcopenshell.entity_instance:
length = self.convert_si_to_unit(self.settings["length"])
thickness = self.convert_si_to_unit(self.settings["thickness"])
thickness *= 1 / cos(self.settings["x_angle"])
if self.settings["direction_sense"] == "NEGATIVE":
thickness *= -1
points = (
@@ -112,7 +113,7 @@ class Usecase:
self.file.createIfcDirection((1.0, 0.0, 0.0)),
),
extrusion_direction,
self.convert_si_to_unit(self.settings["height"]),
self.convert_si_to_unit(self.settings["height"]) * abs(1 / cos(self.settings["x_angle"])),
)
if self.settings["booleans"]:
extrusion = self.apply_booleans(extrusion)
@@ -381,6 +381,7 @@ class Usecase:
def append_type_product(self):
self.whitelisted_inverse_attributes = {
"IfcObjectDefinition": ["HasAssociations"],
"IfcDistributionElementType": ["IsNestedBy"],
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
"IfcRepresentationItem": ["StyledByItem", "LayerAssignment"],
"IfcRepresentation": ["LayerAssignments"],
@@ -397,6 +398,7 @@ class Usecase:
"IfcObjectDefinition": ["HasAssociations"],
"IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"],
"IfcElement": ["HasOpenings"],
"IfcDistributionElement": ["IsNestedBy"],
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
"IfcRepresentationItem": [
"StyledByItem",
@@ -569,6 +571,8 @@ class Usecase:
return False
elif element.is_a("IfcRoot") and self.by_guid(element.GlobalId) is not None:
return False
elif element.is_a("IfcDistributionPort"):
return False
elif element.is_a(self.target_class):
return True
elif self.target_class == "IfcProduct" and element.is_a("IfcTypeProduct"):
@@ -65,8 +65,6 @@ def disconnect_port(file: ifcopenshell.file, port: ifcopenshell.entity_instance)
rels += port.ConnectedFrom or ()
for rel in rels:
rel.RelatingPort.FlowDirection = None
rel.RelatedPort.FlowDirection = None
history = rel.OwnerHistory
file.remove(rel)
if history:
@@ -750,6 +750,14 @@ class AttributeGetattrTransformer(ast.NodeTransformer):
if node.attr == "create_entity":
return node
if node.attr.startswith("__"):
return node
# Don't rewrite at module scope (top-level, no indent)
enclosing_stmt = next((p for p in parents if isinstance(p, ast.stmt)), None)
if enclosing_stmt is not None and isinstance(getattr(enclosing_stmt, "parent", None), ast.Module):
return node
new_value = self.visit(node.value)
# Replace the Attribute node with a call to the built-in `getattr` function
@@ -842,18 +850,21 @@ if __name__ == "__main__":
print(
"""
def is_indeterminate(v):
return v is None or type(v).__name__ == 'indeterminate_type'
def exists(v):
if callable(v):
try: return v() is not None
except IndexError as e: return False
else: return v is not None
else: return not is_indeterminate(v)
""",
"\n",
file=output,
sep="\n",
)
print(
"def nvl(v, default): return v if v is not None else default",
"def nvl(v, default): return v if not is_indeterminate(v) else default",
"\n",
file=output,
sep="\n",
@@ -871,14 +882,14 @@ def is_entity(inst):
def express_len(v):
if isinstance(v, ifcopenshell.entity_instance) and not is_entity(v):
v = v[0]
elif v is None or v is INDETERMINATE:
elif is_indeterminate(v):
return INDETERMINATE
return len(v)
old_range = range
def range(*args):
if INDETERMINATE in args:
if any(map(is_indeterminate, args)):
return
yield from old_range(*args)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -18,7 +18,7 @@
import math
from decimal import ROUND_HALF_UP, Decimal
from typing import NamedTuple, Optional, Union
from typing import NamedTuple, Optional, Union, Any
import numpy as np
@@ -268,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
@@ -144,7 +144,7 @@ format_grammar = lark.Lark(
| mul_div "*" function -> multiply
| mul_div "/" function -> divide
function: round | number | int | format_length | lower | upper | title | concat | substr | variable | ESCAPED_STRING | NUMBER | "(" expression ")"
function: round | number | int | format_length | lower | upper | title | concat | substr | sort | reverse | join | variable | ESCAPED_STRING | SIGNED_NUMBER | "(" expression ")"
variable: "{{" query_path "}}"
query_path: /[^}]+/
@@ -160,6 +160,9 @@ format_grammar = lark.Lark(
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"
@@ -201,6 +204,8 @@ class FormatTransformer(lark.Transformer):
self.element = element
def start(self, args):
if isinstance(args[0], (list, tuple)):
return ", ".join(args[0])
return args[0]
def expression(self, args):
@@ -208,18 +213,11 @@ class FormatTransformer(lark.Transformer):
def variable(self, args):
"""Handle variable substitution like {{z}} or {{Pset_Wall.FireRating}}"""
if self.element is None:
return "0" # Default value if no element context
query_path = args[0]
try:
value = get_element_value(self.element, query_path)
if value is None:
return "0"
# Convert to string for further processing
return str(value)
except:
return "0" # Return default on error
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"""
@@ -301,6 +299,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