From 6eda4e389c16d49a19934059e6d24b4c651d2215 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 11:37:19 +0500 Subject: [PATCH] typing --- src/bonsai/bonsai/bim/import_ifc.py | 3 +- src/bonsai/bonsai/bim/module/model/data.py | 2 +- src/bonsai/bonsai/bim/module/model/product.py | 7 +- src/bonsai/bonsai/bim/module/root/data.py | 9 +- src/bonsai/bonsai/bim/module/root/operator.py | 29 ++++--- src/bonsai/bonsai/bim/module/root/prop.py | 54 +++++++----- src/bonsai/bonsai/bim/module/root/ui.py | 15 ++-- src/bonsai/bonsai/bim/module/spatial/data.py | 6 +- .../bonsai/bim/module/spatial/decorator.py | 6 +- .../bonsai/bim/module/spatial/operator.py | 2 +- src/bonsai/bonsai/bim/module/spatial/prop.py | 57 ++++++++++++- src/bonsai/bonsai/bim/module/spatial/ui.py | 85 +++++++++++++------ src/bonsai/bonsai/tool/geometry.py | 4 +- src/bonsai/bonsai/tool/project.py | 6 +- src/bonsai/bonsai/tool/root.py | 14 ++- src/bonsai/bonsai/tool/spatial.py | 75 ++++++++++------ src/bonsai/test/tool/test_model.py | 5 +- src/bonsai/test/tool/test_project.py | 6 +- src/bonsai/test/tool/test_root.py | 6 +- src/bonsai/test/tool/test_spatial.py | 6 +- .../api/cost/remove_cost_item_quantity.py | 15 ++-- .../api/cost/remove_cost_value.py | 33 +++---- .../ifcopenshell/api/grid/remove_grid_axis.py | 2 - .../api/material/assign_profile.py | 20 ++--- .../api/owner/remove_organisation.py | 20 ++--- .../ifcopenshell/api/owner/remove_person.py | 19 ++--- .../api/sequence/assign_lag_time.py | 4 +- .../api/sequence/unassign_lag_time.py | 14 +-- .../remove_structural_analysis_model.py | 10 +-- .../remove_structural_boundary_condition.py | 20 ++--- .../api/structural/remove_structural_load.py | 6 +- .../structural/remove_structural_load_case.py | 10 +-- .../remove_structural_load_group.py | 10 +-- .../ifcopenshell/util/unit.py | 1 + 34 files changed, 336 insertions(+), 245 deletions(-) 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)