diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py
index a492752f8d..788e398cde 100644
--- a/src/bonsai/bonsai/bim/helper.py
+++ b/src/bonsai/bonsai/bim/helper.py
@@ -16,11 +16,12 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
-# from datetime import date
+from __future__ import annotations
import bpy
import json
import math
import ifcopenshell
+import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import ifcopenshell.util.unit
@@ -29,14 +30,27 @@ from mathutils import geometry
from mathutils import Vector
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
-from typing import Optional, Callable, Any, Union
+from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ import bonsai.bim.prop
+
+ # ImportCallback return values:
+ # - None - property should be imported by default workflow
+ # - True - setting value for imported attribute should be skipped
+ # - False - property should be skipped entirely from import
+ ImportCallback = Callable[[str, Optional[bonsai.bim.prop.Attribute], dict[str, Any], Union[bool, None]]]
+ # ExportCallback return values:
+ # - True - property should be skipped entirely from export
+ # - False - property should be exproted by default workflow
+ ExportCallback = Callable[[dict[str, Any], bonsai.bim.prop.Attribute], bool]
def draw_attributes(
- props: list[bpy.types.PropertyGroup],
+ props: list[bonsai.bim.prop.Attribute],
layout: bpy.types.UILayout,
copy_operator: Optional[str] = None,
- popup_active_attribute: Optional[bpy.types.PropertyGroup] = None,
+ popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
) -> None:
"""you can set attribute active in popup with `active_attribute`
meaning you will be able to type into attribute's field without having to click
@@ -50,7 +64,7 @@ def draw_attributes(
def draw_attribute(
- attribute: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
+ attribute: bonsai.bim.prop.Attribute, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
) -> None:
value_name = attribute.get_value_name()
if not value_name:
@@ -94,7 +108,12 @@ def draw_attribute(
op.name = attribute.name
-def import_attributes(ifc_class, props, data, callback=None):
+def import_attributes(
+ ifc_class: str,
+ props: bpy.types.bpy_prop_collection,
+ data: dict[str, Any],
+ callback: Optional[ImportCallback] = None,
+) -> None:
for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
import_attribute(attribute, props, data, callback=callback)
@@ -102,8 +121,8 @@ def import_attributes(ifc_class, props, data, callback=None):
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
- props: bpy.types.PropertyGroup,
- callback: Optional[Callable] = None,
+ props: bpy.types.bpy_prop_collection,
+ callback: Optional[ImportCallback] = None,
) -> None:
if isinstance(element, str):
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
@@ -116,8 +135,14 @@ def import_attributes2(
import_attribute(attribute, props, info, callback=callback)
-def import_attribute(attribute, props, data, callback=None):
+def import_attribute(
+ attribute: W.attribute,
+ props: bpy.types.bpy_prop_collection,
+ data: dict[str, Any],
+ callback: Optional[ImportCallback] = None,
+) -> None:
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
+ # Complex data types (aggregates and entities) are handled only by callback.
if isinstance(data_type, tuple) or data_type == "entity":
callback(attribute.name(), None, data) if callback else None
return
@@ -156,7 +181,7 @@ def import_attribute(attribute, props, data, callback=None):
ATTRIBUTE_MIN_MAX_CONSTRAINTS = {"IfcMaterialLayer": {"Priority": {"value_min": 0, "value_max": 100}}}
-def add_attribute_min_max(attribute_blender):
+def add_attribute_min_max(attribute_blender: bonsai.bim.prop.Attribute) -> None:
if attribute_blender.ifc_class in ATTRIBUTE_MIN_MAX_CONSTRAINTS:
constraints = ATTRIBUTE_MIN_MAX_CONSTRAINTS[attribute_blender.ifc_class].get(attribute_blender.name, {})
for constraint, value in constraints.items():
@@ -164,7 +189,9 @@ def add_attribute_min_max(attribute_blender):
setattr(attribute_blender, constraint + "_constraint", True)
-def add_attribute_enum_items_descriptions(attribute_blender, enum_items):
+def add_attribute_enum_items_descriptions(
+ attribute_blender: bonsai.bim.prop.Attribute, enum_items: Iterable[str]
+) -> None:
attribute_blender.enum_descriptions.clear()
if isinstance(enum_items, dict):
enum_items = enum_items.keys()
@@ -178,7 +205,7 @@ def add_attribute_enum_items_descriptions(attribute_blender, enum_items):
new_enum_description.name = description
-def add_attribute_description(attribute_blender, attribute_ifc=None):
+def add_attribute_description(attribute_blender: bonsai.bim.prop.Attribute, attribute_ifc=None):
if not attribute_blender.name:
return
version = tool.Ifc.get_schema()
@@ -196,8 +223,11 @@ def add_attribute_description(attribute_blender, attribute_ifc=None):
attribute_blender.description = description
-def export_attributes(props, callback: Optional[Callable] = None) -> dict[str, Any]:
- attributes = {}
+def export_attributes(
+ props: bpy.types.bpy_prop_collection,
+ callback: Optional[ExportCallback] = None,
+) -> dict[str, Any]:
+ attributes: dict[str, Any] = {}
for prop in props:
is_handled_by_callback = callback(attributes, prop) if callback else False
if is_handled_by_callback:
@@ -206,7 +236,13 @@ def export_attributes(props, callback: Optional[Callable] = None) -> dict[str, A
return attributes
-def prop_with_search(layout, data, prop_name, should_click_ok_to_validate=False, **kwargs):
+def prop_with_search(
+ layout: bpy.types.UILayout,
+ data: Union[bpy.types.PropertyGroup, bpy.types.ID],
+ prop_name: str,
+ should_click_ok_to_validate: bool = False,
+ **kwargs: Any,
+):
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
row.prop(data, prop_name, **kwargs)
@@ -221,7 +257,11 @@ def prop_with_search(layout, data, prop_name, should_click_ok_to_validate=False,
pass
-def get_enum_items(data, prop_name, context=None):
+def get_enum_items(
+ data: Union[bpy.types.PropertyGroup, bpy.types.ID], prop_name: str, context: Optional[bpy.types.Context] = None
+) -> Union[
+ Iterable[Union[tuple[str, str, str], tuple[str, str, str, int], tuple[str, str, str, str, int], None]], None
+]:
# Retrieve items from a dynamic EnumProperty, which is otherwise not supported
# Or throws an error in the console when the items callback returns an empty list
# See https://blender.stackexchange.com/q/215781/86891
@@ -235,7 +275,7 @@ def get_enum_items(data, prop_name, context=None):
return items
-def convert_property_group_from_si(property_group, skip_props=()):
+def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip_props: tuple[str, ...] = ()) -> None:
"""Method converts property group values from si to current ifc project units
based on default values of the properties.
@@ -254,7 +294,7 @@ def convert_property_group_from_si(property_group, skip_props=()):
setattr(property_group, prop_name, prop_value)
-def draw_filter(layout, filter_groups, data, module):
+def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None:
if not data.is_loaded:
data.load()
diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py
index ecf59304c8..8681615fd3 100644
--- a/src/bonsai/bonsai/bim/import_ifc.py
+++ b/src/bonsai/bonsai/bim/import_ifc.py
@@ -1095,7 +1095,9 @@ class IfcImporter:
self.mesh_data["loop_start"].extend(loop_start)
# list(di1.keys())
- def create_native_swept_disk_solid(self, element, mesh_name, native_data):
+ def create_native_swept_disk_solid(
+ self, element: ifcopenshell.entity_instance, mesh_name: str, native_data: dict[str, Any]
+ ) -> bpy.types.Curve:
# TODO: georeferencing?
curve = bpy.data.curves.new(mesh_name, type="CURVE")
curve.dimensions = "3D"
diff --git a/src/bonsai/bonsai/core/context.py b/src/bonsai/bonsai/core/context.py
index 5ef631bae2..c251a263c4 100644
--- a/src/bonsai/bonsai/core/context.py
+++ b/src/bonsai/bonsai/core/context.py
@@ -16,8 +16,22 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
+from typing import TYPE_CHECKING, Optional
-def add_context(ifc, context_type=None, context_identifier=None, target_view=None, parent=None):
+if TYPE_CHECKING:
+ import bpy
+ import ifcopenshell
+ import bonsai.tool as tool
+
+
+def add_context(
+ ifc: tool.Ifc,
+ context_type: Optional[str] = None,
+ context_identifier: Optional[str] = None,
+ target_view: Optional[str] = None,
+ parent: Optional[str] = None,
+) -> ifcopenshell.entity_instance:
return ifc.run(
"context.add_context",
context_type=context_type,
@@ -27,19 +41,19 @@ def add_context(ifc, context_type=None, context_identifier=None, target_view=Non
)
-def remove_context(ifc, context=None):
+def remove_context(ifc: tool.Ifc, context: ifcopenshell.entity_instance) -> None:
ifc.run("context.remove_context", context=context)
-def enable_editing_context(context_tool, context=None):
+def enable_editing_context(context_tool: tool.Context, context: ifcopenshell.entity_instance) -> None:
context_tool.set_context(context)
context_tool.import_attributes()
-def disable_editing_context(context):
+def disable_editing_context(context: tool.Context) -> None:
context.clear_context()
-def edit_context(ifc, context):
+def edit_context(ifc: tool.Ifc, context: tool.Context) -> None:
ifc.run("context.edit_context", context=context.get_context(), attributes=context.export_attributes())
disable_editing_context(context)
diff --git a/src/bonsai/bonsai/tool/context.py b/src/bonsai/bonsai/tool/context.py
index dfb3590e59..ec38f2a1e4 100644
--- a/src/bonsai/bonsai/tool/context.py
+++ b/src/bonsai/bonsai/tool/context.py
@@ -20,20 +20,22 @@ import bpy
import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.tool
+import ifcopenshell
+from typing import Any, Union
class Context(bonsai.core.tool.Context):
@classmethod
- def set_context(cls, context):
+ def set_context(cls, context: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMContextProperties.active_context_id = context.id()
@classmethod
- def import_attributes(cls):
+ def import_attributes(cls) -> None:
props = bpy.context.scene.BIMContextProperties
props.context_attributes.clear()
context = cls.get_context()
- def callback(name, prop, data):
+ def callback(name: str, prop, data) -> Union[bool, None]:
if context.is_a("IfcGeometricRepresentationSubContext"):
if name == "Precision":
props.context_attributes.remove(props.context_attributes.find("Precision"))
@@ -45,13 +47,13 @@ class Context(bonsai.core.tool.Context):
bonsai.bim.helper.import_attributes(context.is_a(), props.context_attributes, context.get_info(), callback)
@classmethod
- def clear_context(cls):
+ def clear_context(cls) -> None:
bpy.context.scene.BIMContextProperties.active_context_id = 0
@classmethod
- def get_context(cls):
+ def get_context(cls) -> ifcopenshell.entity_instance:
return tool.Ifc.get().by_id(bpy.context.scene.BIMContextProperties.active_context_id)
@classmethod
- def export_attributes(cls):
+ def export_attributes(cls) -> dict[str, Any]:
return bonsai.bim.helper.export_attributes(bpy.context.scene.BIMContextProperties.context_attributes)