diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py
index 9cea2cca2e..e0679a18c2 100644
--- a/src/bonsai/bonsai/bim/handler.py
+++ b/src/bonsai/bonsai/bim/handler.py
@@ -55,10 +55,10 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
return
if isinstance(obj, bpy.types.Material):
- props = obj.BIMStyleProperties
+ props = tool.Style.get_material_style_props(obj)
if ifc_definition_id := props.ifc_definition_id:
if props.is_renaming:
- props.is_renmaing = False
+ props.is_renaming = False
return
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py
index ce84c66f02..281d39df8e 100644
--- a/src/bonsai/bonsai/bim/ifc.py
+++ b/src/bonsai/bonsai/bim/ifc.py
@@ -265,7 +265,8 @@ class IfcStore:
IfcStore.guid_map[global_id] = obj
if element.is_a("IfcSurfaceStyle"):
- obj.BIMStyleProperties.ifc_definition_id = element.id()
+ props = tool.Style.get_material_style_props(obj)
+ props.ifc_definition_id = element.id()
else:
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = element.id()
@@ -406,7 +407,8 @@ class IfcStore:
@staticmethod
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
if isinstance(obj, bpy.types.Material):
- obj.BIMStyleProperties.ifc_definition_id = 0
+ props = tool.Style.get_material_style_props(obj)
+ props.ifc_definition_id = 0
else: # bpy.types.Object
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py
index 0ed6f3d9e7..ade2a99143 100644
--- a/src/bonsai/bonsai/bim/import_ifc.py
+++ b/src/bonsai/bonsai/bim/import_ifc.py
@@ -86,7 +86,7 @@ class MaterialCreator:
def load_existing_materials(self) -> None:
for material in bpy.data.materials:
- if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
+ if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
self.styles[ifc_definition_id] = material
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
@@ -964,10 +964,11 @@ class IfcImporter:
self.material_creator.styles[style.id()] = blender_material
style_elements = tool.Style.get_style_elements(blender_material)
+ props = tool.Style.get_material_style_props(blender_material)
if tool.Style.has_blender_external_style(style_elements):
- blender_material.BIMStyleProperties.active_style_type = "External"
+ props.active_style_type = "External"
else:
- blender_material.BIMStyleProperties.active_style_type = "Shading"
+ props.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py
index 2b75406d5c..42e17cf063 100644
--- a/src/bonsai/bonsai/bim/module/drawing/operator.py
+++ b/src/bonsai/bonsai/bim/module/drawing/operator.py
@@ -3353,7 +3353,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
obj.material_slots[0].material = material
bpy.ops.bim.add_style()
- style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id)
+ style = tool.Ifc.get_entity(material)
+ assert style
tool.Style.assign_style_to_object(style, obj)
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py
index 22f7fa5071..9b12dbad55 100644
--- a/src/bonsai/bonsai/bim/module/model/profile.py
+++ b/src/bonsai/bonsai/bim/module/model/profile.py
@@ -1019,7 +1019,7 @@ class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
position = Matrix()
direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized()
- tool.Model.import_axis([Vector((0, 0, 0)), direction * extrusion.Depth], obj=obj, position=position)
+ tool.Model.import_axis((Vector((0, 0, 0)), direction * extrusion.Depth), obj=obj, position=position)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_axis(context))
diff --git a/src/bonsai/bonsai/bim/module/style/data.py b/src/bonsai/bonsai/bim/module/style/data.py
index afcc7eba8d..09cb5447b1 100644
--- a/src/bonsai/bonsai/bim/module/style/data.py
+++ b/src/bonsai/bonsai/bim/module/style/data.py
@@ -97,9 +97,8 @@ class BlenderMaterialStyleData:
material = obj.active_material
if not material:
return False
- props = material.BIMStyleProperties
- style_id = props.ifc_definition_id
- style = tool.Ifc.get_entity_by_id(style_id)
+
+ style = tool.Ifc.get_entity(material)
if not style:
return False
diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py
index 6ec0ccdc3c..0ead9b6751 100644
--- a/src/bonsai/bonsai/bim/module/style/operator.py
+++ b/src/bonsai/bonsai/bim/module/style/operator.py
@@ -118,8 +118,7 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator):
# Don't check blender_material and style_id as this operator is only called from UI.
assert isinstance(self.blender_material, str) # Type checker.
material = bpy.data.materials[self.blender_material]
- style_id = material.BIMStyleProperties.ifc_definition_id
- style = tool.Ifc.get_entity_by_id(style_id)
+ style = tool.Ifc.get_entity(material)
# Material is linked to a style from a different project.
if not style or tool.Ifc.get_object(style) != material:
@@ -221,18 +220,25 @@ class UpdateCurrentStyle(bpy.types.Operator):
def execute(self, context):
style = tool.Ifc.get().by_id(self.style_id)
material = tool.Ifc.get_object(style)
- current_style_type = material.BIMStyleProperties.active_style_type
+ msprops = tool.Style.get_material_style_props(material)
+ current_style_type = msprops.active_style_type
if self.update_all:
sprops = tool.Style.get_style_props()
sprops.active_style_type = current_style_type
return {"FINISHED"}
- updated_materials = set()
+ updated_materials: set[bpy.types.Material] = set()
for obj in context.selected_objects:
+ if not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve)):
+ continue
for mat in obj.data.materials:
- if mat and mat not in updated_materials and mat.BIMStyleProperties.ifc_definition_id != 0:
- mat.BIMStyleProperties.active_style_type = current_style_type
+ if (
+ mat
+ and mat not in updated_materials
+ and (msprops_ := tool.Style.get_material_style_props(mat)).ifc_definition_id != 0
+ ):
+ msprops_.active_style_type = current_style_type
updated_materials.add(mat)
return {"FINISHED"}
@@ -755,7 +761,8 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
bonsai.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback)
material = tool.Ifc.get_object(style)
- active_style_type = material.BIMStyleProperties.active_style_type
+ msprops = tool.Style.get_material_style_props(material)
+ active_style_type = msprops.active_style_type
if self.ifc_class == "IfcExternallyDefinedSurfaceStyle" and active_style_type != "External":
if tool.Style.has_blender_external_style(style_elements):
tool.Style.switch_shading(material, "External")
@@ -803,9 +810,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
# restore selected style type
material = tool.Ifc.get_object(self.style)
- material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type
+ msprops = tool.Style.get_material_style_props(material)
+ msprops.active_style_type = msprops.active_style_type
- def edit_existing_style(self):
+ def edit_existing_style(self) -> None:
ifc_file = tool.Ifc.get()
material = tool.Ifc.get_object(self.style)
assert self.surface_style
@@ -867,7 +875,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
attributes=bonsai.bim.helper.export_attributes(attributes),
)
- def add_new_style(self):
+ def add_new_style(self) -> None:
material = tool.Ifc.get_object(self.style)
if self.props.is_editing_class == "IfcSurfaceStyleShading":
surface_style = ifcopenshell.api.run(
@@ -911,13 +919,13 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
attributes=bonsai.bim.helper.export_attributes(attributes),
)
- def get_shading_attributes(self):
+ def get_shading_attributes(self) -> dict[str, Any]:
return {
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
"Transparency": self.props.transparency or None,
}
- def get_rendering_attributes(self):
+ def get_rendering_attributes(self) -> dict[str, Any]:
if self.props.is_diffuse_colour_null:
diffuse_colour = None
elif self.props.diffuse_colour_class == "IfcColourRgb":
@@ -961,7 +969,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
textures.append(texture_data)
return textures
- def color_to_dict(self, x):
+ def color_to_dict(self, x: tuple[float, float, float]) -> dict[str, Any]:
return {"Red": x[0], "Green": x[1], "Blue": x[2]}
diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py
index 040db82f60..d533473ba2 100644
--- a/src/bonsai/bonsai/bim/module/style/prop.py
+++ b/src/bonsai/bonsai/bim/module/style/prop.py
@@ -39,13 +39,13 @@ from typing import Literal, Union, TYPE_CHECKING, get_args
_ = gettext.gettext
-def get_style_types(self, context):
+def get_style_types(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not StylesData.is_loaded:
StylesData.load()
return StylesData.data["style_types"]
-def get_reflectance_methods(self, context):
+def get_reflectance_methods(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not StylesData.is_loaded:
StylesData.load()
return StylesData.data["reflectance_methods"]
@@ -77,6 +77,16 @@ class Style(PropertyGroup):
type=bpy.types.Material,
)
+ if TYPE_CHECKING:
+ ifc_definition_id: int
+ total_elements: int
+ style_classes: bpy.types.bpy_prop_collection_idprop[StrProperty]
+ has_surface_colour: bool
+ surface_colour: tuple[float, float, float]
+ has_diffuse_colour: bool
+ diffuse_colour: tuple[float, float, float]
+ blender_material: Union[bpy.types.Material, None]
+
STYLE_TYPES = [
("Shading", "Shading", ""),
@@ -86,7 +96,7 @@ STYLE_TYPES = [
def update_shading_styles(self: "BIMStylesProperties", context: bpy.types.Context) -> None:
for mat in bpy.data.materials:
- if mat.BIMStyleProperties.ifc_definition_id == 0:
+ if tool.Blender.get_ifc_definition_id(mat) == 0:
continue
tool.Style.change_current_style_type(mat, self.active_style_type)
@@ -111,7 +121,9 @@ UV_MODES = [
("Camera", "Camera", _("UV from position coordinate in camera space")),
]
-
+TextureMapMode = Literal[
+ "DIFFUSE", "NORMAL", "METALLICROUGHNESS", "SPECULAR", "SHININESS", "EMISSIVE", "OCCLUSION", "AMBIENT"
+]
TEXTURE_MAPS_MODS = (
("DIFFUSE", "DIFFUSE", ""),
("NORMAL", "NORMAL", ""),
@@ -129,6 +141,10 @@ class Texture(PropertyGroup):
# NOTE: subtype `FILE_PATH` is not used to avoid .blend relative paths
path: StringProperty(name="Texture Path", update=update_shader_graph)
+ if TYPE_CHECKING:
+ mode: TextureMapMode
+ path: str
+
class ColourRgb(PropertyGroup):
name: StringProperty()
@@ -136,6 +152,10 @@ class ColourRgb(PropertyGroup):
# not exposed in the UI, here just to preserve the data
color_name: StringProperty(name="Color Name")
+ if TYPE_CHECKING:
+ color_value: tuple[float, float, float]
+ color_name: str
+
# to fit blender.bim.helper.export_attributes
def get_value(self):
return {
diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py
index 4cdf8aa88d..a5042fa800 100644
--- a/src/bonsai/bonsai/bim/module/style/ui.py
+++ b/src/bonsai/bonsai/bim/module/style/ui.py
@@ -16,11 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.module.style.data import StylesData, BlenderMaterialStyleData
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from bonsai.bim.module.style.prop import BIMStylesProperties, Style
class BIM_PT_styles(Panel):
@@ -94,7 +99,8 @@ class BIM_PT_styles(Panel):
if active_style:
row = self.layout.row(align=True)
if material := style.blender_material:
- row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
+ msprops = tool.Style.get_material_style_props(material)
+ row.prop(msprops, "active_style_type", icon="SHADING_RENDERED", text="")
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
op.style_id = style.ifc_definition_id
@@ -255,11 +261,19 @@ class BIM_PT_styles(Panel):
class BIM_UL_styles(UIList):
- def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_property):
+ def draw_item(
+ self,
+ context,
+ layout: bpy.types.UILayout,
+ data: BIMStylesProperties,
+ item: Style,
+ icon,
+ active_data,
+ active_property,
+ ):
if item:
row = layout.row(align=True)
- props = tool.Style.get_style_props()
- if item.ifc_definition_id == props.is_editing_style:
+ if item.ifc_definition_id == data.is_editing_style:
row.label(text="", icon="GREASEPENCIL")
row.prop(item, "name", text="", emboss=False)
if item.has_surface_colour:
@@ -295,7 +309,7 @@ class BIM_PT_style(Panel):
@classmethod
def poll(cls, context):
- return bool(tool.Ifc.get() and (material := context.material) and material.BIMStyleProperties.ifc_definition_id)
+ return bool(tool.Ifc.get() and (material := context.material) and tool.Blender.get_ifc_definition_id(material))
def draw(self, context):
# NOTE: this UI is needed only to indicate whether blender material is linked to IFC
@@ -305,10 +319,11 @@ class BIM_PT_style(Panel):
BlenderMaterialStyleData.load()
material = context.material
- style_id = material.BIMStyleProperties.ifc_definition_id
+ assert material
+ style_id = tool.Blender.get_ifc_definition_id(material)
row = self.layout.row(align=True)
- if style_id and not BlenderMaterialStyleData.data["is_linked_to_style"]:
+ if not BlenderMaterialStyleData.data["is_linked_to_style"]:
row.label(text="Material has linked IFC from a different project.")
op = row.operator("bim.unlink_style", icon="UNLINKED", text="")
op.blender_material = material.name
diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py
index 6ea77101a1..c8846ace19 100644
--- a/src/bonsai/bonsai/tool/blender.py
+++ b/src/bonsai/bonsai/tool/blender.py
@@ -1601,5 +1601,7 @@ class Blender(bonsai.core.tool.Blender):
return obj.BIMObjectProperties
@classmethod
- def get_ifc_definition_id(cls, obj: bpy.types.Object) -> int:
- return tool.Blender.get_object_bim_props(obj).ifc_definition_id
+ def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
+ if isinstance(obj, bpy.types.Object):
+ return tool.Blender.get_object_bim_props(obj).ifc_definition_id
+ return tool.Style.get_material_style_props(obj).ifc_definition_id
diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py
index 56c8e275c6..9524e91ea3 100644
--- a/src/bonsai/bonsai/tool/geometry.py
+++ b/src/bonsai/bonsai/tool/geometry.py
@@ -639,7 +639,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_object_materials_without_styles(cls, obj: bpy.types.Object) -> list[bpy.types.Material]:
return [
- s.material for s in obj.material_slots if s.material and not s.material.BIMStyleProperties.ifc_definition_id
+ s.material for s in obj.material_slots if s.material and not tool.Blender.get_ifc_definition_id(s.material)
]
@classmethod
diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py
index 5f68fc4b0f..83c05101ef 100644
--- a/src/bonsai/bonsai/tool/ifc.py
+++ b/src/bonsai/bonsai/tool/ifc.py
@@ -110,7 +110,7 @@ class Ifc(bonsai.core.tool.Ifc):
if isinstance(obj, bpy.types.Object):
props = tool.Blender.get_object_bim_props(obj)
elif isinstance(obj, bpy.types.Material):
- props = obj.BIMStyleProperties
+ props = tool.Style.get_material_style_props(obj)
else:
props = tool.Geometry.get_mesh_props(obj)
diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py
index 2af631b671..0704fe7154 100644
--- a/src/bonsai/bonsai/tool/model.py
+++ b/src/bonsai/bonsai/tool/model.py
@@ -293,8 +293,19 @@ class Model(bonsai.core.tool.Model):
else:
break
+ unit_scale: float
+ vertices: list[Vector]
+ edges: list[Sequence[int]]
+ arcs: list[Sequence[int]]
+ circles: list[Sequence[int]]
+
@classmethod
- def import_axis(cls, axis, obj=None, position=None):
+ def import_axis(
+ cls,
+ axis: Union[ifcopenshell.entity_instance, tuple[Vector, Vector]],
+ obj=None,
+ position: Optional[Matrix] = None,
+ ) -> bpy.types.Object:
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if position is None:
@@ -305,7 +316,7 @@ class Model(bonsai.core.tool.Model):
cls.arcs = []
cls.circles = []
- if isinstance(axis, list):
+ if isinstance(axis, tuple):
cls.vertices.extend(
[
position @ Vector(cls.convert_unit_to_si(axis[0])).to_3d(),
@@ -478,7 +489,7 @@ class Model(bonsai.core.tool.Model):
@classmethod
def convert_curve_to_mesh(
cls,
- obj: bpy.types.Object,
+ obj: Union[bpy.types.Object, None], # Unused argument.
position: Matrix,
curve: ifcopenshell.entity_instance,
x_angle: Optional[float] = None,
@@ -1624,7 +1635,7 @@ class Model(bonsai.core.tool.Model):
loop_edges = list(bm.edges)
# Create loops from edges
- loops = []
+ loops: list[list[bmesh.types.BMEdge]] = []
while loop_edges:
edge = loop_edges.pop()
loop = [edge]
@@ -1645,19 +1656,19 @@ class Model(bonsai.core.tool.Model):
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
- def is_in_group(v, group_name):
+ def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool:
for group_index in groups[group_name]:
if group_index in v[deform_layer]:
return True
return False
- def get_group_index(v, group_name):
+ def get_group_index(v: bmesh.types.BMVert, group_name: str) -> Union[int, None]:
for group_index in groups[group_name]:
if group_index in v[deform_layer]:
return group_index
# Convert all loops into IFC curves
- curves = []
+ curves: list[ifcopenshell.entity_instance] = []
for loop in loops:
if len(loop) == 1 and all([is_in_group(v, "IFCCIRCLE") for v in loop[0].verts]):
@@ -1670,7 +1681,7 @@ class Model(bonsai.core.tool.Model):
tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius)
)
else:
- loop_verts = []
+ loop_verts: list[bmesh.types.BMVert] = []
for i, edge in enumerate(loop):
if i == 0 and len(loop) == 1:
loop_verts.append(edge.verts[0])
@@ -1746,7 +1757,7 @@ class Model(bonsai.core.tool.Model):
curves.append(tmp.createIfcIndexedPolyCurve(points))
# Sort IFC curves into either closed, or closed with void profile defs
- profile_defs = []
+ profile_defs: list[ifcopenshell.entity_instance] = []
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py
index 4c824f8b62..b8269f4134 100644
--- a/src/bonsai/bonsai/tool/root.py
+++ b/src/bonsai/bonsai/tool/root.py
@@ -418,8 +418,8 @@ class Root(bonsai.core.tool.Root):
"""Rename material without triggerring name callback and unnecessary writing to IFC."""
if material.name == name:
return
- props = material.BIMStyleProperties
- props.is_renaming = True
+ msprops = tool.Style.get_material_style_props(material)
+ msprops.is_renaming = True
material.name = name # The handler will trigger, and reset is_renaming to False.
@classmethod
diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py
index b7a7cb7241..42a3dc207a 100644
--- a/src/bonsai/bonsai/tool/style.py
+++ b/src/bonsai/bonsai/tool/style.py
@@ -162,14 +162,14 @@ class Style(bonsai.core.tool.Style):
cls, blender_material_or_style: Union[bpy.types.Material, ifcopenshell.entity_instance]
) -> dict[str, ifcopenshell.entity_instance]:
if isinstance(blender_material_or_style, bpy.types.Material):
- if not (ifc_definition_id := blender_material_or_style.BIMStyleProperties.ifc_definition_id):
+ style = tool.Ifc.get_entity(blender_material_or_style)
+ if not style:
return {}
- style = tool.Ifc.get().by_id(ifc_definition_id)
else:
style = blender_material_or_style
style_elements = {}
- for style in style.Styles:
- style_elements[style.is_a()] = style
+ for style_ in style.Styles:
+ style_elements[style_.is_a()] = style_
return style_elements
@classmethod
@@ -305,7 +305,7 @@ class Style(bonsai.core.tool.Style):
return next((l.from_node for l in input_pin.links if l.from_node.type == of_type), None)
return next((l.from_node for l in input_pin.links), None)
- props = obj.BIMStyleProperties
+ props = tool.Style.get_material_style_props(obj)
transparency = 1 - obj.diffuse_color[3]
diffuse_color = obj.diffuse_color
viewport_color = color_to_ifc_format(obj.diffuse_color)
@@ -487,16 +487,14 @@ class Style(bonsai.core.tool.Style):
@classmethod
def get_surface_shading_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
- if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id:
- style = tool.Ifc.get().by_id(ifc_definition_id)
+ if style := tool.Ifc.get_entity(obj):
items = [s for s in style.Styles if s.is_a() == "IfcSurfaceStyleShading"]
if items:
return items[0]
@classmethod
def get_surface_texture_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
- if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id:
- style = tool.Ifc.get().by_id(ifc_definition_id)
+ if style := tool.Ifc.get_entity(obj):
items = [s for s in style.Styles if s.is_a("IfcSurfaceStyleWithTextures")]
if items:
return items[0]
@@ -581,7 +579,8 @@ class Style(bonsai.core.tool.Style):
@classmethod
def change_current_style_type(cls, blender_material: bpy.types.Material, style_type: str) -> None:
- blender_material.BIMStyleProperties.active_style_type = style_type
+ props = cls.get_material_style_props(blender_material)
+ props.active_style_type = style_type
@classmethod
def get_styled_items(cls, style: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
@@ -632,7 +631,8 @@ class Style(bonsai.core.tool.Style):
@classmethod
def reload_material_from_ifc(cls, blender_material: bpy.types.Material) -> None:
- blender_material.BIMStyleProperties.active_style_type = blender_material.BIMStyleProperties.active_style_type
+ props = cls.get_material_style_props(blender_material)
+ props.active_style_type = props.active_style_type
@classmethod
def switch_shading(cls, blender_material: bpy.types.Material, style_type: StyleType) -> None:
diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py
index 2163489685..7385543ff9 100644
--- a/src/bonsai/test/bim/test_feature.py
+++ b/src/bonsai/test/bim/test_feature.py
@@ -911,14 +911,14 @@ def the_material_name_is_not_an_ifc_material(name):
@then(parsers.parse('the material "{name}" is an IFC style'))
def the_material_name_is_an_ifc_style(name):
obj = the_material_name_exists(name)
- ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id
+ ifc_definition_id = tool.Blender.get_ifc_definition_id(obj)
assert ifc_definition_id != 0, f"The material {obj} has a style ID of {ifc_definition_id}"
@then(parsers.parse('the material "{name}" is not an IFC style'))
def the_material_name_is_not_an_ifc_style(name):
obj = the_material_name_exists(name)
- ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id
+ ifc_definition_id = tool.Blender.get_ifc_definition_id(obj)
assert ifc_definition_id == 0, f"The material {obj} has a style ID of {ifc_definition_id}"
diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py
index 2a2cb6b718..6c4c338b3d 100644
--- a/src/bonsai/test/tool/test_drawing.py
+++ b/src/bonsai/test/tool/test_drawing.py
@@ -891,11 +891,12 @@ class TestAddReferenceImage(NewFile):
assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0)))
material = obj.active_material
+ assert material
assert material.name == "image"
- assert material.BIMStyleProperties.ifc_definition_id != 0
+ assert tool.Blender.get_ifc_definition_id(material) != 0
- ifc_file = tool.Ifc.get()
- style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id)
+ style = tool.Ifc.get_entity(material)
+ assert style
styled_items = set(tool.Style.get_styled_items(style))
representation_items = set(tool.Geometry.get_active_representation(obj).Items)
assert styled_items == representation_items
diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py
index c0d1819d9a..85a0a4c79d 100644
--- a/src/bonsai/test/tool/test_geometry.py
+++ b/src/bonsai/test/tool/test_geometry.py
@@ -112,7 +112,8 @@ class TestGetObjectMaterialsWithoutStyles(NewFile):
material1 = bpy.data.materials.new("Material")
material2 = bpy.data.materials.new("Material")
material3 = bpy.data.materials.new("Material")
- material3.BIMStyleProperties.ifc_definition_id = 1
+ props = tool.Style.get_material_style_props(material3)
+ props.ifc_definition_id = 1
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
obj.data.materials.append(material1)
obj.data.materials.append(material2)
@@ -307,7 +308,7 @@ class TestRecordObjectMaterials(NewFile):
tool.Ifc.set(ifc)
style = ifc.createIfcSurfaceStyle()
material = bpy.data.materials.new("Material")
- material.BIMStyleProperties.ifc_definition_id = style.id()
+ tool.Ifc.link(style, material)
obj.data.materials.append(material)
subject.record_object_materials(obj)
assert tool.Geometry.get_mesh_props(obj.data).material_checksum == str([style.id()])
diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py
index afcf0e6b5a..8056db1349 100644
--- a/src/bonsai/test/tool/test_model.py
+++ b/src/bonsai/test/tool/test_model.py
@@ -462,7 +462,7 @@ class TestApplyIfcMaterialChanges(NewFile):
def get_used_styles(self, obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
return {
- ifc_file.by_id(s.material.BIMStyleProperties.ifc_definition_id) for s in obj.material_slots if s.material
+ ifc_file.by_id(tool.Blender.get_ifc_definition_id(s.material)) for s in obj.material_slots if s.material
}
def get_mesh(self, obj: bpy.types.Object) -> bpy.types.Mesh:
diff --git a/src/bonsai/test/tool/test_polyline.py b/src/bonsai/test/tool/test_polyline.py
index b7d4303b0d..9dd1e8f41c 100644
--- a/src/bonsai/test/tool/test_polyline.py
+++ b/src/bonsai/test/tool/test_polyline.py
@@ -18,6 +18,9 @@
import bpy
import ifcopenshell
+import ifcopenshell.api.project
+import ifcopenshell.api.root
+import ifcopenshell.api.unit
import bonsai.core.tool
import bonsai.tool as tool
from test.bim.bootstrap import NewFile
diff --git a/src/bonsai/test/tool/test_style.py b/src/bonsai/test/tool/test_style.py
index 6af83895a3..5f37d397b5 100644
--- a/src/bonsai/test/tool/test_style.py
+++ b/src/bonsai/test/tool/test_style.py
@@ -308,7 +308,8 @@ class TestGetSurfaceRenderingStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleRendering()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
- obj.BIMStyleProperties.ifc_definition_id = style.id()
+ props = tool.Style.get_material_style_props(obj)
+ props.ifc_definition_id = style.id()
assert subject.get_surface_rendering_style(obj) == style_item
@@ -347,7 +348,7 @@ class TestGetSurfaceShadingStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleShading()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
- obj.BIMStyleProperties.ifc_definition_id = style.id()
+ tool.Ifc.link(style, obj)
assert subject.get_surface_shading_style(obj) == style_item
def test_do_not_get_rendering_styles(self):
@@ -355,7 +356,7 @@ class TestGetSurfaceShadingStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleRendering()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
- obj.BIMStyleProperties.ifc_definition_id = style.id()
+ tool.Ifc.link(style, obj)
assert subject.get_surface_shading_style(obj) is None
@@ -365,7 +366,7 @@ class TestGetSurfaceTextureStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleWithTextures()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
- obj.BIMStyleProperties.ifc_definition_id = style.id()
+ tool.Ifc.link(style, obj)
assert subject.get_surface_texture_style(obj) == style_item
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py
index ddaab44e20..9458131046 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py
@@ -21,159 +21,203 @@ import ifcopenshell.util.element
import ifcopenshell.util.shape
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
-from typing import Any, Union, Optional, Literal
+from typing import Any, Union, Optional, Literal, get_args
VECTOR_3D = tuple[float, float, float]
+CardinalPointNumeric = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
+CardinalPointString = Literal[
+ "bottom left",
+ "bottom centre",
+ "bottom right",
+ "mid-depth left",
+ "mid-depth centre",
+ "mid-depth right",
+ "top left",
+ "top centre",
+ "top right",
+ "geometric centroid",
+ "bottom in line with the geometric centroid",
+ "left in line with the geometric centroid",
+ "right in line with the geometric centroid",
+ "top in line with the geometric centroid",
+ "shear centre",
+ "bottom in line with the shear centre",
+ "left in line with the shear centre",
+ "right in line with the shear centre",
+ "top in line with the shear centre",
+]
+CARDINAL_POINT_VALUES: tuple[CardinalPointString, ...] = get_args(CardinalPointString)
+CardinalPoint = Union[CardinalPointNumeric, CardinalPointString]
def add_profile_representation(
file: ifcopenshell.file,
- # IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
- # in meters
depth: float = 1.0,
- cardinal_point: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 5,
- # A list of planes that define clipping half space solids
- # Planes are defined either by Clipping objects
- # or by dictionaries of arguments for `Clipping.parse`
+ # TODO: None makes more sense as default value?
+ cardinal_point: Union[CardinalPoint, None] = 5,
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None),
) -> ifcopenshell.entity_instance:
+ """Add profile representation.
+
+ :param context: The IfcGeometricRepresentationContext for the representation,
+ only Model/Body/MODEL_VIEW type of representations are currently supported.
+ :param profile: The IfcProfileDef to extrude.
+ :param depth: The depth of the extrusion in meters.
+ :param cardinal_point: The cardinal point of the profile.
+ :param clippings: A list of planes that define clipping half space solids.
+ Planes are defined either by Clipping objects
+ or by dictionaries of arguments for `Clipping.parse`.
+ :param placement_zx_axes: A tuple of two vectors that define the placement of the profile.
+ The first vector is the Z axis, the second vector is the X axis.
+ :return: IfcShapeRepresentation.
+ """
usecase = Usecase()
usecase.file = file
- usecase.settings = {
- "context": context,
- "profile": profile,
- "depth": depth,
- "cardinal_point": cardinal_point,
- "clippings": clippings if clippings is not None else [],
- "placement_zx_axes": placement_zx_axes,
- }
- return usecase.execute()
+ clippings = clippings if clippings is not None else []
+ return usecase.execute(context, profile, depth, cardinal_point, clippings, placement_zx_axes)
class Usecase:
file: ifcopenshell.file
- settings: dict[str, Any]
+ clippings: list[Clipping]
- def execute(self):
- self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
- self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
- return self.file.createIfcShapeRepresentation(
- self.settings["context"],
- self.settings["context"].ContextIdentifier,
- "Clipping" if self.settings["clippings"] else "SweptSolid",
+ def execute(
+ self,
+ context: ifcopenshell.entity_instance,
+ profile: ifcopenshell.entity_instance,
+ depth: float,
+ cardinal_point: Union[CardinalPoint, None],
+ clippings: list[Union[Clipping, dict[str, Any]]],
+ placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]],
+ ) -> ifcopenshell.entity_instance:
+ if isinstance(cardinal_point, int):
+ cardinal_point = CARDINAL_POINT_VALUES[cardinal_point - 1]
+
+ self.cardinal_point = cardinal_point
+ self.profile = profile
+ self.clippings = [Clipping.parse(c) for c in clippings]
+ self.depth = depth
+ self.placement_zx_axes = placement_zx_axes
+ self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
+ return self.file.create_entity(
+ "IfcShapeRepresentation",
+ context,
+ context.ContextIdentifier,
+ "Clipping" if self.clippings else "SweptSolid",
[self.create_item()],
)
- def create_item(self):
+ def create_item(self) -> ifcopenshell.entity_instance:
point = self.get_point()
placement = self.file.createIfcAxis2Placement3D(
point,
- self.file.createIfcDirection(self.settings["placement_zx_axes"][0] or (0.0, 0.0, 1.0)),
- self.file.createIfcDirection(self.settings["placement_zx_axes"][1] or (1.0, 0.0, 0.0)),
+ self.file.create_entity("IfcDirection", self.placement_zx_axes[0] or (0.0, 0.0, 1.0)),
+ self.file.create_entity("IfcDirection", self.placement_zx_axes[1] or (1.0, 0.0, 0.0)),
)
- extrusion = self.file.createIfcExtrudedAreaSolid(
- self.settings["profile"],
+ extrusion = self.file.create_entity(
+ "IfcExtrudedAreaSolid",
+ self.profile,
placement,
self.file.createIfcDirection((0.0, 0.0, 1.0)),
- self.convert_si_to_unit(self.settings["depth"]),
+ self.convert_si_to_unit(self.depth),
)
- if self.settings["clippings"]:
+ if self.clippings:
return self.apply_clippings(extrusion)
return extrusion
- def apply_clippings(self, first_operand):
- while self.settings["clippings"]:
- clipping = self.settings["clippings"].pop()
+ def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
+ while self.clippings:
+ clipping = self.clippings.pop()
if isinstance(clipping, ifcopenshell.entity_instance):
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
else: # Clipping
- first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"])
+ first_operand = clipping.apply(self.file, first_operand, self.unit_scale)
return first_operand
- def convert_si_to_unit(self, co):
- return co / self.settings["unit_scale"]
+ def convert_si_to_unit(self, co: float) -> float:
+ return co / self.unit_scale
- def get_point(self):
- if not self.settings["cardinal_point"]:
+ def get_point(self) -> ifcopenshell.entity_instance:
+ if not self.cardinal_point:
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
- elif self.settings["cardinal_point"] == 1:
+ elif self.cardinal_point == "bottom left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, self.get_y() / 2, 0.0))
- elif self.settings["cardinal_point"] == 2:
+ elif self.cardinal_point == "bottom centre":
return self.file.createIfcCartesianPoint((0.0, self.get_y() / 2, 0.0))
- elif self.settings["cardinal_point"] == 3:
+ elif self.cardinal_point == "bottom right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, self.get_y() / 2, 0.0))
- elif self.settings["cardinal_point"] == 4:
+ elif self.cardinal_point == "mid-depth left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, 0.0, 0.0))
- elif self.settings["cardinal_point"] == 5:
+ elif self.cardinal_point == "mid-depth centre":
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
- elif self.settings["cardinal_point"] == 6:
+ elif self.cardinal_point == "mid-depth right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, 0.0, 0.0))
- elif self.settings["cardinal_point"] == 7:
+ elif self.cardinal_point == "top left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, -self.get_y() / 2, 0.0))
- elif self.settings["cardinal_point"] == 8:
+ elif self.cardinal_point == "top centre":
return self.file.createIfcCartesianPoint((0.0, -self.get_y() / 2, 0.0))
- elif self.settings["cardinal_point"] == 9:
+ elif self.cardinal_point == "top right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, -self.get_y() / 2, 0.0))
# TODO other cardinal points
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
- def get_x(self):
- if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"):
- return self.settings["profile"].OverallWidth
- elif self.settings["profile"].is_a("IfcCShapeProfileDef"):
- return self.settings["profile"].Width
- elif self.settings["profile"].is_a("IfcCircleProfileDef"):
- return self.settings["profile"].Radius * 2
- elif self.settings["profile"].is_a("IfcEllipseProfileDef"):
- return self.settings["profile"].SemiAxis1 * 2
- elif self.settings["profile"].is_a("IfcIShapeProfileDef"):
- return self.settings["profile"].OverallWidth
- elif self.settings["profile"].is_a("IfcLShapeProfileDef"):
- return self.settings["profile"].Width
- elif self.settings["profile"].is_a("IfcRectangleProfileDef"):
- return self.settings["profile"].XDim
- elif self.settings["profile"].is_a("IfcTShapeProfileDef"):
- return self.settings["profile"].FlangeWidth
- elif self.settings["profile"].is_a("IfcUShapeProfileDef"):
- return self.settings["profile"].FlangeWidth
- elif self.settings["profile"].is_a("IfcZShapeProfileDef"):
- return (self.settings["profile"].FlangeWidth * 2) - self.settings["profile"].WebThickness
+ def get_x(self) -> float:
+ if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
+ return self.profile.OverallWidth
+ elif self.profile.is_a("IfcCShapeProfileDef"):
+ return self.profile.Width
+ elif self.profile.is_a("IfcCircleProfileDef"):
+ return self.profile.Radius * 2
+ elif self.profile.is_a("IfcEllipseProfileDef"):
+ return self.profile.SemiAxis1 * 2
+ elif self.profile.is_a("IfcIShapeProfileDef"):
+ return self.profile.OverallWidth
+ elif self.profile.is_a("IfcLShapeProfileDef"):
+ return self.profile.Width
+ elif self.profile.is_a("IfcRectangleProfileDef"):
+ return self.profile.XDim
+ elif self.profile.is_a("IfcTShapeProfileDef"):
+ return self.profile.FlangeWidth
+ elif self.profile.is_a("IfcUShapeProfileDef"):
+ return self.profile.FlangeWidth
+ elif self.profile.is_a("IfcZShapeProfileDef"):
+ return (self.profile.FlangeWidth * 2) - self.profile.WebThickness
else:
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
- shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"])
+ shape = ifcopenshell.geom.create_shape(settings, self.profile)
return self.convert_si_to_unit(ifcopenshell.util.shape.get_x(shape))
return 0.0
- def get_y(self):
- if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"):
- return self.settings["profile"].OverallDepth
- elif self.settings["profile"].is_a("IfcCShapeProfileDef"):
- return self.settings["profile"].Depth
- elif self.settings["profile"].is_a("IfcCircleProfileDef"):
- return self.settings["profile"].Radius * 2
- elif self.settings["profile"].is_a("IfcEllipseProfileDef"):
- return self.settings["profile"].SemiAxis2 * 2
- elif self.settings["profile"].is_a("IfcIShapeProfileDef"):
- return self.settings["profile"].OverallDepth
- elif self.settings["profile"].is_a("IfcLShapeProfileDef"):
- return self.settings["profile"].Depth
- elif self.settings["profile"].is_a("IfcRectangleProfileDef"):
- return self.settings["profile"].YDim
- elif self.settings["profile"].is_a("IfcTShapeProfileDef"):
- return self.settings["profile"].Depth
- elif self.settings["profile"].is_a("IfcUShapeProfileDef"):
- return self.settings["profile"].Depth
- elif self.settings["profile"].is_a("IfcZShapeProfileDef"):
- return self.settings["profile"].Depth
+ def get_y(self) -> float:
+ if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
+ return self.profile.OverallDepth
+ elif self.profile.is_a("IfcCShapeProfileDef"):
+ return self.profile.Depth
+ elif self.profile.is_a("IfcCircleProfileDef"):
+ return self.profile.Radius * 2
+ elif self.profile.is_a("IfcEllipseProfileDef"):
+ return self.profile.SemiAxis2 * 2
+ elif self.profile.is_a("IfcIShapeProfileDef"):
+ return self.profile.OverallDepth
+ elif self.profile.is_a("IfcLShapeProfileDef"):
+ return self.profile.Depth
+ elif self.profile.is_a("IfcRectangleProfileDef"):
+ return self.profile.YDim
+ elif self.profile.is_a("IfcTShapeProfileDef"):
+ return self.profile.Depth
+ elif self.profile.is_a("IfcUShapeProfileDef"):
+ return self.profile.Depth
+ elif self.profile.is_a("IfcZShapeProfileDef"):
+ return self.profile.Depth
else:
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
- shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"])
+ shape = ifcopenshell.geom.create_shape(settings, self.profile)
return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape))
return 0.0
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py
index 0fc5cccdf9..4c004ecfdd 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py
@@ -149,6 +149,8 @@ class Usecase:
self.settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(self.settings_2d, dummy_solid)
+ # NOTE: points do not need unit conversion
+ # as dummy file is inherently using project units.
if self.cardinal_point == 1:
return self.get_bottom_left(shape)
elif self.cardinal_point == 2:
diff --git a/src/ifcopenshell-python/ifcopenshell/util/data.py b/src/ifcopenshell-python/ifcopenshell/util/data.py
index 365f0df2cb..9f02d973a5 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/data.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/data.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
+from __future__ import annotations
import numpy as np
import ifcopenshell
from typing import Any, Union
@@ -30,7 +31,9 @@ class Clipping:
operand_type: str = "IfcHalfSpaceSolid"
@classmethod
- def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, "Clipping", None]:
+ def parse(
+ cls, raw_data: Union[ifcopenshell.entity_instance, Clipping, dict[str, Any]]
+ ) -> Union[ifcopenshell.entity_instance, Clipping]:
"""Parse various formats into a clipping object
`raw_data` can be either:
diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py b/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py
index 1ee1b3ac87..c827f1e33a 100644
--- a/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py
+++ b/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py
@@ -20,6 +20,7 @@ import test.bootstrap
import ifcopenshell.api.root
import ifcopenshell.api.context
import ifcopenshell.api.geometry
+import ifcopenshell.util.shape_builder
class TestAddShapeAspect(test.bootstrap.IFC4):