diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py
index b159020d43..2a3a5f97a2 100644
--- a/src/bonsai/bonsai/bim/import_ifc.py
+++ b/src/bonsai/bonsai/bim/import_ifc.py
@@ -1076,9 +1076,10 @@ class IfcImporter:
print(traceback.format_exc())
def set_default_context(self):
+ rprops = tool.Root.get_root_props()
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
if subcontext.ContextIdentifier == "Body":
- bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
+ rprops.contexts = str(subcontext.id())
break
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py
index cc713e5bd7..797f72714f 100644
--- a/src/bonsai/bonsai/bim/module/model/data.py
+++ b/src/bonsai/bonsai/bim/module/model/data.py
@@ -97,7 +97,7 @@ class AuthoringData:
@classmethod
def default_container(cls) -> str | None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if props.default_container:
try:
return tool.Ifc.get().by_id(props.default_container).Name
diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py
index ef00ea7b21..4e4ebd731d 100644
--- a/src/bonsai/bonsai/bim/module/model/product.py
+++ b/src/bonsai/bonsai/bim/module/model/product.py
@@ -58,7 +58,8 @@ class AddEmptyType(bpy.types.Operator, AddObjectHelper):
def execute(self, context):
obj = bpy.data.objects.new("TYPEX", None)
context.scene.collection.objects.link(obj)
- context.scene.BIMRootProperties.ifc_product = "IfcElementType"
+ rprops = tool.Root.get_root_props()
+ rprops.ifc_product = "IfcElementType"
tool.Blender.select_and_activate_single_object(context, obj)
return {"FINISHED"}
@@ -71,7 +72,7 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator):
ifc_element_type: bpy.props.StringProperty()
def _execute(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
props.ifc_product = "IfcElementType"
props.ifc_class = self.ifc_element_type
if self.ifc_element_type == "IfcWallType":
@@ -363,7 +364,7 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator):
)
bonsai.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
- rprops = context.scene.BIMRootProperties
+ rprops = tool.Root.get_root_props()
ifc_context = None
if get_enum_items(rprops, "contexts", context):
ifc_context = int(rprops.contexts or "0") or None
diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py
index 8a0376eb64..64edaa2a9d 100644
--- a/src/bonsai/bonsai/bim/module/root/data.py
+++ b/src/bonsai/bonsai/bim/module/root/data.py
@@ -60,7 +60,8 @@ class IfcClassData:
@classmethod
def ifc_classes(cls):
- ifc_product = bpy.context.scene.BIMRootProperties.ifc_product
+ rprops = tool.Root.get_root_props()
+ ifc_product = rprops.ifc_product
declaration = tool.Ifc.schema().declaration_by_name(ifc_product)
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
@@ -80,7 +81,8 @@ class IfcClassData:
@classmethod
def ifc_predefined_types(cls):
types_enum = []
- ifc_class = bpy.context.scene.BIMRootProperties.ifc_class
+ rprops = tool.Root.get_root_props()
+ ifc_class = rprops.ifc_class
declaration = tool.Ifc.schema().declaration_by_name(ifc_class)
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
@@ -114,7 +116,8 @@ class IfcClassData:
@classmethod
def representation_template(cls):
- ifc_class = bpy.context.scene.BIMRootProperties.ifc_class
+ rprops = tool.Root.get_root_props()
+ ifc_class = rprops.ifc_class
templates = [
("EMPTY", "No Geometry", "Start with an empty object"),
None,
diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py
index 77fc3eebb9..a699287365 100644
--- a/src/bonsai/bonsai/bim/module/root/operator.py
+++ b/src/bonsai/bonsai/bim/module/root/operator.py
@@ -42,6 +42,7 @@ class EnableReassignClass(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
+ rprops = tool.Root.get_root_props()
obj = context.active_object
self.file = tool.Ifc.get()
element = tool.Ifc.get_entity(obj)
@@ -53,7 +54,7 @@ class EnableReassignClass(bpy.types.Operator):
declaration = schema.declaration_by_name(ifc_class)
for ifc_product in ifc_products:
if ifcopenshell.util.schema.is_a(declaration, ifc_product):
- context.scene.BIMRootProperties.ifc_product = ifc_product
+ rprops.ifc_product = ifc_product
break
else:
self.report({"ERROR"}, f"Couldn't find matching IFC product for the selected object: '{element}'.")
@@ -61,13 +62,13 @@ class EnableReassignClass(bpy.types.Operator):
return {"CANCELLED"}
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
- context.scene.BIMRootProperties.ifc_class = element.is_a()
- context.scene.BIMRootProperties.relating_class_object = None
+ rprops.ifc_class = element.is_a()
+ rprops.relating_class_object = None
if hasattr(element, "PredefinedType"):
if element.PredefinedType:
- context.scene.BIMRootProperties.ifc_predefined_type = element.PredefinedType
+ rprops.ifc_predefined_type = element.PredefinedType
userdefined_type = ifcopenshell.util.element.get_predefined_type(element)
- context.scene.BIMRootProperties.ifc_userdefined_type = userdefined_type or ""
+ rprops.ifc_userdefined_type = userdefined_type or ""
return {"FINISHED"}
@@ -94,9 +95,9 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
else:
objects = set(context.selected_objects + [context.active_object])
self.file = tool.Ifc.get()
- root_props = context.scene.BIMRootProperties
- ifc_product: str = root_props.ifc_product
- ifc_class: str = root_props.ifc_class
+ root_props = tool.Root.get_root_props()
+ ifc_product = root_props.ifc_product
+ ifc_class = root_props.ifc_class
type_ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class, self.file.schema)), None)
predefined_type = root_props.ifc_predefined_type
@@ -176,7 +177,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
ifc_representation_class: bpy.props.StringProperty()
def _execute(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
objects: list[bpy.types.Object] = []
if self.obj:
objects = [bpy.data.objects[self.obj]]
@@ -358,7 +359,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context, event):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
# For convenience, preselect OBJs if applicable
if props.ifc_product == "IfcFeatureElement":
if (obj := tool.Blender.get_active_object(is_selected=True)) and obj.type == "MESH":
@@ -376,7 +377,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def _execute(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
predefined_type = (
props.ifc_userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type
)
@@ -500,7 +501,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
else:
material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown")
if representation_template == "PROFILESET":
- profile_id = tool.Blender.get_enum_safe(context.scene.BIMRootProperties, "profile")
+ profile_id = tool.Blender.get_enum_safe(props, "profile")
if profile_id in ("-", None):
profile = next((p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName), None)
if profile is None:
@@ -589,7 +590,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.set_active_object(obj)
def draw(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
self.layout.use_property_split = True
self.layout.use_property_decorate = False
row = self.layout.row()
@@ -600,7 +601,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
if not self.ifc_product:
prop_with_search(self.layout, props, "ifc_product", text="Definition", should_click_ok=True)
prop_with_search(self.layout, props, "ifc_class", should_click_ok=True)
- ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context)
+ ifc_predefined_types = root_prop.get_ifc_predefined_types(props, context)
if ifc_predefined_types:
prop_with_search(self.layout, props, "ifc_predefined_type", should_click_ok=True)
if props.ifc_predefined_type == "USERDEFINED":
diff --git a/src/bonsai/bonsai/bim/module/root/prop.py b/src/bonsai/bonsai/bim/module/root/prop.py
index 5243d95c03..4c358dfde1 100644
--- a/src/bonsai/bonsai/bim/module/root/prop.py
+++ b/src/bonsai/bonsai/bim/module/root/prop.py
@@ -34,28 +34,27 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
+from typing import TYPE_CHECKING, Union
-def get_ifc_predefined_types(self, context):
+def get_ifc_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_predefined_types"]
-def get_representation_template(self, context):
+def get_representation_template(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["representation_template"]
-def refresh_classes(self, context):
- old_class = context.scene.BIMRootProperties.ifc_class
- old_predefined_type = (
- context.scene.BIMRootProperties.ifc_predefined_type if get_ifc_predefined_types(self, context) else ""
- )
+def refresh_classes(self: "BIMRootProperties", context: bpy.types.Context) -> None:
+ old_class = self.ifc_class
+ old_predefined_type = self.ifc_predefined_type if get_ifc_predefined_types(self, context) else ""
enum = get_ifc_classes(self, context)
- context.scene.BIMRootProperties.ifc_class = enum[0][0]
+ self.ifc_class = enum[0][0]
IfcClassData.load()
if self.ifc_product == "IfcFeatureElement":
@@ -81,48 +80,44 @@ def refresh_classes(self, context):
self.ifc_predefined_type = old_predefined_type
-def refresh_predefined_types(self, context):
+def refresh_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> None:
IfcClassData.load()
enum = get_ifc_predefined_types(self, context)
if enum:
- context.scene.BIMRootProperties.ifc_predefined_type = enum[0][0]
+ self.ifc_predefined_type = enum[0][0]
-def update_class_enum(self, context):
- self.ifc_class = self.ifc_class_filter_enum
-
-
-def get_ifc_products(self, context):
+def get_ifc_products(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_products"]
-def get_ifc_classes(self, context):
+def get_ifc_classes(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_classes"]
-def get_ifc_classes_suggestions():
+def get_ifc_classes_suggestions(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_classes_suggestions"]
-def get_contexts(self, context):
+def get_contexts(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["contexts"]
-def get_profile(self, context):
+def get_profile(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["profile"]
-def update_relating_class_from_object(self, context):
+def update_relating_class_from_object(self: "BIMRootProperties", context: bpy.types.Context) -> None:
if self.relating_class_object is None:
return
element = tool.Ifc.get_entity(self.relating_class_object)
@@ -139,7 +134,7 @@ def update_relating_class_from_object(self, context):
bpy.ops.bim.reassign_class()
-def is_object_class_applicable(self, obj):
+def is_object_class_applicable(self: "BIMRootProperties", obj: bpy.types.Object) -> bool:
element = tool.Ifc.get_entity(obj)
if not element:
return False
@@ -149,11 +144,11 @@ def is_object_class_applicable(self, obj):
return element.is_a("IfcTypeObject") == active_element.is_a("IfcTypeObject")
-def poll_representation_obj(self, obj):
+def poll_representation_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool:
return obj.type == "MESH" and obj.data.polygons
-def poll_featured_obj(self, obj):
+def poll_featured_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool:
return tool.Ifc.get_entity(obj)
@@ -192,3 +187,16 @@ class BIMRootProperties(PropertyGroup):
getter_enum_suggestions = {
"ifc_class": get_ifc_classes_suggestions,
}
+
+ if TYPE_CHECKING:
+ contexts: str
+ description: str
+ ifc_product: str
+ ifc_class: str
+ ifc_predefined_type: str
+ ifc_userdefined_type: str
+ featured_obj: Union[bpy.types.Object, None]
+ representation_template: str
+ representation_obj: Union[bpy.types.Object, None]
+ profile: str
+ relating_class_object: Union[bpy.types.Object, None]
diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py
index b1a2b52637..6985bf832b 100644
--- a/src/bonsai/bonsai/bim/module/root/ui.py
+++ b/src/bonsai/bonsai/bim/module/root/ui.py
@@ -44,6 +44,7 @@ class BIM_PT_class(Panel):
if not IfcClassData.is_loaded:
IfcClassData.load()
props = context.active_object.BIMObjectProperties
+ rprops = tool.Root.get_root_props()
if props.ifc_definition_id:
if not IfcClassData.data["has_entity"]:
row = self.layout.row(align=True)
@@ -58,10 +59,10 @@ class BIM_PT_class(Panel):
row.operator("bim.disable_reassign_class", icon="CANCEL", text="")
self.draw_class_dropdowns(
context,
- root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context),
+ root_prop.get_ifc_predefined_types(rprops, context),
is_reassigning_class=True,
)
- self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN")
+ self.layout.prop(rprops, "relating_class_object", icon="COPYDOWN")
else:
row = self.layout.row(align=True)
row.label(
@@ -78,16 +79,16 @@ class BIM_PT_class(Panel):
if AuthoringData.data["is_representation_item_active"]:
return
- ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context)
+ ifc_predefined_types = root_prop.get_ifc_predefined_types(rprops, context)
self.draw_class_dropdowns(context, ifc_predefined_types)
row = self.layout.row(align=True)
op = row.operator("bim.assign_class")
- op.ifc_class = context.scene.BIMRootProperties.ifc_class
- op.predefined_type = context.scene.BIMRootProperties.ifc_predefined_type if ifc_predefined_types else ""
- op.userdefined_type = context.scene.BIMRootProperties.ifc_userdefined_type
+ op.ifc_class = rprops.ifc_class
+ op.predefined_type = rprops.ifc_predefined_type if ifc_predefined_types else ""
+ op.userdefined_type = rprops.ifc_userdefined_type
def draw_class_dropdowns(self, context, ifc_predefined_types, is_reassigning_class=False):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
layout = self.layout
prop_with_search(layout, props, "ifc_product")
prop_with_search(layout, props, "ifc_class")
diff --git a/src/bonsai/bonsai/bim/module/spatial/data.py b/src/bonsai/bonsai/bim/module/spatial/data.py
index eb8e6585b4..b3e863a329 100644
--- a/src/bonsai/bonsai/bim/module/spatial/data.py
+++ b/src/bonsai/bonsai/bim/module/spatial/data.py
@@ -58,7 +58,7 @@ class SpatialData:
@classmethod
def default_container(cls) -> str | None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if props.default_container:
try:
return tool.Ifc.get().by_id(props.default_container).Name
@@ -102,7 +102,7 @@ class SpatialDecompositionData:
@classmethod
def default_container(cls) -> str | None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if props.default_container:
try:
return tool.Ifc.get().by_id(props.default_container).Name
@@ -112,7 +112,7 @@ class SpatialDecompositionData:
@classmethod
def subelement_class(cls) -> list[tuple[str, str, str]]:
results = []
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if not (container := props.active_container):
return results
container_class = tool.Ifc.get().by_id(container.ifc_definition_id).is_a()
diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py
index b977210805..a7c1059fc7 100644
--- a/src/bonsai/bonsai/bim/module/spatial/decorator.py
+++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py
@@ -66,7 +66,8 @@ class GridDecorator:
blf.size(font_id, 12)
blf.enable(font_id, blf.SHADOW)
- for axis in context.scene.BIMGridProperties.grid_axes:
+ grid_props = tool.Spatial.get_grid_props()
+ for axis in grid_props.grid_axes:
if not (obj := axis.obj) or obj.hide_get() == True:
continue
if obj.select_get() and context.mode != "OBJECT":
@@ -120,7 +121,8 @@ class GridDecorator:
selected_edges = []
unselected_verts = []
unselected_edges = []
- for axis in context.scene.BIMGridProperties.grid_axes:
+ grid_props = tool.Spatial.get_grid_props()
+ for axis in grid_props.grid_axes:
if (obj := axis.obj) and obj.hide_get() == False:
if obj.select_get():
if context.mode != "OBJECT":
diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py
index 9caa736cbe..9d134729f3 100644
--- a/src/bonsai/bonsai/bim/module/spatial/operator.py
+++ b/src/bonsai/bonsai/bim/module/spatial/operator.py
@@ -96,7 +96,7 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
container = tool.Ifc.get().by_id(self.container)
elif (
(obj := tool.Blender.get_active_object())
- and (props := obj.BIMObjectSpatialProperties)
+ and (props := tool.Spatial.get_object_spatial_props(obj))
and (container_obj := props.container_obj)
and (container := tool.Ifc.get_entity(container_obj))
):
diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py
index 1017d8f069..910bb2e28c 100644
--- a/src/bonsai/bonsai/bim/module/spatial/prop.py
+++ b/src/bonsai/bonsai/bim/module/spatial/prop.py
@@ -36,15 +36,18 @@ import bonsai.core.geometry
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
+from typing import TYPE_CHECKING, Union, Literal
-def get_subelement_class(self, context):
+def get_subelement_class(
+ self: "BIMSpatialDecompositionProperties", context: bpy.types.Context
+) -> list[tuple[str, str, str]]:
if not SpatialDecompositionData.is_loaded:
SpatialDecompositionData.load()
return SpatialDecompositionData.data["subelement_class"]
-def update_elevation(self, context):
+def update_elevation(self: "BIMContainer", context: bpy.types.Context) -> None:
try:
elevation = float(self.elevation)
if self.elevation != str(elevation):
@@ -91,7 +94,7 @@ def update_element_mode(self: "BIMSpatialDecompositionProperties", context: bpy.
tool.Spatial.load_contained_elements()
-def update_grid_is_locked(self, context):
+def update_grid_is_locked(self: "BIMGridProperties", context: bpy.types.Context) -> None:
if not tool.Ifc.get():
return
if tool.Ifc.get().schema in ("IFC2X3", "IFC4"):
@@ -108,7 +111,7 @@ def update_grid_is_locked(self, context):
bonsai.bim.handler.refresh_ui_data()
-def update_spatial_is_locked(self, context):
+def update_spatial_is_locked(self: "BIMSpatialDecompositionProperties", context: bpy.types.Context) -> None:
if not tool.Ifc.get():
return
if tool.Ifc.get().schema == "IFC2X3":
@@ -150,6 +153,10 @@ class BIMObjectSpatialProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
container_obj: PointerProperty(type=bpy.types.Object, name="Container", poll=poll_container_obj)
+ if TYPE_CHECKING:
+ is_editing: bool
+ container_obj: Union[bpy.types.Object, None]
+
class BIMContainer(PropertyGroup):
name: StringProperty(name="Name", update=update_name)
@@ -162,6 +169,16 @@ class BIMContainer(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded")
ifc_definition_id: IntProperty(name="IFC Definition ID")
+ if TYPE_CHECKING:
+ ifc_class: str
+ description: str
+ long_name: str
+ elevation: str
+ level_index: int
+ has_children: bool
+ is_expanded: bool
+ ifc_definition_id: int
+
class Element(PropertyGroup):
name: StringProperty(name="Name")
@@ -185,6 +202,16 @@ class Element(PropertyGroup):
),
)
+ if TYPE_CHECKING:
+ ifc_class: str
+ identification: str
+ ifc_definition_id: int
+ level: int
+ has_children: bool
+ total: int
+ is_expanded: bool
+ type: Literal["CLASS", "TYPE", "CLASSIFICATION", "OCCURRENCE"]
+
class BIMSpatialDecompositionProperties(PropertyGroup):
is_locked: BoolProperty(
@@ -223,6 +250,23 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
name="Should Include Children", default=True, update=update_should_include_children
)
+ if TYPE_CHECKING:
+ is_locked: bool
+ is_visible: bool
+ container_filter: str
+ containers: bpy.types.bpy_prop_collection_idprop[BIMContainer]
+ contracted_containers: str
+ active_container_index: int
+ element_filter: str
+ elements: bpy.types.bpy_prop_collection_idprop[Element]
+ expanded_elements: str
+ active_element_index: int
+ total_elements: int
+ element_mode: Literal["TYPE", "DECOMPOSITION", "CLASSIFICATION"]
+ subelement_class: str
+ default_container: int
+ should_include_children: bool
+
@property
def active_container(self):
if self.containers and self.active_container_index < len(self.containers):
@@ -248,3 +292,8 @@ class BIMGridProperties(PropertyGroup):
update=update_grid_is_visible,
)
grid_axes: CollectionProperty(name="Grid Axes", type=ObjProperty)
+
+ if TYPE_CHECKING:
+ is_locked: bool
+ is_visible: bool
+ grid_axes: bpy.types.bpy_prop_collection_idprop[ObjProperty]
diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py
index 0569504e25..da20a63d7c 100644
--- a/src/bonsai/bonsai/bim/module/spatial/ui.py
+++ b/src/bonsai/bonsai/bim/module/spatial/ui.py
@@ -16,10 +16,15 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.module.spatial.data import SpatialData, SpatialDecompositionData
import bonsai.tool as tool
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from bonsai.bim.module.spatial.prop import BIMSpatialDecompositionProperties, BIMContainer, Element
class BIM_PT_spatial(Panel):
@@ -40,7 +45,9 @@ class BIM_PT_spatial(Panel):
if not SpatialData.is_loaded:
SpatialData.load()
- osprops = context.active_object.BIMObjectSpatialProperties
+ obj = context.active_object
+ assert obj
+ osprops = tool.Spatial.get_object_spatial_props(obj)
if osprops.is_editing:
if SpatialData.data["default_container"]:
@@ -103,7 +110,7 @@ class BIM_PT_spatial_decomposition(Panel):
return tool.Ifc.get()
def draw_header(self, context):
- props = context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
@@ -114,7 +121,7 @@ class BIM_PT_spatial_decomposition(Panel):
def draw(self, context):
if not SpatialDecompositionData.is_loaded:
SpatialDecompositionData.load()
- self.props = context.scene.BIMSpatialDecompositionProperties
+ self.props = tool.Spatial.get_spatial_props()
if SpatialDecompositionData.data["default_container"]:
row = self.layout.row(align=True)
@@ -233,7 +240,7 @@ class BIM_PT_grids(Panel):
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
def draw_header(self, context):
- props = context.scene.BIMGridProperties
+ props = tool.Spatial.get_grid_props()
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
@@ -243,25 +250,36 @@ class BIM_PT_grids(Panel):
class BIM_UL_containers_manager(UIList):
+ icon_by_class = {
+ "IfcProject": "FILE",
+ "IfcSite": "WORLD",
+ "IfcBuilding": "HOME",
+ "IfcBuildingStorey": "LINENUMBERS_OFF",
+ "IfcSpace": "ANTIALIASED",
+ "IfcFacilityPart": "MOD_FLUID",
+ "IfcBridgePart": "MOD_FLUID",
+ "IfcFacilityPartCommon": "MOD_FLUID",
+ "IfcMarinePart": "MOD_FLUID",
+ "IfcRailwayPart": "MOD_FLUID",
+ "IfcRoadPart": "MOD_FLUID",
+ }
+
def __init__(self):
self.use_filter_show = True
- def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
+ def draw_item(
+ self,
+ context,
+ layout: bpy.types.UILayout,
+ data: BIMSpatialDecompositionProperties,
+ item: BIMContainer,
+ icon,
+ active_data,
+ active_propname,
+ ):
if item:
row = layout.row(align=True)
- icon = {
- "IfcProject": "FILE",
- "IfcSite": "WORLD",
- "IfcBuilding": "HOME",
- "IfcBuildingStorey": "LINENUMBERS_OFF",
- "IfcSpace": "ANTIALIASED",
- "IfcFacilityPart": "MOD_FLUID",
- "IfcBridgePart": "MOD_FLUID",
- "IfcFacilityPartCommon": "MOD_FLUID",
- "IfcMarinePart": "MOD_FLUID",
- "IfcRailwayPart": "MOD_FLUID",
- "IfcRoadPart": "MOD_FLUID",
- }.get(item.ifc_class, "META_PLANE")
+ icon = self.icon_by_class.get(item.ifc_class, "META_PLANE")
split = row.split(factor=0.85)
if item.long_name:
split2 = split.split(factor=0.7)
@@ -275,7 +293,7 @@ class BIM_UL_containers_manager(UIList):
row.prop(item, "name", emboss=False, text="", icon=icon)
split.prop(item, "elevation", emboss=False, text="")
- def draw_hierarchy(self, row, item):
+ def draw_hierarchy(self, row: bpy.types.UILayout, item: BIMContainer) -> None:
if item.level_index:
for i in range(0, item.level_index - 1):
row.label(text="", icon="BLANK1")
@@ -293,11 +311,12 @@ class BIM_UL_containers_manager(UIList):
def draw_filter(self, context, layout):
row = layout.row()
- row.prop(context.scene.BIMSpatialDecompositionProperties, "container_filter", text="", icon="VIEWZOOM")
+ props = tool.Spatial.get_spatial_props()
+ row.prop(props, "container_filter", text="", icon="VIEWZOOM")
- def filter_items(self, context, data, propname):
+ def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str):
items = getattr(data, propname)
- filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter.lower()
+ filter_name = data.container_filter.lower()
filter_flags = [self.bitflag_filter_item] * len(items)
for idx, item in enumerate(items):
@@ -313,7 +332,7 @@ class BIM_UL_containers_manager(UIList):
return filter_flags, []
items = getattr(data, propname)
- filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter
+ filter_name = data.container_filter
filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name")
return filtered, []
@@ -326,7 +345,18 @@ class BIM_UL_elements(UIList):
icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT"
row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index
- def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag):
+ def draw_item(
+ self,
+ context,
+ layout: bpy.types.UILayout,
+ data: BIMSpatialDecompositionProperties,
+ item: Element,
+ icon,
+ active_data,
+ active_propname,
+ index,
+ fit_flag,
+ ):
if item:
row = layout.row(align=True)
for _ in range(item.level):
@@ -341,11 +371,12 @@ class BIM_UL_elements(UIList):
def draw_filter(self, context, layout):
row = layout.row()
- row.prop(context.scene.BIMSpatialDecompositionProperties, "element_filter", text="", icon="VIEWZOOM")
+ props = tool.Spatial.get_spatial_props()
+ row.prop(props, "element_filter", text="", icon="VIEWZOOM")
- def filter_items(self, context, data, propname):
+ def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str):
items = getattr(data, propname)
- filter_name = context.scene.BIMSpatialDecompositionProperties.element_filter
+ filter_name = data.element_filter
filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name")
return filtered, []
diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py
index 93c6ee7420..446b51dd7b 100644
--- a/src/bonsai/bonsai/tool/geometry.py
+++ b/src/bonsai/bonsai/tool/geometry.py
@@ -147,11 +147,11 @@ class Geometry(bonsai.core.tool.Geometry):
def is_locked(cls, element: ifcopenshell.entity_instance) -> bool:
if element.is_a("IfcProject"):
return True
- elif tool.Root.is_spatial_element(element) and bpy.context.scene.BIMSpatialDecompositionProperties.is_locked:
+ elif tool.Root.is_spatial_element(element) and tool.Spatial.get_spatial_props().is_locked:
return True
elif (
element.is_a("IfcPositioningElement") or element.is_a("IfcGrid") or element.is_a("IfcGridAxis")
- ) and bpy.context.scene.BIMGridProperties.is_locked:
+ ) and tool.Spatial.get_grid_props().is_locked:
return True
return False
diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py
index 3f4d77f0d7..4741e16a59 100644
--- a/src/bonsai/bonsai/tool/project.py
+++ b/src/bonsai/bonsai/tool/project.py
@@ -137,13 +137,15 @@ class Project(bonsai.core.tool.Project):
@classmethod
def set_context(cls, context):
bonsai.bim.handler.refresh_ui_data()
- bpy.context.scene.BIMRootProperties.contexts = str(context.id())
+ rprops = tool.Root.get_root_props()
+ rprops.contexts = str(context.id())
@classmethod
def set_default_context(cls):
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
if context:
- bpy.context.scene.BIMRootProperties.contexts = str(context.id())
+ rprops = tool.Root.get_root_props()
+ rprops.contexts = str(context.id())
@classmethod
def set_default_modeling_dimensions(cls):
diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py
index c665e1226c..392d53e2fe 100644
--- a/src/bonsai/bonsai/tool/root.py
+++ b/src/bonsai/bonsai/tool/root.py
@@ -16,6 +16,7 @@
# 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 ifcopenshell
import ifcopenshell.api
@@ -27,12 +28,19 @@ import bonsai.core.tool
import bonsai.core.aggregate
import bonsai.core.geometry
import bonsai.tool as tool
-from typing import Union, Optional, Any, Literal
+from typing import Union, Optional, Any, Literal, TYPE_CHECKING
from bonsai.bim.module.spatial.decorator import GridDecorator
from bonsai.bim.module.geometry.decorator import ItemDecorator
+if TYPE_CHECKING:
+ from bonsai.bim.module.root.prop import BIMRootProperties
+
class Root(bonsai.core.tool.Root):
+ @classmethod
+ def get_root_props(cls) -> BIMRootProperties:
+ return bpy.context.scene.BIMRootProperties
+
@classmethod
def add_tracked_opening(cls, obj: bpy.types.Object, opening_type: Literal["OPENING", "BOOLEAN"]) -> None:
"""Add tracked opening or boolean object."""
@@ -112,7 +120,7 @@ class Root(bonsai.core.tool.Root):
@classmethod
def get_default_container(cls) -> Optional[ifcopenshell.entity_instance]:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if container := props.default_container:
try:
return tool.Ifc.get().by_id(container)
@@ -233,7 +241,7 @@ class Root(bonsai.core.tool.Root):
@classmethod
def reload_grid_decorator(cls) -> None:
- axes = bpy.context.scene.BIMGridProperties.grid_axes
+ axes = tool.Spatial.get_grid_props().grid_axes
axes.clear()
for axis in tool.Ifc.get().by_type("IfcGridAxis"):
if obj := tool.Ifc.get_object(axis):
diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py
index 26ae9db532..2d400229c5 100644
--- a/src/bonsai/bonsai/tool/spatial.py
+++ b/src/bonsai/bonsai/tool/spatial.py
@@ -41,12 +41,31 @@ import json
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon
-from typing import Generator, Optional, Union, Literal, List, Any, Iterable
+from typing import Generator, Optional, Union, Literal, List, Any, Iterable, TYPE_CHECKING
from collections import defaultdict
from natsort import natsorted
+if TYPE_CHECKING:
+ from bonsai.bim.module.spatial.prop import (
+ BIMGridProperties,
+ BIMSpatialDecompositionProperties,
+ BIMObjectSpatialProperties,
+ )
+
class Spatial(bonsai.core.tool.Spatial):
+ @classmethod
+ def get_spatial_props(cls) -> BIMSpatialDecompositionProperties:
+ return bpy.context.scene.BIMSpatialDecompositionProperties
+
+ @classmethod
+ def get_object_spatial_props(cls, obj: bpy.types.Object) -> BIMObjectSpatialProperties:
+ return obj.BIMObjectSpatialProperties
+
+ @classmethod
+ def get_grid_props(cls) -> BIMGridProperties:
+ return bpy.context.scene.BIMGridProperties
+
@classmethod
def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool:
if not (element := tool.Ifc.get_entity(element_obj)):
@@ -81,7 +100,8 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
- obj.BIMObjectSpatialProperties.is_editing = False
+ props = cls.get_object_spatial_props(obj)
+ props.is_editing = False
@classmethod
def duplicate_object_and_data(cls, obj: bpy.types.Object) -> bpy.types.Object:
@@ -92,8 +112,9 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def enable_editing(cls, obj: bpy.types.Object) -> None:
- obj.BIMObjectSpatialProperties.is_editing = True
- obj.BIMObjectSpatialProperties.relating_container_object = None
+ props = cls.get_object_spatial_props(obj)
+ props.is_editing = True
+ props.relating_container_object = None
@classmethod
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
@@ -207,7 +228,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_container_elements_grouped_by_classification(cls, container: ifcopenshell.entity_instance) -> dict:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
results = {}
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
@@ -254,7 +275,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_container_elements_grouped_by_type(cls, container: ifcopenshell.entity_instance) -> dict:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
results: defaultdict[str, dict[int, Any]] = defaultdict(dict)
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
@@ -280,7 +301,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements(cls) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
props.elements.clear()
if not (container := props.active_container):
return
@@ -295,7 +316,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
results = cls.get_container_elements_grouped_by_type(container)
expanded_elements = json.loads(props.expanded_elements)
@@ -350,7 +371,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements_by_decomposition(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements = json.loads(props.expanded_elements)
expanded_ifc_ids = expanded_elements.get("IFC_ID", [])
@@ -381,7 +402,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements_by_classification(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements = json.loads(props.expanded_elements)
expanded_classifications = expanded_elements.get("CLASSIFICATION", [])
expanded_classifications_r = expanded_elements.get("CLASSIFICATION_R", [])
@@ -445,7 +466,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def import_spatial_decomposition(cls) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
previous_container_index = props.active_container_index
props.containers.clear()
cls.contracted_containers = json.loads(props.contracted_containers)
@@ -456,7 +477,7 @@ class Spatial(bonsai.core.tool.Spatial):
def import_spatial_element(cls, element: ifcopenshell.entity_instance, level_index: int) -> None:
if not element.is_a("IfcProject") and not tool.Root.is_spatial_element(element):
return
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
new = props.containers.add()
new.ifc_class = element.is_a()
new["name"] = element.Name or "Unnamed"
@@ -485,28 +506,28 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_active_container(cls) -> Union[ifcopenshell.entity_instance, None]:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
if props.active_container_index < len(props.containers):
container = tool.Ifc.get().by_id(props.containers[props.active_container_index].ifc_definition_id)
return container
@classmethod
def contract_container(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.append(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def expand_container(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.remove(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def toggle_container_element(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
if props.element_mode == "TYPE":
cls.toggle_container_element_by_type(element_index, is_recursive)
elif props.element_mode == "DECOMPOSITION":
@@ -516,7 +537,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements)
element = props.elements[element_index]
@@ -565,7 +586,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def toggle_container_element_by_decomposition(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
element = props.elements[element_index]
expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements)
expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault("IFC_ID", [])
@@ -598,7 +619,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def toggle_container_element_by_classification(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements: dict[str, list[str]] = json.loads(props.expanded_elements)
expanded_elements_list: list[str] = expanded_elements.setdefault("CLASSIFICATION", [])
@@ -679,7 +700,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_boundary_lines_from_context_visible_objects(cls) -> list[shapely.LineString]:
- props = props = tool.Model.get_model_props()
+ props = tool.Model.get_model_props()
calculation_rl = props.rl3
container = tool.Root.get_default_container()
container_obj = tool.Ifc.get_object(container)
@@ -1154,8 +1175,8 @@ class Spatial(bonsai.core.tool.Spatial):
def set_default_container(cls, container: ifcopenshell.entity_instance) -> None:
from bonsai.bim.module.spatial.data import SpatialDecompositionData
- assert bpy.context
- bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id()
+ props = cls.get_spatial_props()
+ props.default_container = container.id()
SpatialDecompositionData.data["default_container"] = SpatialDecompositionData.default_container()
project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
@@ -1212,16 +1233,16 @@ class Spatial(bonsai.core.tool.Spatial):
def set_target_container_as_default(cls) -> None:
if (
(container := tool.Root.get_default_container())
- and (obj := tool.Ifc.get_object(container))
- and bpy.context.active_object
+ and (container_obj := tool.Ifc.get_object(container))
+ and (obj := bpy.context.active_object)
):
- props = bpy.context.active_object.BIMObjectSpatialProperties
- props.container_obj = obj
+ props = cls.get_object_spatial_props(obj)
+ props.container_obj = container_obj
@classmethod
def get_filtered_elements(cls, should_filter: bool = True) -> Iterable[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
container = ifc_file.by_id(props.active_container.ifc_definition_id)
element_filter = props.element_filter
active_element = props.active_element
diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py
index a77d90ab92..f01399e88c 100644
--- a/src/bonsai/test/tool/test_model.py
+++ b/src/bonsai/test/tool/test_model.py
@@ -383,7 +383,8 @@ class TestUsingArrays(NewFile):
bpy.ops.mesh.primitive_cube_add()
obj = bpy.context.active_object
- bpy.context.scene.BIMRootProperties.ifc_product = "IfcElement"
+ rprops = tool.Root.get_root_props()
+ rprops.ifc_product = "IfcElement"
bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="")
bpy.ops.bim.add_array()
@@ -520,7 +521,7 @@ class TestApplyIfcMaterialChanges(NewFile):
bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id)
with_opening = bpy.context.active_object
with_opening.name = "With Opening"
- props = bpy.context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
props.representation_obj = with_opening
bpy.ops.bim.add_element(ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement")
diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py
index a5f94dbe4b..a87b00f753 100644
--- a/src/bonsai/test/tool/test_project.py
+++ b/src/bonsai/test/tool/test_project.py
@@ -97,7 +97,8 @@ class TestSetContext(NewFile):
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
subject.set_context(context)
- assert bpy.context.scene.BIMRootProperties.contexts == str(context.id())
+ rprops = tool.Root.get_root_props()
+ assert rprops.contexts == str(context.id())
class TestSetDefaultContext(NewFile):
@@ -115,7 +116,8 @@ class TestSetDefaultContext(NewFile):
target_view="MODEL_VIEW",
)
subject.set_default_context()
- assert bpy.context.scene.BIMRootProperties.contexts == str(body.id())
+ rprops = tool.Root.get_root_props()
+ assert rprops.contexts == str(body.id())
class TestSetDefaultModelingDimensions(NewFile):
diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py
index e87eb312a7..2c89a4e5fa 100644
--- a/src/bonsai/test/tool/test_root.py
+++ b/src/bonsai/test/tool/test_root.py
@@ -191,8 +191,10 @@ class TestReassignClass(NewFile):
slabs = [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSlab")]
assert len(slabs) == 3
tool.Blender.set_objects_selection(context, slabs[0], (slabs[1],))
- context.scene.BIMRootProperties.ifc_product = "IfcElement"
- context.scene.BIMRootProperties.ifc_class = "IfcWall"
+
+ props = tool.Root.get_root_props()
+ props.ifc_product = "IfcElement"
+ props.ifc_class = "IfcWall"
bpy.ops.bim.reassign_class()
assert len(ifc_file.by_type("IfcWall")) == 3
diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py
index 2985b458d1..9bcc84c6dc 100644
--- a/src/bonsai/test/tool/test_spatial.py
+++ b/src/bonsai/test/tool/test_spatial.py
@@ -129,7 +129,8 @@ class TestDisableEditing(NewFile):
obj = bpy.data.objects.new("Object", None)
subject.enable_editing(obj)
subject.disable_editing(obj)
- assert obj.BIMObjectSpatialProperties.is_editing is False
+ props = tool.Spatial.get_object_spatial_props(obj)
+ assert props.is_editing is False
class TestDuplicateObjectAndData(NewFile):
@@ -148,7 +149,8 @@ class TestEnableEditing(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
subject.enable_editing(obj)
- assert obj.BIMObjectSpatialProperties.is_editing is True
+ props = tool.Spatial.get_object_spatial_props(obj)
+ assert props.is_editing is True
class TestGetContainer(NewFile):
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py
index c9ef89455e..48ac816f18 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py
@@ -28,11 +28,8 @@ def remove_cost_item_quantity(
removed.
:param cost_item: The IfcCostItem that the quantity is assigned to
- :type cost_item: ifcopenshell.entity_instance
:param physical_quantity: The IfcPhysicalQuantity to remove
- :type physical_quantity: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -46,11 +43,9 @@ def remove_cost_item_quantity(
ifcopenshell.api.cost.remove_cost_item(model,
cost_item=item, physical_quantity=quantity)
"""
- settings = {"cost_item": cost_item, "physical_quantity": physical_quantity}
-
- if len(file.get_inverse(settings["physical_quantity"])) == 1:
- file.remove(settings["physical_quantity"])
+ if len(file.get_inverse(physical_quantity)) == 1:
+ file.remove(physical_quantity)
return
- quantities = list(settings["cost_item"].CostQuantities or [])
- quantities.remove(settings["physical_quantity"])
- settings["cost_item"].CostQuantities = quantities
+ quantities = list(cost_item.CostQuantities or [])
+ quantities.remove(physical_quantity)
+ cost_item.CostQuantities = quantities
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py
index 2c7745d7f9..a99417f4df 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py
@@ -28,11 +28,8 @@ def remove_cost_value(
:param parent: The IfcCostItem, IfcConstructionResource, or IfcCostValue
that the IfcCostValue is assigned to.
- :type parent: ifcopenshell.entity_instance
:param cost_value: The IfcCostValue that you want to remove
- :type parent: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -48,20 +45,18 @@ def remove_cost_value(
ifcopenshell.api.cost.remove_cost_value(model, parent=item, cost_value=value)
"""
- settings = {"parent": parent, "cost_value": cost_value}
-
- if len(file.get_inverse(settings["cost_value"])) == 1:
- file.remove(settings["cost_value"])
+ if len(file.get_inverse(cost_value)) == 1:
+ file.remove(cost_value)
# TODO deep purge
- elif settings["parent"].is_a("IfcCostItem"):
- values = list(settings["parent"].CostValues)
- values.remove(settings["cost_value"])
- settings["parent"].CostValues = values if values else None
- elif settings["parent"].is_a("IfcConstructionResource"):
- values = list(settings["parent"].BaseCosts)
- values.remove(settings["cost_value"])
- settings["parent"].BaseCosts = values if values else None
- elif settings["parent"].is_a("IfcCostValue"):
- components = list(settings["parent"].Components)
- components.remove(settings["cost_value"])
- settings["parent"].Components = components if components else None
+ elif parent.is_a("IfcCostItem"):
+ values = list(parent.CostValues)
+ values.remove(cost_value)
+ parent.CostValues = values if values else None
+ elif parent.is_a("IfcConstructionResource"):
+ values = list(parent.BaseCosts)
+ values.remove(cost_value)
+ parent.BaseCosts = values if values else None
+ elif parent.is_a("IfcCostValue"):
+ components = list(parent.Components)
+ components.remove(cost_value)
+ parent.Components = components if components else None
diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py
index 8239ab9614..c47165643f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py
@@ -23,9 +23,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance
"""Removes a grid axis from a grid
:param axis: The IfcGridAxis you want to remove.
- :type axis: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py
index 24fa93487b..a749ad11c4 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py
@@ -92,19 +92,17 @@ def assign_profile(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"material_profile": material_profile, "profile": profile}
- return usecase.execute()
+ return usecase.execute(material_profile, profile)
class Usecase:
file: ifcopenshell.file
- settings: dict[str, Any]
- def execute(self) -> None:
+ def execute(self, material_profile: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None:
# TODO: handle composite profiles
- old_profile = self.settings["material_profile"].Profile
- self.settings["material_profile"].Profile = self.settings["profile"]
- for profile_set in self.settings["material_profile"].ToMaterialProfileSet:
+ old_profile = material_profile.Profile
+ material_profile.Profile = profile
+ for profile_set in material_profile.ToMaterialProfileSet:
for inverse in self.file.get_inverse(profile_set):
if not inverse.is_a("IfcMaterialProfileSetUsage"):
continue
@@ -113,20 +111,20 @@ class Usecase:
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
- self.change_profile(element)
+ self.change_profile(element, profile)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
- self.change_profile(element)
+ self.change_profile(element, profile)
if old_profile and len(self.file.get_inverse(old_profile)) == 0:
# TODO: check remove deep
self.file.remove(old_profile)
- def change_profile(self, element: ifcopenshell.entity_instance) -> None:
+ def change_profile(self, element: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
for subelement in self.file.traverse(representation):
if subelement.is_a("IfcSweptAreaSolid"):
- subelement.SweptArea = self.settings["profile"]
+ subelement.SweptArea = profile
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py
index fbc25adcbe..de1efef20d 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py
@@ -27,9 +27,7 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti
removed.
:param organisation: The IfcOrganization to remove
- :type organisation: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -39,31 +37,29 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti
identification="AWB", name="Architects Without Ballpens")
ifcopenshell.api.owner.remove_organisation(model, organisation=organisation)
"""
- settings = {"organisation": organisation}
-
- for role in settings["organisation"].Roles or []:
+ for role in organisation.Roles or []:
if len(file.get_inverse(role)) == 1:
ifcopenshell.api.owner.remove_role(file, role=role)
- for address in settings["organisation"].Addresses or []:
+ for address in organisation.Addresses or []:
if len(file.get_inverse(address)) == 1:
ifcopenshell.api.owner.remove_address(file, address=address)
- for inverse in file.get_inverse(settings["organisation"]):
+ for inverse in file.get_inverse(organisation):
if inverse.is_a("IfcOrganizationRelationship"):
- if inverse.RelatingOrganization == settings["organisation"]:
+ if inverse.RelatingOrganization == organisation:
file.remove(inverse)
- elif inverse.RelatedOrganizations == (settings["organisation"],):
+ elif inverse.RelatedOrganizations == (organisation,):
file.remove(inverse)
elif inverse.is_a("IfcDocumentInformation"):
- if inverse.Editors == (settings["organisation"],):
+ if inverse.Editors == (organisation,):
inverse.Editors = None
elif inverse.is_a("IfcPersonAndOrganization"):
ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse)
elif inverse.is_a("IfcActor"):
ifcopenshell.api.root.remove_product(file, product=inverse)
elif inverse.is_a("IfcResourceLevelRelationship") and not inverse.is_a("IfcOrganizationRelationship"):
- if inverse.RelatedResourceObjects == (settings["organisation"],):
+ if inverse.RelatedResourceObjects == (organisation,):
file.remove(inverse)
elif inverse.is_a("IfcApplication"):
ifcopenshell.api.owner.remove_application(file, application=inverse)
- file.remove(settings["organisation"])
+ file.remove(organisation)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py
index a4f4d64f1f..07508a977e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py
@@ -29,9 +29,7 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance)
the only responsile person for them.
:param person: The IfcPerson to remove
- :type person: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -41,31 +39,30 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance)
identification="bobthebuilder", family_name="Thebuilder", given_name="Bob")
ifcopenshell.api.owner.remove_person(model, person=person)
"""
- settings = {"person": person}
- for role in settings["person"].Roles or []:
+ for role in person.Roles or []:
if len(file.get_inverse(role)) == 1:
ifcopenshell.api.owner.remove_role(file, role=role)
- for address in settings["person"].Addresses or []:
+ for address in person.Addresses or []:
if len(file.get_inverse(address)) == 1:
ifcopenshell.api.owner.remove_address(file, address=address)
- for inverse in file.get_inverse(settings["person"]):
+ for inverse in file.get_inverse(person):
if inverse.is_a("IfcWorkControl"):
- if inverse.Creators == (settings["person"],):
+ if inverse.Creators == (person,):
inverse.Creators = None
elif inverse.is_a("IfcInventory"):
- if inverse.ResponsiblePersons == (settings["person"],):
+ if inverse.ResponsiblePersons == (person,):
# in IFC2X3 ResponsiblePersons is not optional and without it IfcInventory is not valid
if file.schema == "IFC2X3":
ifcopenshell.api.root.remove_product(file, product=inverse)
elif inverse.is_a("IfcDocumentInformation"):
- if inverse.Editors == (settings["person"],):
+ if inverse.Editors == (person,):
inverse.Editors = None
elif inverse.is_a("IfcPersonAndOrganization"):
ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse)
elif inverse.is_a("IfcActor"):
ifcopenshell.api.root.remove_product(file, product=inverse)
elif inverse.is_a("IfcResourceLevelRelationship"):
- if inverse.RelatedResourceObjects == (settings["person"],):
+ if inverse.RelatedResourceObjects == (person,):
file.remove(inverse)
- file.remove(settings["person"])
+ file.remove(person)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py
index a5d813756e..905602db16 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py
@@ -80,8 +80,8 @@ def assign_lag_time(
# for whatever reason.
ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1D")
"""
- lag_value = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration"))
- lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=lag_value)
+ duration = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration"))
+ lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=duration)
if rel_sequence.is_a("IfcRelSequence"):
if rel_sequence.TimeLag and len(file.get_inverse(rel_sequence.TimeLag)) == 1:
file.remove(rel_sequence.TimeLag)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py
index 08ffcd9f40..fb44184c5f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py
@@ -25,9 +25,7 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity
The schedule is cascaded afterwards.
:param rel_sequence: The sequence to remove the lag time from.
- :type rel_sequence: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -57,12 +55,8 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity
# What if you didn't?
ifcopenshell.api.sequence.unassign_lag_time(model, rel_sequence=sequence)
"""
- settings = {
- "rel_sequence": rel_sequence,
- }
-
- if len(file.get_inverse(settings["rel_sequence"].TimeLag)) == 1:
- file.remove(settings["rel_sequence"].TimeLag)
+ if len(file.get_inverse(rel_sequence.TimeLag)) == 1:
+ file.remove(rel_sequence.TimeLag)
else:
- settings["rel_sequence"].TimeLag = None
- ifcopenshell.api.sequence.cascade_schedule(file, task=settings["rel_sequence"].RelatedProcess)
+ rel_sequence.TimeLag = None
+ ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py
index 8135bc0255..144b96f5a9 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py
@@ -29,18 +29,14 @@ def remove_structural_analysis_model(
:param structural_analysis_model: The IfcStructuralAnalysisModel to
remove.
- :type structural_analysis_model: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"structural_analysis_model": structural_analysis_model}
-
- for rel in settings["structural_analysis_model"].IsGroupedBy or []:
+ for rel in structural_analysis_model.IsGroupedBy or []:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- history = settings["structural_analysis_model"].OwnerHistory
- file.remove(settings["structural_analysis_model"])
+ history = structural_analysis_model.OwnerHistory
+ file.remove(structural_analysis_model)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py
index 63c6331e51..d31811a739 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py
@@ -28,23 +28,21 @@ def remove_structural_boundary_condition(
:param connection: The IfcStructuralConnection to remove the condition
from. If omitted, it is assumed to be an orphaned condition.
- :type connection: ifcopenshell.entity_instance,optional
:param boundary_condition: The IfcBoundaryCondition to remove.
- :type boundary_condition: ifcopenshell.entity_instance, optional.
:return: None
- :rtype: None
"""
- settings = {"connection": connection, "boundary_condition": boundary_condition}
- if settings["connection"]:
+ if connection:
# remove boundary condition from a connection
- if not settings["connection"].AppliedCondition:
+ if not connection.AppliedCondition:
return
- if len(file.get_inverse(settings["connection"].AppliedCondition)) == 1:
- file.remove(settings["connection"].AppliedCondition)
- settings["connection"].AppliedCondition = None
+ applied_condition = connection.AppliedCondition
+ if file.get_total_inverses(applied_condition) == 1:
+ file.remove(applied_condition)
+ connection.AppliedCondition = None
else:
+ assert boundary_condition, "Either connection or boundary_condition must be provided."
# remove the boundary condition
- for conn in file.get_inverse(settings["boundary_condition"]):
+ for conn in file.get_inverse(boundary_condition):
conn.AppliedCondition = None
- file.remove(settings["boundary_condition"])
+ file.remove(boundary_condition)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py
index 1d406a16e8..0c1a6ad3a1 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py
@@ -22,10 +22,6 @@ def remove_structural_load(file: ifcopenshell.file, structural_load: ifcopenshel
"""Removes a structural load
:param structural_load: The IfcStructuralLoad to remove.
- :type structural_load: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"structural_load": structural_load}
-
- file.remove(settings["structural_load"])
+ file.remove(structural_load)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py
index 1d9473515a..401db40dbc 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py
@@ -25,18 +25,14 @@ def remove_structural_load_case(file: ifcopenshell.file, load_case: ifcopenshell
"""Removes a structural load case
:param load_case: The IfcStructuralLoadCase to remove.
- :type load_case: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"load_case": load_case}
-
# TODO: do a deep purge
- for rel in settings["load_case"].IsGroupedBy or []:
+ for rel in load_case.IsGroupedBy or []:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- history = settings["load_case"].OwnerHistory
- file.remove(settings["load_case"])
+ history = load_case.OwnerHistory
+ file.remove(load_case)
ifcopenshell.util.element.remove_deep2(file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py
index 281630fd7a..25acbbe9cc 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py
@@ -25,20 +25,16 @@ def remove_structural_load_group(file: ifcopenshell.file, load_group: ifcopenshe
"""Removes a structural load group
:param load_group: The IfcStructuralLoadGroup to remove.
- :type load_group: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"load_group": load_group}
-
# TODO: do a deep purge
- for inverse in file.get_inverse(settings["load_group"]):
+ for inverse in file.get_inverse(load_group):
if inverse.is_a("IfcRelAssignsToGroup") and len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- history = settings["load_group"].OwnerHistory
- file.remove(settings["load_group"])
+ history = load_group.OwnerHistory
+ file.remove(load_group)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py
index b19b8154c9..47adbc284e 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py
@@ -835,6 +835,7 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.api.georeference
+ import ifcopenshell.api.unit
prefix = get_prefix(target_units)
si_unit = get_unit_name(target_units)