diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index 84173a9ec5..8b010fd141 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -137,7 +137,7 @@ class IfcExporter: return checksum != tool.Geometry.get_material_checksum(obj) def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id) if tool.Geometry.is_scaled(obj): bpy.ops.bim.update_representation(obj=obj.name) # update_representation might not apply scale if the object has openings diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 4ed12d74f0..e958098c16 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -64,14 +64,15 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - refresh_ui_data() return - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not props.ifc_definition_id: return - if obj.BIMObjectProperties.is_renaming: - obj.BIMObjectProperties.is_renaming = False + if props.is_renaming: + props.is_renaming = False return - element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id) + element = tool.Ifc.get().by_id(props.ifc_definition_id) if "/" in obj.name: object_name = obj.name element_name = obj.name.split("/", 1)[1] @@ -87,8 +88,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - if not element.is_a("IfcRoot"): return element.Name = element_name - if obj.BIMObjectProperties.collection: - obj.BIMObjectProperties.collection.name = object_name + if props.collection: + props.collection.name = object_name refresh_ui_data() diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 35b40b7b3c..ce84c66f02 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -267,7 +267,8 @@ class IfcStore: if element.is_a("IfcSurfaceStyle"): obj.BIMStyleProperties.ifc_definition_id = element.id() else: - obj.BIMObjectProperties.ifc_definition_id = element.id() + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = element.id() tool.Ifc.setup_listeners(obj) @@ -407,7 +408,8 @@ class IfcStore: if isinstance(obj, bpy.types.Material): obj.BIMStyleProperties.ifc_definition_id = 0 else: # bpy.types.Object - obj.BIMObjectProperties.ifc_definition_id = 0 + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = 0 @staticmethod def execute_ifc_operator( diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 2a3a5f97a2..e777545780 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -933,7 +933,8 @@ class IfcImporter: self.project = {"ifc": project} obj = tool.Ifc.get_object(project) if obj: - self.project["blender"] = obj.BIMObjectProperties.collection + props = tool.Blender.get_object_bim_props(obj) + self.project["blender"] = props.collection self.has_existing_project = True return self.project["blender"] = bpy.data.collections.new( @@ -943,7 +944,8 @@ class IfcImporter: obj.hide_select = True self.project["blender"].objects.link(obj) self.project["blender"].BIMCollectionProperties.obj = obj - obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"] + props = tool.Blender.get_object_bim_props(obj) + props.collection = self.collections[project.GlobalId] = self.project["blender"] def create_styles(self) -> None: for style in self.file.by_type("IfcSurfaceStyle"): diff --git a/src/bonsai/bonsai/bim/module/aggregate/ui.py b/src/bonsai/bonsai/bim/module/aggregate/ui.py index 74389c723d..0755d3a54f 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/ui.py +++ b/src/bonsai/bonsai/bim/module/aggregate/ui.py @@ -34,9 +34,9 @@ class BIM_PT_aggregate(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): @@ -66,9 +66,9 @@ class BIM_PT_aggregate(Panel): col.enabled = False op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK") if props.relating_object: - op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id + op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id elif props.related_object: - op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id + op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="") return else: @@ -115,9 +115,9 @@ class BIM_PT_linked_aggregate(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index 75cf32cb66..ac81aa2a65 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -316,7 +316,8 @@ class ColourByRelatedBuildingElement(bpy.types.Operator): def _execute(self, context): for obj in context.visible_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not props.ifc_definition_id: continue element = tool.Ifc.get_entity(obj) if not element.is_a("IfcRelSpaceBoundary"): diff --git a/src/bonsai/bonsai/bim/module/boundary/ui.py b/src/bonsai/bonsai/bim/module/boundary/ui.py index e7332d50c6..ba7592b34c 100644 --- a/src/bonsai/bonsai/bim/module/boundary/ui.py +++ b/src/bonsai/bonsai/bim/module/boundary/ui.py @@ -52,9 +52,9 @@ class BIM_PT_Boundary(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): @@ -65,7 +65,7 @@ class BIM_PT_Boundary(Panel): def draw(self, context): obj = context.active_object assert obj - props = obj.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) ifc_file = tool.Ifc.get() boundary = ifc_file.by_id(props.ifc_definition_id) self.bprops = tool.Boundary.get_object_boundary_props(obj) @@ -128,9 +128,9 @@ class BIM_PT_SpaceBoundaries(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): @@ -145,7 +145,9 @@ class BIM_PT_SpaceBoundaries(Panel): if not SpaceBoundariesData.is_loaded: SpaceBoundariesData.load() - self.props = context.active_object.BIMObjectProperties + obj = context.active_object + assert obj + self.props = tool.Blender.get_object_bim_props(obj) self.ifc_file = tool.Ifc.get() row = self.layout.row() row.operator("bim.load_space_boundaries") diff --git a/src/bonsai/bonsai/bim/module/cad/prop.py b/src/bonsai/bonsai/bim/module/cad/prop.py index 9fd2dd4d3b..7247408f00 100644 --- a/src/bonsai/bonsai/bim/module/cad/prop.py +++ b/src/bonsai/bonsai/bim/module/cad/prop.py @@ -20,6 +20,7 @@ import bpy from bonsai.bim.module.model.data import AuthoringData from bpy.types import PropertyGroup from math import pi +from typing import TYPE_CHECKING class BIMCadProperties(PropertyGroup): @@ -31,3 +32,11 @@ class BIMCadProperties(PropertyGroup): gable_roof_edge_angle: bpy.props.FloatProperty( name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE" ) + + if TYPE_CHECKING: + resolution: int + radius: float + distance: float + x: float + y: float + gable_roof_edge_angle: float diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 4b70d04df1..459fb7ef50 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -146,7 +146,7 @@ class CadTool(WorkSpaceTool): if ( (RailingData.is_loaded or not RailingData.load()) and RailingData.data["pset_data"] - and obj.BIMRailingProperties.is_editing_path + and tool.Model.get_railing_props(obj).is_editing_path ): add_header_apply_button( layout, @@ -159,7 +159,7 @@ class CadTool(WorkSpaceTool): elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and obj.BIMRoofProperties.is_editing_path + and tool.Model.get_roof_props(obj).is_editing_path ): add_header_apply_button( layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context @@ -195,12 +195,14 @@ class CadHotkey(bpy.types.Operator): return operator.description or "" def execute(self, context): - self.props = context.scene.BIMCadProperties + self.props = tool.Cad.get_cad_props() getattr(self, f"hotkey_{self.hotkey}")() return {"FINISHED"} def draw(self, context): - props = context.scene.BIMCadProperties + props = tool.Cad.get_cad_props() + obj = context.active_object + if self.hotkey == "S_C": if tool.Geometry.is_profile_object_active(): row = self.layout.row() @@ -226,7 +228,7 @@ class CadHotkey(bpy.types.Operator): elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and bpy.context.active_object.BIMRoofProperties.is_editing_path + and tool.Model.get_roof_props(obj).is_editing_path ): self.layout.row().prop(props, "gable_roof_edge_angle") @@ -281,13 +283,17 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.edit_extrusion_axis() def hotkey_S_R(self): + obj = bpy.context.active_object + if not obj: + return + if tool.Geometry.is_profile_object_active(): si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion) elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and bpy.context.active_object.BIMRoofProperties.is_editing_path + and tool.Model.get_roof_props(obj).is_editing_path ): bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index c3d87e4759..3fd4e3b4de 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -316,7 +316,8 @@ class SelectIfcClashResults(bpy.types.Operator): global_ids.extend([clash["a_global_id"], clash["b_global_id"]]) for obj in context.visible_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not props.ifc_definition_id: continue ifc_file = "" @@ -335,7 +336,7 @@ class SelectIfcClashResults(bpy.types.Operator): element_file = self.file try: - element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = element_file.by_id(props.ifc_definition_id) except: continue diff --git a/src/bonsai/bonsai/bim/module/constraint/operator.py b/src/bonsai/bonsai/bim/module/constraint/operator.py index 3a66b074c7..4ae0dde395 100644 --- a/src/bonsai/bonsai/bim/module/constraint/operator.py +++ b/src/bonsai/bonsai/bim/module/constraint/operator.py @@ -161,8 +161,12 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects + products = [ + self.file.by_id(obj_id) + for obj in objs + if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id) + ] if products: ifcopenshell.api.run( "constraint.assign_constraint", @@ -184,8 +188,12 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects + products = [ + self.file.by_id(obj_id) + for obj in objs + if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id) + ] if products: ifcopenshell.api.run( "constraint.unassign_constraint", diff --git a/src/bonsai/bonsai/bim/module/constraint/ui.py b/src/bonsai/bonsai/bim/module/constraint/ui.py index 76f4c4a1bb..3693e9565a 100644 --- a/src/bonsai/bonsai/bim/module/constraint/ui.py +++ b/src/bonsai/bonsai/bim/module/constraint/ui.py @@ -79,18 +79,18 @@ class BIM_PT_object_constraints(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): - return False - return bool(context.active_object.BIMObjectProperties.ifc_definition_id) + props = tool.Blender.get_object_bim_props(obj) + return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id)) def draw(self, context): if not ObjectConstraintsData.is_loaded: ObjectConstraintsData.load() obj = context.active_object - self.oprops = obj.BIMObjectProperties + assert obj + self.oprops = tool.Blender.get_object_bim_props(obj) self.sprops = context.scene.BIMConstraintProperties self.props = obj.BIMObjectConstraintProperties self.file = tool.Ifc.get() diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index 00d5f5fec0..ead79ba57a 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -740,15 +740,14 @@ class LoadProductCostItems(bpy.types.Operator): @classmethod def poll(cls, context): - if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)): cls.poll_message_set("No IFC object is active.") return False return True def execute(self, context): - core.load_product_cost_items( - tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) - ) + obj = context.active_object + core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj)) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/covering/prop.py b/src/bonsai/bonsai/bim/module/covering/prop.py index 2a26d88e35..3dc65903aa 100644 --- a/src/bonsai/bonsai/bim/module/covering/prop.py +++ b/src/bonsai/bonsai/bim/module/covering/prop.py @@ -18,9 +18,13 @@ import bpy from bpy.types import PropertyGroup +from typing import TYPE_CHECKING class BIMCoveringProperties(PropertyGroup): ceiling_height: bpy.props.FloatProperty( name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height" ) + + if TYPE_CHECKING: + ceiling_height: float diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index c24f478408..904bd98a71 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -55,7 +55,7 @@ class CoveringToolUI: def draw(cls, context, layout, ifc_element_type=None): cls.layout = layout cls.props = tool.Model.get_model_props() - cls.covering_props = context.scene.BIMCoveringProperties + cls.covering_props = tool.Covering.get_covering_props() row = cls.layout.row(align=True) if not tool.Ifc.get(): @@ -159,12 +159,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return operator.description or "" def _execute(self, context): - # self.props = context.scene.BIMCoveringProperties + self.props = tool.Covering.get_covering_props() getattr(self, f"hotkey_{self.hotkey}")() def invoke(self, context, event): # https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey - # self.props = context.scene.BIMSpatialProperties return self.execute(context) def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/covetool/operator.py b/src/bonsai/bonsai/bim/module/covetool/operator.py index 1d64dd958e..a11e836750 100644 --- a/src/bonsai/bonsai/bim/module/covetool/operator.py +++ b/src/bonsai/bonsai/bim/module/covetool/operator.py @@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator): if modifier.type == "TRIANGULATE": return True - def get_covetool_category(self, obj): + def get_covetool_category(self, obj: bpy.types.Object): if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh): return - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): return - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = self.file.by_id(ifc_id) ifc_class = element.is_a() if "IfcSlab" in ifc_class: return "floors" diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 4b16c3fb12..67ba6e5199 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -382,7 +382,7 @@ class InspectFromObject(bpy.types.Operator): def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]: obj = context.active_object assert obj - if ifc_id := obj.BIMObjectProperties.ifc_definition_id: + if ifc_id := tool.Blender.get_ifc_definition_id(obj): return ifc_id if ( (data := obj.data) diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index dc9e981b19..4c243bc28b 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -65,7 +65,7 @@ class VisualiseDiff(bpy.types.Operator): obj.color = (0.0, 0.0, 0.7, 1.0) continue - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue ifc_file = "" @@ -84,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator): element_file = ifc_file try: - element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = element_file.by_id(ifc_id) except: continue global_id = getattr(element, "GlobalId", None) @@ -253,7 +253,7 @@ class SelectDiffObjects(bpy.types.Operator): obj.select_set(True) continue - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue ifc_file = "" @@ -272,7 +272,7 @@ class SelectDiffObjects(bpy.types.Operator): element_file = ifc_file try: - element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = element_file.by_id(ifc_id) except: continue global_id = getattr(element, "GlobalId", None) diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index 4bd935c8f6..fefb87fe6c 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -90,18 +90,20 @@ class BIM_PT_object_documents(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): return False - return bool(context.active_object.BIMObjectProperties.ifc_definition_id) + if not tool.Ifc.get_object_by_identifier(ifc_id): + return False + return True def draw(self, context): if not ObjectDocumentData.is_loaded: ObjectDocumentData.load() obj = context.active_object - self.oprops = obj.BIMObjectProperties + self.oprops = tool.Blender.get_object_bim_props(obj) self.props = tool.Document.get_document_props() self.file = tool.Ifc.get() diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 9f02989384..d312298f1b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -51,7 +51,7 @@ class Annotator: curve.font = font props = tool.Drawing.get_text_props(obj) props.font_size = "2.5" - collection = bpy.context.scene.camera.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection collection.objects.link(obj) Annotator.resize_text(obj) return obj @@ -133,7 +133,7 @@ class Annotator: co1, _, _, _ = Annotator.get_placeholder_coords(camera) matrix_world = tool.Drawing.get_camera_matrix(camera) matrix_world.translation = co1 - collection = camera.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(camera).collection if object_type == "TEXT": obj = bpy.data.objects.new(object_type, None) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index c63fd3d910..5a46b8e27b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -343,7 +343,7 @@ def get_active_drawing( props = tool.Drawing.get_document_props() try: camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id)) - return camera.BIMObjectProperties.collection, camera + return tool.Blender.get_object_bim_props(camera).collection, camera except: return None, None diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 3528e4dc36..2b75406d5c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -246,7 +246,7 @@ class CreateDrawing(bpy.types.Operator): def execute(self, context): self.props = tool.Drawing.get_document_props() - active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id + active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera) if self.print_all: original_drawing_id = active_drawing_id drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing] @@ -391,7 +391,7 @@ class CreateDrawing(bpy.types.Operator): bpy.ops.render.render(write_still=True) else: previous_visibility = {} - for obj in self.camera.BIMObjectProperties.collection.objects: + for obj in tool.Blender.get_object_bim_props(self.camera).collection.objects: if bpy.context.view_layer.objects.get(obj.name): previous_visibility[obj.name] = obj.hide_get() obj.hide_set(True) @@ -2095,7 +2095,7 @@ class ResizeText(bpy.types.Operator): # TODO: check undo redo def execute(self, context): - for obj in context.scene.camera.BIMObjectProperties.collection.objects: + for obj in tool.Blender.get_object_bim_props(context.scene.camera).collection.objects: if isinstance(obj.data, bpy.types.TextCurve): annotation.Annotator.resize_text(obj) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 6c875d1cfe..8963fa5b8f 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -266,7 +266,7 @@ def update_titleblocks(self, context): def update_should_draw_decorations(self, context: bpy.types.Context) -> None: if self.should_draw_decorations: # TODO: design a proper text variable templating renderer - collection = context.scene.camera.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(context.scene.camera).collection for obj in collection.objects: element = tool.Ifc.get_entity(obj) if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]): diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 4e9eb9cf6f..79f2ceeb67 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -102,7 +102,7 @@ def block_scale(scene: bpy.types.Scene) -> None: import bonsai.tool as tool if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active): - if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id: + if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj): if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id: diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 0763bd4963..99612e638f 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -464,7 +464,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): assert tool.Geometry.is_data_supported_for_adding_representation(data) mprops = tool.Geometry.get_mesh_props(data) - product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + product = tool.Ifc.get_entity(obj) + assert product material = ifcopenshell.util.element.get_material(product, should_skip_usage=True) # NOTE: Currently iterator doesn't detect whether opening is actually affected the representation @@ -903,7 +904,7 @@ class OverrideOutlinerDelete(bpy.types.Operator): if element := tool.Ifc.get_entity(obj): if tool.Geometry.is_locked(element): self.report({"ERROR"}, lock_error_message(obj.name)) - if collection := obj.BIMObjectProperties.collection: + if collection := tool.Blender.get_object_bim_props(obj).collection: collections_to_delete.discard(collection) continue tool.Geometry.delete_ifc_object(obj) @@ -1067,7 +1068,7 @@ class OverrideDuplicateMove(bpy.types.Operator): continue # clear object's collection so it will be able to have it's own - new_obj.BIMObjectProperties.collection = None + tool.Blender.get_object_bim_props(new_obj).collection = None # copy the actual class new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py index 8ba3958222..b1fe7bea61 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -334,9 +334,9 @@ class BIM_PT_connections(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(tool.Blender.get_ifc_definition_id(obj)): return False return tool.Ifc.get() @@ -345,7 +345,6 @@ class BIM_PT_connections(Panel): ConnectionsData.load() layout = self.layout - props = context.active_object.BIMObjectProperties if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]: layout.label(text="No connections found") @@ -480,12 +479,16 @@ class BIM_PT_placement(Panel): @classmethod def poll(cls, context): - return (obj := context.active_object) and obj.BIMObjectProperties.ifc_definition_id + return (obj := context.active_object) and tool.Blender.get_ifc_definition_id(obj) def draw(self, context): if not PlacementData.is_loaded: PlacementData.load() + obj = context.active_object + assert obj + props = tool.Blender.get_object_bim_props(obj) + if not PlacementData.data["has_placement"]: row = self.layout.row() row.label(text="No Object Placement Found") @@ -496,12 +499,12 @@ class BIM_PT_placement(Panel): row = self.layout.row() row.prop(context.active_object, "rotation_euler", text="Rotation") - if context.active_object.BIMObjectProperties.blender_offset_type != "NONE": + if props.blender_offset_type != "NONE": row = self.layout.row(align=True) row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS") - row.label(text=context.active_object.BIMObjectProperties.blender_offset_type) + row.label(text=props.blender_offset_type) - if context.active_object.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE": + if props.blender_offset_type != "NOT_APPLICABLE": row = self.layout.row(align=True) row.label(text=PlacementData.data["original_x"], icon="EMPTY_AXIS") row.label(text=PlacementData.data["original_y"]) diff --git a/src/bonsai/bonsai/bim/module/group/ui.py b/src/bonsai/bonsai/bim/module/group/ui.py index 8cd7d1fe2a..309c49d5d0 100644 --- a/src/bonsai/bonsai/bim/module/group/ui.py +++ b/src/bonsai/bonsai/bim/module/group/ui.py @@ -91,9 +91,10 @@ class BIM_PT_object_groups(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - return tool.Ifc.get() and context.active_object.BIMObjectProperties.ifc_definition_id + props = tool.Blender.get_object_bim_props(obj) + return tool.Ifc.get() and props.ifc_definition_id def draw(self, context): if not ObjectGroupsData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/ifcgit/operator.py b/src/bonsai/bonsai/bim/module/ifcgit/operator.py index bbb7133122..de339c111c 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/operator.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/operator.py @@ -374,16 +374,17 @@ class ObjectLog(bpy.types.Operator): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): cls.poll_message_set("No Active Object") - elif not context.active_object.BIMObjectProperties.ifc_definition_id: + elif not tool.Blender.get_ifc_definition_id(obj): cls.poll_message_set("Active Object doesn't have an IFC definition") else: return True def execute(self, context): - - step_id = context.active_object.BIMObjectProperties.ifc_definition_id + obj = context.active_object + assert obj + step_id = tool.Blender.get_ifc_definition_id(obj) core.entity_log(tool.IfcGit, tool.Ifc, step_id, self) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index fe0fce2413..eaf4ee06c4 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -137,14 +137,14 @@ class BIM_PT_object_material(Panel): def poll(cls, context): if not tool.Blender.is_tab(context, "GEOMETRY"): return False - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not hasattr(tool.Ifc.get().by_id(props.ifc_definition_id), "HasAssociations"): + if not hasattr(tool.Ifc.get().by_id(ifc_id), "HasAssociations"): return False return True @@ -152,9 +152,11 @@ class BIM_PT_object_material(Panel): if not ObjectMaterialData.is_loaded: ObjectMaterialData.load() + obj = context.active_object + assert obj self.file = tool.Ifc.get() - self.oprops = context.active_object.BIMObjectProperties - self.props = context.active_object.BIMObjectMaterialProperties + self.oprops = tool.Blender.get_object_bim_props(obj) + self.props = obj.BIMObjectMaterialProperties self.mprops = tool.Material.get_material_props() if not ObjectMaterialData.data["materials"]: diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 20bd454d35..a1ff930408 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -257,7 +257,7 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator): sources = [] for obj in bpy.context.selected_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + if not tool.Blender.get_ifc_definition_id(obj): continue element = tool.Ifc.get_entity(obj) diff --git a/src/bonsai/bonsai/bim/module/model/array.py b/src/bonsai/bonsai/bim/module/model/array.py index 7e91db2804..0ec49d5561 100644 --- a/src/bonsai/bonsai/bim/module/model/array.py +++ b/src/bonsai/bonsai/bim/module/model/array.py @@ -72,7 +72,9 @@ class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - context.active_object.BIMArrayProperties.is_editing = -1 + obj = context.active_object + assert obj + tool.Model.get_array_props(obj).is_editing = -1 return {"FINISHED"} @@ -84,8 +86,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMArrayProperties + props = tool.Model.get_array_props(obj) relating_obj = props.relating_array_object @@ -119,7 +122,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMArrayProperties + props = tool.Model.get_array_props(obj) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") @@ -182,7 +185,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMArrayProperties + props = tool.Model.get_array_props(obj) pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") data = json.loads(pset["Data"]) @@ -302,7 +305,8 @@ class Input3DCursorXArray(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMArrayProperties + assert obj + props = tool.Model.get_array_props(obj) cursor = context.scene.cursor if props.use_local_space: props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).x @@ -318,7 +322,8 @@ class Input3DCursorYArray(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMArrayProperties + assert obj + props = tool.Model.get_array_props(obj) cursor = context.scene.cursor if props.use_local_space: props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).y @@ -334,7 +339,8 @@ class Input3DCursorZArray(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMArrayProperties + assert obj + props = tool.Model.get_array_props(obj) cursor = context.scene.cursor if props.use_local_space: props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).z diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index 937c586343..3af67e4990 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -385,7 +385,9 @@ class StairData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMStairProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_stair_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_props_kwargs(stair_type=data["stair_type"]) @@ -451,7 +453,9 @@ class WindowData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMWindowProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_window_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_general_kwargs() @@ -462,7 +466,9 @@ class WindowData: @classmethod def lining_params(cls): - props = bpy.context.active_object.BIMWindowProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_window_props(obj) data = cls.data["pset_data"]["data_dict"] lining_data = data["lining_properties"] lining_params = {} @@ -474,7 +480,9 @@ class WindowData: @classmethod def panel_params(cls): - props = bpy.context.active_object.BIMWindowProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_window_props(obj) panel_data = cls.data["pset_data"]["data_dict"]["panel_properties"] panel_params = {} panel_props = props.get_panel_kwargs() @@ -565,7 +573,9 @@ class RailingData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMRailingProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_railing_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_general_kwargs(railing_type=data["railing_type"]) @@ -599,7 +609,9 @@ class RoofData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMRoofProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_roof_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_general_kwargs(generation_method=data["generation_method"]) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index c8b9e2548f..04446deaeb 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -718,14 +718,15 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator): obj.matrix_world = newmat -def generate_box(usecase_path, ifc_file, settings): +def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None: box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW") if not box_context: return obj = settings["blender_object"] if 0 in list(obj.dimensions): return - product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + product = tool.Ifc.get_entity(obj) + assert product old_box = ifcopenshell.util.representation.get_representation(product, "Model", "Box", "MODEL_VIEW") if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body": if old_box: diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 512d88aec5..a8e486c2a6 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -28,34 +28,36 @@ from math import pi, radians from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator from bonsai.bim.module.model.door import update_door_modifier_bmesh from bonsai.bim.module.model.window import update_window_modifier_bmesh -from typing import TYPE_CHECKING, Literal, get_args +from typing import TYPE_CHECKING, Literal, get_args, Union, get_args -def get_ifc_class(self, context): +def get_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["ifc_classes"] -def get_boundary_class(self, context): +def get_boundary_class(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["boundary_class"] -def get_relating_type_id(self, context): +def get_relating_type_id(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["relating_type_id"] -def get_materials(self, context): +def get_materials( + self: Union["BIMWindowProperties", "BIMDoorProperties"], context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["materials"] -def update_ifc_class(self, context): +def update_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> None: bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) AuthoringData.data["ifc_class_current"] = self.ifc_class AuthoringData.data["type_elements"] = AuthoringData.type_elements() @@ -72,46 +74,46 @@ def update_ifc_class(self, context): AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types() -def update_relating_type_id(self, context): +def update_relating_type_id(self: "BIMModelProperties", context: bpy.types.Context) -> None: AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() AuthoringData.data["relating_type_data"] = AuthoringData.relating_type_data() self.type_page = [e[0] for e in AuthoringData.data["relating_type_id"]].index(self.relating_type_id) // 9 + 1 -def update_type_page(self, context): +def update_type_page(self: "BIMModelProperties", context: bpy.types.Context) -> None: AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types() bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class, offset=9 * (self.type_page - 1), limit=9) self["type_page"] = min(self["type_page"], AuthoringData.data["total_pages"]) self["type_page"] = max(self["type_page"], 1) -def update_relating_array_from_object(self, context): +def update_relating_array_from_object(self: "BIMArrayProperties", context: bpy.types.Context) -> None: bpy.ops.bim.enable_editing_array(item=self.is_editing) return -def is_object_array_applicable(self, obj): +def is_object_array_applicable(self: "BIMArrayProperties", obj: bpy.types.Object) -> bool: element = tool.Ifc.get_entity(obj) if not element: return False return ifcopenshell.util.element.get_pset(element, "BBIM_Array") -def update_wall_axis_decorator(self, context): +def update_wall_axis_decorator(self: "BIMModelProperties", context: bpy.types.Context) -> None: if self.show_wall_axis: WallAxisDecorator.install(bpy.context) else: WallAxisDecorator.uninstall() -def update_slab_direction_decorator(self, context): +def update_slab_direction_decorator(self: "BIMModelProperties", context: bpy.types.Context) -> None: if self.show_slab_direction: SlabDirectionDecorator.install(bpy.context) else: SlabDirectionDecorator.uninstall() -def update_search_name(self, context): +def update_search_name(self: "BIMModelProperties", context: bpy.types.Context) -> None: AuthoringData.load() # Total number of pages may decrease when using the search bar : if self.type_page > AuthoringData.data["total_pages"]: @@ -119,17 +121,17 @@ def update_search_name(self, context): bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) -def update_x_angle(self, context): +def update_x_angle(self: "BIMModelProperties", context: bpy.types.Context) -> None: angle_deg = math.degrees(self.x_angle) if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5): self.x_angle = 0 -def update_door(self, context): +def update_door(self: "BIMDoorProperties", context: bpy.types.Context) -> None: update_door_modifier_bmesh(context) -def update_window(self, context): +def update_window(self: "BIMWindowProperties", context: bpy.types.Context) -> None: update_window_modifier_bmesh(context) @@ -303,36 +305,45 @@ class BIMArrayProperties(PropertyGroup): poll=is_object_array_applicable, ) + if TYPE_CHECKING: + is_editing: int + count: int + x: float + y: float + z: float + use_local_space: bool + method: Literal["OFFSET", "DISTRIBUTE"] + sync_children: bool + relating_array_object: Union[bpy.types.Object, None] -def update_total_length_target(self, context): + +def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None: self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) -def update_tread_run(self, context): +def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None: if self.total_length_lock: self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1) else: self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run -def update_number_of_treads(self, context): +def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None: if self.total_length_lock: self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) else: self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run +StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"] + + class BIMStairProperties(PropertyGroup): - def validate_nosing_value(self, context): + def validate_nosing_value(self, context: bpy.types.Context) -> None: if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0: self["nosing_length"] = 0 non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type") - stair_types = ( - ("CONCRETE", "Concrete", ""), - ("WOOD/STEEL", "Wood / Steel", ""), - ("GENERIC", "Generic", ""), - ) is_editing: bpy.props.BoolProperty(default=False) width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE") @@ -361,7 +372,10 @@ class BIMStairProperties(PropertyGroup): top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE") has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True) stair_type: bpy.props.EnumProperty( - name="Stair Type", items=stair_types, default="CONCRETE", update=validate_nosing_value + name="Stair Type", + items=[(i, i.replace("/", " / ").title(), "") for i in get_args(StairType)], + default="CONCRETE", + update=validate_nosing_value, ) custom_first_last_tread_run: bpy.props.FloatVectorProperty( name="Custom First / Last Treads Widths", @@ -385,6 +399,23 @@ class BIMStairProperties(PropertyGroup): name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH" ) + if TYPE_CHECKING: + is_editing: bool + width: float + height: float + number_of_treads: int + total_length_target: float + total_length_lock: bool + tread_depth: float + tread_run: float + base_slab_depth: float + top_slab_depth: float + has_top_nib: bool + stair_type: str + custom_first_last_tread_run: tuple[float, float] + nosing_length: float + nosing_depth: float + def get_props_kwargs(self, convert_to_project_units=False, stair_type=None): if not stair_type: stair_type = self.stair_type @@ -445,20 +476,22 @@ def window_type_prop_update(self, context): update_window(self, context) +WindowType = Literal[ + "SINGLE_PANEL", + "DOUBLE_PANEL_HORIZONTAL", + "DOUBLE_PANEL_VERTICAL", + "TRIPLE_PANEL_BOTTOM", + "TRIPLE_PANEL_TOP", + "TRIPLE_PANEL_LEFT", + "TRIPLE_PANEL_RIGHT", + "TRIPLE_PANEL_HORIZONTAL", + "TRIPLE_PANEL_VERTICAL", +] + + # default prop values are in mm and converted later class BIMWindowProperties(PropertyGroup): non_si_units_props = ("is_editing", "window_type") - window_types = ( - ("SINGLE_PANEL", "SINGLE_PANEL", ""), - ("DOUBLE_PANEL_HORIZONTAL", "DOUBLE_PANEL_HORIZONTAL", ""), - ("DOUBLE_PANEL_VERTICAL", "DOUBLE_PANEL_VERTICAL", ""), - ("TRIPLE_PANEL_BOTTOM", "TRIPLE_PANEL_BOTTOM", ""), - ("TRIPLE_PANEL_TOP", "TRIPLE_PANEL_TOP", ""), - ("TRIPLE_PANEL_LEFT", "TRIPLE_PANEL_LEFT", ""), - ("TRIPLE_PANEL_RIGHT", "TRIPLE_PANEL_RIGHT", ""), - ("TRIPLE_PANEL_HORIZONTAL", "TRIPLE_PANEL_HORIZONTAL", ""), - ("TRIPLE_PANEL_VERTICAL", "TRIPLE_PANEL_VERTICAL", ""), - ) # number of panels and default mullion/transom values # fmt: off @@ -477,7 +510,10 @@ class BIMWindowProperties(PropertyGroup): is_editing: bpy.props.BoolProperty(default=False) window_type: bpy.props.EnumProperty( - name="Window Type", items=window_types, default="SINGLE_PANEL", update=window_type_prop_update + name="Window Type", + items=[(i, i, "") for i in get_args(WindowType)], + default="SINGLE_PANEL", + update=window_type_prop_update, ) overall_height: bpy.props.FloatProperty( name="Overall Height", default=0.9, subtype="DISTANCE", update=update_window @@ -546,6 +582,31 @@ class BIMWindowProperties(PropertyGroup): framing_material: bpy.props.EnumProperty(name="Framing Material", items=get_materials, options=set()) glazing_material: bpy.props.EnumProperty(name="Glazing Material", items=get_materials, options=set()) + if TYPE_CHECKING: + is_editing: bool + window_type: WindowType + overall_height: float + overall_width: float + lining_depth: float + lining_thickness: float + lining_offset: float + lining_to_panel_offset_x: float + lining_to_panel_offset_y: float + mullion_thickness: float + first_mullion_offset: float + second_mullion_offset: float + first_transom_offset: float + second_transom_offset: float + + # Panel properties. + frame_depth: tuple[float, float, float] + frame_thickness: tuple[float, float, float] + + # Material properties. + lining_material: str + framing_material: str + glazing_material: str + def get_general_kwargs(self, convert_to_project_units=False): kwargs = { "window_type": self.window_type, @@ -824,6 +885,10 @@ class BIMDoorProperties(PropertyGroup): setattr(self, prop_name, kwargs[prop_name]) +RailingType = Literal["FRAMELESS_PANEL", "WALL_MOUNTED_HANDRAIL"] +CapType = Literal["TO_END_POST_AND_FLOOR", "TO_END_POST", "TO_FLOOR", "TO_WALL", "180", "NONE"] + + class BIMRailingProperties(PropertyGroup): non_si_units_props = ( "is_editing", @@ -833,23 +898,12 @@ class BIMRailingProperties(PropertyGroup): "path_data", ) - railing_types = ( - ("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""), - ("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""), - ) - cap_types = ( - ("TO_END_POST_AND_FLOOR", "TO_END_POST_AND_FLOOR", ""), - ("TO_END_POST", "TO_END_POST", ""), - ("TO_FLOOR", "TO_FLOOR", ""), - ("TO_WALL", "TO_WALL", ""), - ("180", "180", ""), - ("NONE", "NONE", ""), - ) - is_editing: bpy.props.BoolProperty(default=False) is_editing_path: bpy.props.BoolProperty(default=False) - railing_type: bpy.props.EnumProperty(name="Railing Type", items=railing_types, default="FRAMELESS_PANEL") + railing_type: bpy.props.EnumProperty( + name="Railing Type", items=[(i, i, "") for i in get_args(RailingType)], default="FRAMELESS_PANEL" + ) height: bpy.props.FloatProperty(name="Height", default=1.0, subtype="DISTANCE") thickness: bpy.props.FloatProperty(name="Thickness", default=0.050, subtype="DISTANCE") spacing: bpy.props.FloatProperty(name="Spacing", default=0.050, subtype="DISTANCE") @@ -875,7 +929,24 @@ class BIMRailingProperties(PropertyGroup): description="Clear width between the railing and the wall", subtype="DISTANCE", ) - terminal_type: bpy.props.EnumProperty(name="Terminal Type", items=cap_types, default="180") + terminal_type: bpy.props.EnumProperty( + name="Terminal Type", items=[(i, i, "") for i in get_args(CapType)], default="180" + ) + + if TYPE_CHECKING: + is_editing: bool + is_editing_path: bool + + railing_type: RailingType + height: float + thickness: float + spacing: float + + use_manual_supports: bool + support_spacing: float + railing_diameter: float + clear_width: float + terminal_type: CapType def get_general_kwargs(self, railing_type=None, convert_to_project_units=False): if railing_type is None: @@ -921,11 +992,15 @@ def to_percentage(angle: float) -> float: return math.tan(angle) * 100 +RoofType = Literal["HIP/GABLE ROOF"] +RoofGenerationMethod = Literal["HEIGHT", "ANGLE"] + + class BIMRoofProperties(PropertyGroup): - def update_angle(self, context) -> None: + def update_angle(self, context: bpy.types.Context) -> None: self["angle"] = to_angle(self.percentage) - def update_percentage(self, context) -> None: + def update_percentage(self, context: bpy.types.Context) -> None: self["percentage"] = to_percentage(self.angle) non_si_units_props = ( @@ -937,18 +1012,15 @@ class BIMRoofProperties(PropertyGroup): "percentage", "rafter_edge_angle", ) - roof_types = (("HIP/GABLE ROOF", "HIP/GABLE ROOF", ""),) - roof_generation_methods = ( - ("HEIGHT", "HEIGHT", ""), - ("ANGLE", "ANGLE", ""), - ) is_editing: bpy.props.BoolProperty(default=False) is_editing_path: bpy.props.BoolProperty(default=False) - roof_type: bpy.props.EnumProperty(name="Roof Type", items=roof_types, default="HIP/GABLE ROOF") + roof_type: bpy.props.EnumProperty( + name="Roof Type", items=[(i, i, "") for i in get_args(RoofType)], default="HIP/GABLE ROOF" + ) generation_method: bpy.props.EnumProperty( - name="Roof Generation Method", items=roof_generation_methods, default="ANGLE" + name="Roof Generation Method", items=[(i, i, "") for i in get_args(RoofGenerationMethod)], default="ANGLE" ) height: bpy.props.FloatProperty( name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE" @@ -978,6 +1050,17 @@ class BIMRoofProperties(PropertyGroup): name="Rafter Edge Angle", min=0, max=pi / 2, default=pi / 2, subtype="ANGLE" ) + if TYPE_CHECKING: + is_editing: bool + is_editing_path: bool + roof_type: Literal["HIP/GABLE ROOF"] + generation_method: Literal["HEIGHT", "ANGLE"] + height: float + angle: float + percentage: float + roof_thickness: float + rafter_edge_angle: float + def get_general_kwargs(self, generation_method=None, convert_to_project_units=False): if generation_method is None: generation_method = self.generation_method diff --git a/src/bonsai/bonsai/bim/module/model/railing.py b/src/bonsai/bonsai/bim/module/model/railing.py index 78c94f4ed3..632e1748be 100644 --- a/src/bonsai/bonsai/bim/module/model/railing.py +++ b/src/bonsai/bonsai/bim/module/model/railing.py @@ -55,8 +55,10 @@ def update_railing_modifier_ifc_data(context: bpy.types.Context) -> None: since it's going to update ifc representation """ obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) element = tool.Ifc.get_entity(obj) + assert element ifc_file = tool.Ifc.get() # type attributes @@ -122,7 +124,8 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None: If BBIM Pset just changed should call refresh() before updating bmesh """ obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) V_ = tool.Blender.V_ # NOTE: using Data since bmesh update will hapen very often @@ -327,8 +330,10 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + assert element + props = tool.Model.get_railing_props(obj) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) railing_data = props.get_general_kwargs(convert_to_project_units=True) @@ -367,7 +372,8 @@ class CopyRailingParameters(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): source_obj = context.active_object - source_props = source_obj.BIMRailingProperties + assert source_obj + source_props = tool.Model.get_railing_props(source_obj) railing_data = source_props.get_general_kwargs(convert_to_project_units=True) for target_obj in context.selected_objects: @@ -379,7 +385,8 @@ class CopyRailingParameters(bpy.types.Operator, tool.Ifc.Operator): continue railing_data["path_data"] = RailingData.data["path_data"] target_element = tool.Ifc.get_entity(target_obj) - target_props = target_obj.BIMRailingProperties + assert target_element + target_props = tool.Model.get_railing_props(target_obj) target_props.set_props_kwargs_from_ifc_data(railing_data) update_bbim_railing_pset(target_element, railing_data) @@ -398,7 +405,8 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] data["path_data"] = json.dumps(data["path_data"]) @@ -416,8 +424,9 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] - props = obj.BIMRailingProperties + props = tool.Model.get_railing_props(obj) # restore previous settings since editing was canceled props.set_props_kwargs_from_ifc_data(data) @@ -434,8 +443,10 @@ class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + assert element + props = tool.Model.get_railing_props(obj) pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing") path_data = pset_data["data_dict"]["path_data"] @@ -457,8 +468,10 @@ class FlipRailingPathOrder(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + assert element + props = tool.Model.get_railing_props(obj) pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing") path_data = pset_data["data_dict"]["path_data"] @@ -488,7 +501,8 @@ class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object [o.select_set(False) for o in context.selected_objects if o != obj] - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] # required since we could load pset from .ifc and BIMRoofProperties won't be set props.set_props_kwargs_from_ifc_data(data) @@ -505,7 +519,8 @@ class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): def cancel_editing_railing_path(context: bpy.types.Context) -> set[str]: obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) ProfileDecorator.uninstall() props.is_editing_path = False @@ -517,6 +532,7 @@ def cancel_editing_railing_path(context: bpy.types.Context) -> set[str]: update_railing_modifier_bmesh(context) else: element = tool.Ifc.get_entity(obj) + assert element body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") bonsai.core.geometry.switch_representation( tool.Ifc, @@ -547,8 +563,9 @@ class FinishEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + props = tool.Model.get_railing_props(obj) railing_data = props.get_general_kwargs(convert_to_project_units=True) path_data = get_path_data(obj) @@ -574,8 +591,11 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - obj.BIMRailingProperties.is_editing = False + assert element + props = tool.Model.get_railing_props(obj) + props.is_editing = False pset = tool.Pset.get_element_pset(element, "BBIM_Railing") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) diff --git a/src/bonsai/bonsai/bim/module/model/roof.py b/src/bonsai/bonsai/bim/module/model/roof.py index 8afd41bbee..77058bca39 100644 --- a/src/bonsai/bonsai/bim/module/model/roof.py +++ b/src/bonsai/bonsai/bim/module/model/roof.py @@ -409,7 +409,8 @@ def update_roof_modifier_ifc_data(context: bpy.types.Context) -> None: since it's going to update ifc representation """ obj = context.active_object - props = obj.BIMRoofProperties + assert obj + props = tool.Model.get_roof_props(obj) element = tool.Ifc.get_entity(obj) def roof_is_gabled() -> bool: @@ -442,7 +443,7 @@ def update_roof_modifier_bmesh(obj: bpy.types.Object) -> None: """before using should make sure that Data contains up-to-date information. If BBIM Pset just changed should call refresh() before updating bmesh """ - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) assert isinstance(obj.data, bpy.types.Mesh) # NOTE: using Data since bmesh update will hapen very often @@ -559,7 +560,7 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) # rejecting original roof shape to be safe @@ -607,7 +608,8 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMRoofProperties + assert obj + props = tool.Model.get_roof_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] # required since we could load pset from .ifc and BIMRoofProperties won't be set props.set_props_kwargs_from_ifc_data(data) @@ -624,7 +626,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) # restore previous settings since editing was canceled props.set_props_kwargs_from_ifc_data(data) @@ -642,7 +644,7 @@ class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) pset_data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof") path_data = pset_data["data_dict"]["path_data"] @@ -666,7 +668,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj [o.select_set(False) for o in context.selected_objects if o != obj] - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] # required since we could load pset from .ifc and BIMRoofProperties won't be set props.set_props_kwargs_from_ifc_data(data) @@ -720,7 +722,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): def cancel_editing_roof_path(context: bpy.types.Context) -> set[str]: obj = context.active_object assert obj - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) ProfileDecorator.uninstall() props.is_editing_path = False @@ -751,7 +753,8 @@ class CopyRoofParameters(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): source_obj = context.active_object - source_props = source_obj.BIMRoofProperties + assert source_obj + source_props = tool.Model.get_roof_props(source_obj) data = source_props.get_general_kwargs(convert_to_project_units=True) for target_obj in context.selected_objects: @@ -763,7 +766,7 @@ class CopyRoofParameters(bpy.types.Operator, tool.Ifc.Operator): continue data["path_data"] = RoofData.data["path_data"] target_element = tool.Ifc.get_entity(target_obj) - target_props = target_obj.BIMRoofProperties + target_props = tool.Model.get_roof_props(target_obj) target_props.set_props_kwargs_from_ifc_data(data) update_bbim_roof_pset(target_element, data) @@ -783,7 +786,7 @@ class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) bm = tool.Blender.get_bmesh_for_mesh(obj.data) op_status, error_message = is_valid_roof_footprint(bm) @@ -815,9 +818,12 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - obj.BIMRoofProperties.is_editing = False + props = tool.Model.get_roof_props(obj) + props.is_editing = False + assert element pset = tool.Pset.get_element_pset(element, "BBIM_Roof") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py index 8abd07b838..e325497525 100644 --- a/src/bonsai/bonsai/bim/module/model/stair.py +++ b/src/bonsai/bonsai/bim/module/model/stair.py @@ -34,7 +34,8 @@ from bpy_extras.object_utils import AddObjectHelper, object_data_add def regenerate_stair_mesh(obj: bpy.types.Object) -> None: - props_kwargs = obj.BIMStairProperties.get_props_kwargs() + props = tool.Model.get_stair_props(obj) + props_kwargs = props.get_props_kwargs() vertices, edges, faces = tool.Model.generate_stair_2d_profile(**props_kwargs) bm = bmesh.new() @@ -69,7 +70,8 @@ def update_ifc_stair_props(obj: bpy.types.Object) -> None: since it's going to update ifc representation """ element = tool.Ifc.get_entity(obj) - props = obj.BIMStairProperties + assert element + props = tool.Model.get_stair_props(obj) ifc_file = tool.Ifc.get() if tool.Ifc.get_schema() != "IFC2X3" and element.is_a("IfcStairFlight"): @@ -183,7 +185,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMStairProperties + assert element + props = tool.Model.get_stair_props(obj) ifc_file = tool.Ifc.get() stair_data = props.get_props_kwargs(convert_to_project_units=True) @@ -214,9 +217,11 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) + assert element data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Stair", "Data")) - props = obj.BIMStairProperties + props = tool.Model.get_stair_props(obj) # restore previous settings since editing was canceled props.set_props_kwargs_from_ifc_data(data) regenerate_stair_mesh(obj) @@ -233,8 +238,10 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMStairProperties + assert element + props = tool.Model.get_stair_props(obj) data = props.get_props_kwargs(convert_to_project_units=True) props.is_editing = False @@ -257,7 +264,8 @@ class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMStairProperties + assert obj + props = tool.Model.get_stair_props(obj) element = tool.Ifc.get_entity(obj) data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Stair", "Data")) # required since we could load pset from .ifc and BIMStairProperties won't be set @@ -273,9 +281,11 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMStairProperties + assert obj + props = tool.Model.get_stair_props(obj) element = tool.Ifc.get_entity(obj) - obj.BIMStairProperties.is_editing = False + assert element + props.is_editing = False pset = tool.Pset.get_element_pset(element, "BBIM_Stair") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 3374b0695d..688e7abf42 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -217,7 +217,9 @@ class BIM_PT_array(bpy.types.Panel): if not ArrayData.is_loaded: ArrayData.load() - props = context.active_object.BIMArrayProperties + obj = context.active_object + assert obj + props = tool.Model.get_array_props(obj) if ArrayData.data["parameters"]: row = self.layout.row(align=True) @@ -291,7 +293,7 @@ class BIM_PT_stair(bpy.types.Panel): obj = context.active_object assert obj - props = obj.BIMStairProperties + props = tool.Model.get_stair_props(obj) if StairData.data["pset_data"]: row = self.layout.row(align=True) @@ -404,7 +406,9 @@ class BIM_PT_window(bpy.types.Panel): if not WindowData.is_loaded: WindowData.load() - props = context.active_object.BIMWindowProperties + obj = context.active_object + assert obj + props = tool.Model.get_window_props(obj) if WindowData.data["pset_data"]: row = self.layout.row(align=True) @@ -594,7 +598,9 @@ class BIM_PT_railing(bpy.types.Panel): if not RailingData.is_loaded: RailingData.load() - props = context.active_object.BIMRailingProperties + obj = context.active_object + assert obj + props = tool.Model.get_railing_props(obj) if RailingData.data["pset_data"]: row = self.layout.row(align=True) @@ -659,7 +665,7 @@ class BIM_PT_roof(bpy.types.Panel): obj = context.active_object assert obj - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) if RoofData.data["pset_data"]: row = self.layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index e940ec194d..e005a1b3ac 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -41,8 +41,10 @@ V_ = tool.Blender.V_ def update_window_modifier_representation(context: bpy.types.Context) -> None: obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMWindowProperties + assert element + props = tool.Model.get_window_props(obj) ifc_file = tool.Ifc.get() si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) @@ -258,7 +260,8 @@ def create_bm_window( def update_window_modifier_bmesh(context: bpy.types.Context) -> None: obj = context.active_object - props = obj.BIMWindowProperties + assert obj + props = tool.Model.get_window_props(obj) panel_schema = DEFAULT_PANEL_SCHEMAS[props.window_type] accumulated_height = [0] * len(panel_schema[0]) built_panels = [] @@ -447,8 +450,10 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMWindowProperties + assert element + props = tool.Model.get_window_props(obj) window_data = props.get_general_kwargs(convert_to_project_units=True) lining_props = props.get_lining_kwargs(convert_to_project_units=True) @@ -477,11 +482,13 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) + assert element data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data")) data.update(data.pop("lining_properties")) data.update(data.pop("panel_properties")) - props = obj.BIMWindowProperties + props = tool.Model.get_window_props(obj) props.set_props_kwargs_from_ifc_data(data) body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") @@ -506,8 +513,10 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMWindowProperties + assert element + props = tool.Model.get_window_props(obj) window_data = props.get_general_kwargs(convert_to_project_units=True) lining_props = props.get_lining_kwargs(convert_to_project_units=True) @@ -533,8 +542,10 @@ class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMWindowProperties + assert obj + props = tool.Model.get_window_props(obj) element = tool.Ifc.get_entity(obj) + assert element data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data")) data.update(data.pop("lining_properties")) data.update(data.pop("panel_properties")) @@ -553,9 +564,11 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMWindowProperties + assert obj element = tool.Ifc.get_entity(obj) - obj.BIMWindowProperties.is_editing = False + assert element + props = tool.Model.get_window_props(obj) + props.is_editing = False pset = tool.Pset.get_element_pset(element, "BBIM_Window") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index b35af2b5c9..d1ea3ec9ea 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1046,6 +1046,7 @@ class EditObjectUI: @classmethod def draw_modes(cls, context: bpy.types.Context) -> None: + obj = context.active_object ui_context = str(context.region.type) row = cls.layout.row(align=True) row.separator() @@ -1055,9 +1056,7 @@ class EditObjectUI: if len(context.selected_objects) == 1 and AuthoringData.data["has_extrusion"]: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(row, "Edit Profile", "S_E", "", ui_context) - elif ( - tool.Model.is_parametric_railing_active() and not context.active_object.BIMRailingProperties.is_editing_path - ): + elif tool.Model.is_parametric_railing_active() and not tool.Model.get_railing_props(obj).is_editing_path: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row row.operator( "bim.enable_editing_railing_path", @@ -1208,12 +1207,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): # and it might conflict with one of the conditions below if ( tool.Model.is_parametric_railing_active() - and not bpy.context.active_object.BIMRailingProperties.is_editing_path + and not tool.Model.get_railing_props(active_object).is_editing_path ): bpy.ops.bim.enable_editing_railing_path() return - elif tool.Model.is_parametric_roof_active() and not bpy.context.active_object.BIMRoofProperties.is_editing_path: + elif tool.Model.is_parametric_roof_active() and not tool.Model.get_roof_props(active_object).is_editing_path: # undo the unselection done above because roof has no usage type bpy.ops.bim.enable_editing_roof_path() return diff --git a/src/bonsai/bonsai/bim/module/nest/prop.py b/src/bonsai/bonsai/bim/module/nest/prop.py index 67fed8fe2d..d5e48b2479 100644 --- a/src/bonsai/bonsai/bim/module/nest/prop.py +++ b/src/bonsai/bonsai/bim/module/nest/prop.py @@ -40,7 +40,7 @@ def update_relating_object(self, context): if self.relating_object is None: return - if not self.relating_object.BIMObjectProperties.ifc_definition_id: + if not tool.Blender.get_ifc_definition_id(self.relating_object): context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO") self.relating_object = None diff --git a/src/bonsai/bonsai/bim/module/nest/ui.py b/src/bonsai/bonsai/bim/module/nest/ui.py index 1fd8f5ca4a..601fedf098 100644 --- a/src/bonsai/bonsai/bim/module/nest/ui.py +++ b/src/bonsai/bonsai/bim/module/nest/ui.py @@ -32,14 +32,14 @@ class BIM_PT_nest(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcObjectDefinition"): return False return True @@ -55,7 +55,7 @@ class BIM_PT_nest(Panel): row.prop(props, "relating_object", text="") if props.relating_object: op = row.operator("bim.nest_assign_object", icon="CHECKMARK", text="") - op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id + op.relating_object = tool.Blender.get_ifc_definition_id(props.relating_object) row.operator("bim.disable_editing_nest", icon="CANCEL", text="") else: row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index b91e0d8158..acc363bf16 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2327,11 +2327,12 @@ class RefreshClippingPlanes(bpy.types.Operator): else: break - def is_moved(self, obj): - if not obj.BIMObjectProperties.location_checksum: + def is_moved(self, obj: bpy.types.Object) -> bool: + props = tool.Blender.get_object_bim_props(obj) + if not props.location_checksum: return True # Let's be conservative - loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum)) - rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum)) + loc_check = np.frombuffer(eval(props.location_checksum)) + rot_check = np.frombuffer(eval(props.rotation_checksum)) loc_real = np.array(obj.matrix_world.translation).flatten() rot_real = np.array(obj.matrix_world.to_3x3()).flatten() if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2): diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 691d2e6930..e7a1301c6f 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -351,10 +351,9 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.AddEditProperties for obj in tool.Blender.get_selected_objects(): - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id - if not ifc_definition_id: + ifc_element = tool.Ifc.get_entity(obj) + if not ifc_element: continue - ifc_element = tool.Ifc.get().by_id(ifc_definition_id) for prop in props: value = getattr(prop, prop.get_value_name()) @@ -406,23 +405,19 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.DeletePsets for obj in tool.Blender.get_selected_objects(): - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id - if not ifc_definition_id: + ifc_element = tool.Ifc.get_entity(obj) + if not ifc_element: continue - ifc_element = tool.Ifc.get().by_id(ifc_definition_id) psets = ifcopenshell.util.element.get_psets(ifc_element) for prop in props: pset = prop.pset_name if pset in psets: try: - ifcopenshell.api.run( - "pset.remove_pset", + ifcopenshell.api.pset.remove_pset( self.file, - **{ - "product": self.file.by_id(ifc_definition_id), - "pset": self.file.by_id(psets[pset]["id"]), - }, + product=ifc_element, + pset=self.file.by_id(psets[pset]["id"]), ) except KeyError: pass # Sometimes the pset id is not found, I'm not sure why this happens though. - vulevukusej diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 4adda725dc..c155937da9 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -241,12 +241,12 @@ class BIM_PT_object_psets(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False return True @@ -319,12 +319,12 @@ class BIM_PT_object_qtos(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False return True diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 871b128db6..24db92e11f 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -648,7 +648,8 @@ def get_opening_area( """ total_opening_area = 0 ifc = tool.Ifc.get() - ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id) + ifc_element = tool.Ifc.get_entity(obj) + assert ifc_element if len(openings := ifc_element.HasOpenings) != 0: for opening in openings: opening_id = opening.RelatedOpeningElement.GlobalId @@ -887,7 +888,7 @@ def get_OBB_object(obj: bpy.types.Object) -> bpy.types.Object: :param blender-object obj: Blender Object :return blender-object: OBB of the Object """ - ifc_id = obj.BIMObjectProperties.ifc_definition_id + ifc_id = tool.Blender.get_ifc_definition_id(obj) bbox = obj.bound_box # matrix transformation to go from obj coordinates to world coordinates: obb = [Vector(v) for v in bbox] @@ -929,7 +930,7 @@ def get_AABB_object(obj: bpy.types.Object) -> bpy.types.Object: :param blender-object obj: Blender Object :return blender-object: AABB of the Object """ - ifc_id = obj.BIMObjectProperties.ifc_definition_id + ifc_id = tool.Blender.get_ifc_definition_id(obj) aabb_mesh = bpy.data.meshes.new(f"OBB_{ifc_id}") x = [v.co.x for v in obj.data.vertices] @@ -994,7 +995,7 @@ def get_bisected_obj( :param tuple(x,y,z) plane_no_neg: Tuple describing the normal vector of the lower bisection plane. Example: (0,0,-1) :return _type_: _description_ """ - ifc_id = obj.BIMObjectProperties.ifc_definition_id + ifc_id = tool.Blender.get_ifc_definition_id(obj) bis_obj = obj.copy() bis_obj.data = obj.data.copy() diff --git a/src/bonsai/bonsai/bim/module/qto/helper.py b/src/bonsai/bonsai/bim/module/qto/helper.py index 7265aa4400..47fd42642e 100644 --- a/src/bonsai/bonsai/bim/module/qto/helper.py +++ b/src/bonsai/bonsai/bim/module/qto/helper.py @@ -18,6 +18,7 @@ import bpy import bmesh +import bonsai.tool as tool from typing import Callable @@ -86,7 +87,7 @@ def calculate_formwork_area(objs: list[bpy.types.Object], context: bpy.types.Con bpy.ops.object.modifier_apply(modifier="Boolean") copied_obj.name = "Formwork" - copied_obj.BIMObjectProperties.ifc_definition_id = 0 + tool.Blender.get_object_bim_props(copied_obj).ifc_definition_id = 0 modifier = copied_obj.modifiers.new("Formwork", "REMESH") assert isinstance(modifier, bpy.types.RemeshModifier) modifier.mode = "SHARP" diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index a699287365..0e583e76cd 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -48,7 +48,8 @@ class EnableReassignClass(bpy.types.Operator): element = tool.Ifc.get_entity(obj) assert element ifc_class = element.is_a() - context.active_object.BIMObjectProperties.is_reassigning_class = True + props = tool.Blender.get_object_bim_props(obj) + props.is_reassigning_class = True ifc_products = tool.Root.get_ifc_products() schema = tool.Ifc.schema() declaration = schema.declaration_by_name(ifc_class) @@ -58,10 +59,10 @@ class EnableReassignClass(bpy.types.Operator): break else: self.report({"ERROR"}, f"Couldn't find matching IFC product for the selected object: '{element}'.") - obj.BIMObjectProperties.is_reassigning_class = False + props.is_reassigning_class = False return {"CANCELLED"} - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = self.file.by_id(tool.Blender.get_ifc_definition_id(obj)) rprops.ifc_class = element.is_a() rprops.relating_class_object = None if hasattr(element, "PredefinedType"): @@ -78,7 +79,8 @@ class DisableReassignClass(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.active_object.BIMObjectProperties.is_reassigning_class = False + props = tool.Blender.get_object_bim_props(context.active_object) + props.is_reassigning_class = False return {"FINISHED"} @@ -127,7 +129,8 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator): ) return {"CANCELLED"} - obj.BIMObjectProperties.is_reassigning_class = False + props = tool.Blender.get_object_bim_props(obj) + props.is_reassigning_class = False if element.is_a("IfcTypeObject"): elements_to_reassign[element] = ifc_class elements_to_update.update(ifcopenshell.util.element.get_types(element)) diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index 6985bf832b..5e9fa73eb0 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -43,7 +43,9 @@ class BIM_PT_class(Panel): def draw(self, context): if not IfcClassData.is_loaded: IfcClassData.load() - props = context.active_object.BIMObjectProperties + obj = context.active_object + assert obj + props = tool.Blender.get_object_bim_props(obj) rprops = tool.Root.get_root_props() if props.ifc_definition_id: if not IfcClassData.data["has_entity"]: diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 5f5854edd2..8e75097993 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -1449,15 +1449,17 @@ class LoadProductTasks(bpy.types.Operator): @classmethod def poll(cls, context): - if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)): cls.poll_message_set("No IFC object is active.") return False return True def execute(self, context): - result = core.load_product_related_tasks( - tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) - ) + obj = context.active_object + assert obj + product = tool.Ifc.get_entity(obj) + assert product + result = core.load_product_related_tasks(tool.Sequence, product=product) if isinstance(result, str): self.report({"INFO"}, result) else: diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index ce0e307076..a63a4928c0 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -271,10 +271,10 @@ def update_sort_reversed(self, context): def update_filter_by_active_schedule(self, context): - if context.active_object: - core.load_product_related_tasks( - tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) - ) + if obj := context.active_object: + product = tool.Ifc.get_entity(obj) + assert product + core.load_product_related_tasks(tool.Sequence, product=product) def switch_options(self, context): diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index ba08227179..c483e9d236 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -75,11 +75,13 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + oprops = tool.Blender.get_object_bim_props(obj) props = obj.BIMStructuralProperties file = tool.Ifc.get() related_structural_connection = file.by_id(oprops.ifc_definition_id) - relating_structural_member = file.by_id(props.relating_structural_member.BIMObjectProperties.ifc_definition_id) + relating_structural_member = tool.Ifc.get_entity(props.relating_structural_member) + assert relating_structural_member if not relating_structural_member.is_a("IfcStructuralMember"): return {"FINISHED"} ifcopenshell.api.structural.add_structural_member_connection( @@ -99,7 +101,6 @@ class EnableEditingStructuralConnectionCondition(bpy.types.Operator): def execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties props.active_connects_structural_member = self.connects_structural_member return {"FINISHED"} @@ -350,7 +351,8 @@ class EnableEditingStructuralItemAxis(bpy.types.Operator): def execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + oprops = tool.Blender.get_object_bim_props(obj) props = obj.BIMStructuralProperties self.file = tool.Ifc.get() @@ -403,7 +405,8 @@ class EditStructuralItemAxis(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + oprops = tool.Blender.get_object_bim_props(obj) props = obj.BIMStructuralProperties relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() z_axis = relative_matrix.col[2][0:3] @@ -425,11 +428,12 @@ class EnableEditingStructuralConnectionCS(bpy.types.Operator): def execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj props = obj.BIMStructuralProperties self.file = tool.Ifc.get() - item = self.file.by_id(oprops.ifc_definition_id) + item = tool.Ifc.get_entity(obj) + assert item location = obj.data.vertices[0].co empty = bpy.data.objects.new("Item Connection CS", None) @@ -491,16 +495,17 @@ class EditStructuralConnectionCS(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + item = tool.Ifc.get_entity(obj) + assert item props = obj.BIMStructuralProperties relative_matrix = props.ccs_empty.matrix_world @ obj.matrix_world.inverted() x_axis = relative_matrix.col[0][0:3] z_axis = relative_matrix.col[2][0:3] self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.edit_structural_connection_cs", + ifcopenshell.api.structural.edit_structural_connection_cs( self.file, - structural_item=self.file.by_id(oprops.ifc_definition_id), + structural_item=item, axis=z_axis, ref_direction=x_axis, ) @@ -695,9 +700,9 @@ class AddStructuralActivity(bpy.types.Operator, tool.Ifc.Operator): self.props = context.scene.BIMStructuralProperties self.file = tool.Ifc.get() for obj in context.selected_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + element = tool.Ifc.get_entity(obj) + if not element: continue - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) applied_load_class = self.props.applicable_structural_load_types allowed_load_classes = { diff --git a/src/bonsai/bonsai/bim/module/structural/ui.py b/src/bonsai/bonsai/bim/module/structural/ui.py index d8f69e232e..1e4ceee997 100644 --- a/src/bonsai/bonsai/bim/module/structural/ui.py +++ b/src/bonsai/bonsai/bim/module/structural/ui.py @@ -85,14 +85,14 @@ class BIM_PT_structural_boundary_conditions(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not (tool.Ifc.get_object_by_identifier(ifc_id)): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralConnection"): return False return True @@ -120,14 +120,14 @@ class BIM_PT_connected_structural_members(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not (tool.Ifc.get_object_by_identifier(ifc_id)): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralConnection"): return False return True @@ -173,14 +173,14 @@ class BIM_PT_structural_member(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralMember"): return False return True @@ -216,14 +216,14 @@ class BIM_PT_structural_connection(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralConnection"): return False return True @@ -315,7 +315,6 @@ class BIM_UL_structural_analysis_models(UIList): row.label(text=item.name) if context.active_object: - oprops = context.active_object.BIMObjectProperties if item.ifc_definition_id in StructuralAnalysisModelsData.data["active_model_ids"]: op = row.operator( "bim.unassign_structural_analysis_model", text="", icon="KEYFRAME_HLT", emboss=False diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index b09181c327..0d500caa15 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -187,9 +187,8 @@ class BIM_PT_ports(Panel): if connected_obj_name: connected_obj = bpy.data.objects[connected_obj_name] cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id() - cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - connected_obj.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) + cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id cols[5].label(text=connected_obj_name) else: cols[3].label(text="", icon="UNLINKED") @@ -244,9 +243,8 @@ class BIM_PT_port(Panel): relating_object = bpy.data.objects[relating_object_name] row.label(text="Port located on:") row.label(text=relating_object_name) - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - relating_object.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(relating_object) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id # object connected to the port row = layout.row(align=True) @@ -255,9 +253,8 @@ class BIM_PT_port(Panel): connected_object = bpy.data.objects[connected_object_name] row.label(text="Port connected to:") row.label(text=connected_object_name) - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - connected_object.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(connected_object) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id else: row.label(text="Port is not connected to any element") @@ -304,9 +301,8 @@ class BIM_PT_flow_controls(Panel): op.flow_control = control_id op.flow_element = flow_element_id op.assign = False - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - displayed_object.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(displayed_object) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id row.label(text=f"{displayed_object_name}") element = tool.Ifc.get_entity(context.active_object) diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index b16abd3fcd..a438bdcfb5 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -150,7 +150,8 @@ class SelectRequirement(bpy.types.Operator): area.spaces[0].shading.show_xray = True failed_ids = [e["id"] for e in failed_entities] for obj in context.scene.objects: - if obj.BIMObjectProperties.ifc_definition_id in failed_ids: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if ifc_id in failed_ids: obj.color = (1, 0, 0, 1) else: obj.color = (1, 1, 1, 1) @@ -175,7 +176,8 @@ class SelectFailedEntities(bpy.types.Operator): failed_ids = [e["id"] for e in failed_entities] for obj in context.scene.objects: - if obj.BIMObjectProperties.ifc_definition_id in failed_ids: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if ifc_id in failed_ids: obj.select_set(True) else: obj.select_set(False) diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index ee370e5ae5..89ac192a65 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -337,5 +337,5 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): # Set duplicated type as active in current tool. if ifc_class in (i[0] for i in (bonsai.bim.helper.get_enum_items(props, "ifc_class", context) or ()) if i): props.ifc_class = new.is_a() - props.relating_type_id = str(new_obj.BIMObjectProperties.ifc_definition_id) + props.relating_type_id = str(tool.Blender.get_ifc_definition_id(new_obj)) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py index 405507f168..340662cb3a 100644 --- a/src/bonsai/bonsai/bim/module/type/ui.py +++ b/src/bonsai/bonsai/bim/module/type/ui.py @@ -46,7 +46,9 @@ class BIM_PT_type(Panel): if not TypeData.is_loaded: TypeData.load() - oprops = context.active_object.BIMObjectProperties + obj = context.active_object + assert obj + oprops = tool.Blender.get_object_bim_props(obj) if TypeData.data["is_product"]: self.draw_product_ui(context) @@ -54,21 +56,19 @@ class BIM_PT_type(Panel): self.draw_type_ui(context) def draw_type_ui(self, context): - props = context.active_object.BIMTypeProperties - oprops = context.active_object.BIMObjectProperties + oprops = tool.Blender.get_object_bim_props(context.active_object) row = self.layout.row(align=True) row.label(text=f"{TypeData.data['total_instances']} Typed Objects") select_type_objects_row = row.row(align=True) select_type_objects_row.operator("bim.select_type_objects", icon="RESTRICT_SELECT_OFF", text="") select_type_objects_row.enabled = int(TypeData.data["total_instances"]) > 0 op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="") - op.element = context.active_object.BIMObjectProperties.ifc_definition_id + op.element = oprops.ifc_definition_id row.operator("bim.auto_rename_occurrences", icon="ITALIC", text="") def draw_product_ui(self, context): layout = self.layout props = context.active_object.BIMTypeProperties - oprops = context.active_object.BIMObjectProperties if props.is_editing_type: row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py index 91537ed528..3274b974d7 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -215,8 +215,8 @@ class AddFilling(bpy.types.Operator, tool.Ifc.Operator): if opening is None: return {"FINISHED"} self.file = tool.Ifc.get() - element_id = obj.BIMObjectProperties.ifc_definition_id - opening_id = opening.BIMObjectProperties.ifc_definition_id + element_id = tool.Blender.get_object_bim_props(obj).ifc_definition_id + opening_id = tool.Blender.get_object_bim_props(opening).ifc_definition_id if not element_id or not opening_id or element_id == opening_id: return {"FINISHED"} ifcopenshell.api.run( diff --git a/src/bonsai/bonsai/bim/module/void/ui.py b/src/bonsai/bonsai/bim/module/void/ui.py index 69b34a40e1..66701510e3 100644 --- a/src/bonsai/bonsai/bim/module/void/ui.py +++ b/src/bonsai/bonsai/bim/module/void/ui.py @@ -52,8 +52,6 @@ class BIM_PT_voids(Panel): if not VoidsData.is_loaded: VoidsData.load() - props = context.active_object.BIMObjectProperties - if len(context.selected_objects) >= 2: row = self.layout.row(align=True) op = row.operator("bim.add_opening", icon="ADD", text="Add Opening") diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index f32cd2ef65..9b0d38c5ab 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -857,7 +857,9 @@ class FetchObjectPassport(bpy.types.Operator): def execute(self, context): # TODO: this is dead code, awaiting reimplementation. See #1222. - for reference in context.active_object.BIMObjectProperties.document_references: + obj = context.active_object + props = tool.Blender.get_object_bim_props(obj) + for reference in props.document_references: bim_props = tool.Blender.get_bim_props() reference = bim_props.document_references[reference.name] if reference.location[-6:] == ".blend": diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index a4a2ed4558..7a91cce678 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -41,7 +41,7 @@ from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, G from typing_extensions import assert_never if TYPE_CHECKING: - from bonsai.bim.prop import BIMProperties + from bonsai.bim.prop import BIMProperties, BIMObjectProperties VIEWPORT_ATTRIBUTES = [ @@ -192,7 +192,8 @@ class Blender(bonsai.core.tool.Blender): if context is None: context = bpy.context if obj_type == "Object": - return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id + props = tool.Blender.get_object_bim_props(bpy.data.objects[obj]) + return props.ifc_definition_id elif obj_type == "Material": props = tool.Material.get_material_props() return props.materials[props.active_material_index].ifc_definition_id @@ -221,7 +222,8 @@ class Blender(bonsai.core.tool.Blender): @classmethod def is_ifc_object(cls, obj: bpy.types.Object) -> bool: - return bool(obj.BIMObjectProperties.ifc_definition_id) + props = tool.Blender.get_object_bim_props(obj) + return bool(props.ifc_definition_id) @classmethod def is_ifc_class_active(cls, ifc_class: str) -> bool: @@ -880,7 +882,7 @@ class Blender(bonsai.core.tool.Blender): @classmethod def get_layer_collection(cls, collection: bpy.types.Collection) -> Union[bpy.types.LayerCollection, None]: project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + project_collection = tool.Blender.get_object_bim_props(project).collection for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: @@ -1019,23 +1021,28 @@ class Blender(bonsai.core.tool.Blender): @classmethod def is_editing_railing_path(cls, obj: bpy.types.Object): - return obj.BIMRailingProperties.is_editing_path + props = tool.Model.get_railing_props(obj) + return props.is_editing_path @classmethod def is_editing_roof_path(cls, obj: bpy.types.Object) -> bool: - return obj.BIMRoofProperties.is_editing_path + props = tool.Model.get_roof_props(obj) + return props.is_editing_path @classmethod def is_editing_railing_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMRailingProperties.is_editing + props = tool.Model.get_railing_props(obj) + return props.is_editing @classmethod def is_editing_roof_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMRoofProperties.is_editing + props = tool.Model.get_roof_props(obj) + return props.is_editing @classmethod def is_editing_window_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMWindowProperties.is_editing + props = tool.Model.get_window_props(obj) + return props.is_editing @classmethod def is_editing_door_parameters(cls, obj: bpy.types.Object) -> bool: @@ -1044,7 +1051,8 @@ class Blender(bonsai.core.tool.Blender): @classmethod def is_editing_stair_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMStairProperties.is_editing + props = tool.Model.get_stair_props(obj) + return props.is_editing @classmethod def is_modifier_with_non_editable_path(cls, element: entity_instance) -> bool: @@ -1545,3 +1553,11 @@ class Blender(bonsai.core.tool.Blender): if scene is None: scene = bpy.context.scene return scene.BIMProperties + + @classmethod + def get_object_bim_props(cls, obj: bpy.types.Object) -> BIMObjectProperties: + return obj.BIMObjectProperties + + @classmethod + def get_ifc_definition_id(cls, obj: bpy.types.Object) -> int: + return tool.Blender.get_object_bim_props(obj).ifc_definition_id diff --git a/src/bonsai/bonsai/tool/cad.py b/src/bonsai/bonsai/tool/cad.py index 9812497967..8b2b34dae2 100644 --- a/src/bonsai/bonsai/tool/cad.py +++ b/src/bonsai/bonsai/tool/cad.py @@ -28,7 +28,7 @@ # - An arc is reconstructed from 3 points instead of a full circle # - You can now derive the center from an arc without generating geometry - +from __future__ import annotations import sys import bpy import math @@ -36,12 +36,20 @@ import bmesh import mathutils.geometry from mathutils import Vector, Matrix, geometry import itertools +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.cad.prop import BIMCadProperties VTX_PRECISION = 1.0e-5 class Cad: + @classmethod + def get_cad_props(cls) -> BIMCadProperties: + return bpy.context.scene.BIMCadProperties + @classmethod def is_point_on_edge(cls, p, edge): """ diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 2f669fe32e..a93b5e405f 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -29,7 +29,7 @@ class Collector(bonsai.core.tool.Collector): """Links an object to an appropriate Blender collection.""" if should_clean_users_collection: for users_collection in obj.users_collection: - if obj.BIMObjectProperties.collection == users_collection: + if tool.Blender.get_object_bim_props(obj).collection == users_collection: continue # Users are free to use extra collections for their own # purposes except for the reserved keyword "Ifc" and @@ -87,7 +87,7 @@ class Collector(bonsai.core.tool.Collector): if collection := cls._create_own_collection(obj): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - cls.link_collection_child_safe(project_obj.BIMObjectProperties.collection, collection) + cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) elif ( tool.Ifc.get_schema() != "IFC2X3" and element.is_a("IfcSpatialElement") @@ -98,21 +98,21 @@ class Collector(bonsai.core.tool.Collector): if collection := cls._create_own_collection(obj): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - cls.link_collection_child_safe(project_obj.BIMObjectProperties.collection, collection) + cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": if collection := cls._create_own_collection(obj): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - cls.link_collection_child_safe(project_obj.BIMObjectProperties.collection, collection) + cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) elif element.is_a("IfcAnnotation") and (drawing_obj := cls.get_annotation_drawing_obj(element)): - cls.link_collection_object_safe(drawing_obj.BIMObjectProperties.collection, obj) + cls.link_collection_object_safe(tool.Blender.get_object_bim_props(drawing_obj).collection, obj) elif container := ifcopenshell.util.element.get_container(element): while container.is_a("IfcSpace"): container = ifcopenshell.util.element.get_aggregate(container) container_obj = tool.Ifc.get_object(container) - if not (collection := container_obj.BIMObjectProperties.collection): + if not (collection := tool.Blender.get_object_bim_props(container_obj).collection): cls.assign(container_obj) - collection = container_obj.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(container_obj).collection cls.link_collection_object_safe(collection, obj) else: collection = cls._create_project_child_collection("Unsorted") @@ -128,7 +128,7 @@ class Collector(bonsai.core.tool.Collector): return collection collection = bpy.data.collections.new(name) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_obj.BIMObjectProperties.collection.children.link(collection) + tool.Blender.get_object_bim_props(project_obj).collection.children.link(collection) if layer_collection := tool.Blender.get_layer_collection(collection): cls.set_layer_collection_visibility(layer_collection) return collection @@ -136,11 +136,12 @@ class Collector(bonsai.core.tool.Collector): @classmethod def _create_own_collection(cls, obj: bpy.types.Object) -> bpy.types.Collection: """get or create own collection for the element""" - if obj.BIMObjectProperties.collection: - obj.BIMObjectProperties.collection.name = obj.name + props = tool.Blender.get_object_bim_props(obj) + if props.collection: + props.collection.name = obj.name return collection = bpy.data.collections.new(obj.name) - obj.BIMObjectProperties.collection = collection + props.collection = collection collection.BIMCollectionProperties.obj = obj return collection @@ -184,7 +185,8 @@ class Collector(bonsai.core.tool.Collector): @classmethod def reset_default_visibility(cls) -> None: project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + assert project + project_collection = tool.Blender.get_object_bim_props(project).collection for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: diff --git a/src/bonsai/bonsai/tool/covering.py b/src/bonsai/bonsai/tool/covering.py index 5141cbffbd..c83a6f3e09 100644 --- a/src/bonsai/bonsai/tool/covering.py +++ b/src/bonsai/bonsai/tool/covering.py @@ -16,17 +16,26 @@ # 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.util.element import bonsai.core.tool import bonsai.tool as tool +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.covering.prop import BIMCoveringProperties class Covering(bonsai.core.tool.Covering): + @classmethod + def get_covering_props(cls) -> BIMCoveringProperties: + return bpy.context.scene.BIMCoveringProperties + @classmethod def get_z_from_ceiling_height(cls) -> float: - props = bpy.context.scene.BIMCoveringProperties + props = cls.get_covering_props() return props.ceiling_height # def toggle_spaces_visibility_wired_and_textured(cls, spaces): diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 9f72fd51ee..c223b3ee58 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -524,7 +524,7 @@ class Drawing(bonsai.core.tool.Drawing): def get_drawing_collection(cls, drawing: ifcopenshell.entity_instance) -> Union[bpy.types.Collection, None]: obj = tool.Ifc.get_object(drawing) if obj: - return obj.BIMObjectProperties.collection + return tool.Blender.get_object_bim_props(obj).collection @classmethod def get_drawing_group(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: @@ -1516,8 +1516,8 @@ class Drawing(bonsai.core.tool.Drawing): dst = src.copy() dst.data = dst.data.copy() dst.name = dst.name.replace("IfcGridAxis/", "") - dst.BIMObjectProperties.ifc_definition_id = 0 - tool.Geometry.get_geometry_props(dst).ifc_definition_id = 0 + tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0 + tool.Geometry.get_geometry_props(dst.data).ifc_definition_id = 0 return dst def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]: @@ -1888,7 +1888,7 @@ class Drawing(bonsai.core.tool.Drawing): return bool( camera is not None and camera.type == "CAMERA" - and camera.BIMObjectProperties.ifc_definition_id + and tool.Blender.get_ifc_definition_id(camera) and area is not None ) @@ -1910,11 +1910,12 @@ class Drawing(bonsai.core.tool.Drawing): def isolate_camera_collection(cls, camera: bpy.types.Object) -> None: drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] drawing_collections = [] - camera_collection = camera.BIMObjectProperties.collection + camera_collection = tool.Blender.get_object_bim_props(camera).collection for drawing in drawings: if not (drawing_obj := tool.Ifc.get_object(drawing)): continue - if not (drawing_collection := drawing_obj.BIMObjectProperties.collection): + oprops = tool.Blender.get_object_bim_props(drawing_obj) + if not (drawing_collection := oprops.collection): continue if drawing_obj == camera: drawing_collection.hide_render = False @@ -1922,7 +1923,7 @@ class Drawing(bonsai.core.tool.Drawing): drawing_collection.hide_render = True project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + project_collection = tool.Blender.get_object_bim_props(project).collection for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 4947666bd6..56c8e275c6 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -235,7 +235,7 @@ class Geometry(bonsai.core.tool.Geometry): bpy.data.objects.remove(axis_obj) ifcopenshell.api.grid.remove_grid_axis(tool.Ifc.get(), axis=axis) - collection = obj.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(obj).collection if collection: parent = ifcopenshell.util.element.get_aggregate(element) if not parent: @@ -243,7 +243,7 @@ class Geometry(bonsai.core.tool.Geometry): if parent: parent_obj = tool.Ifc.get_object(parent) if parent_obj: - parent_collection = parent_obj.BIMObjectProperties.collection + parent_collection = tool.Blender.get_object_bim_props(parent_obj).collection for child in collection.children: parent_collection.children.link(child) for child_object in collection.objects: @@ -557,11 +557,9 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_cartesian_point_offset(cls, obj: bpy.types.Object) -> npt.NDArray[np.float64] | None: - if ( - obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT" - and obj.BIMObjectProperties.cartesian_point_offset - ): - return np.array(tuple(map(float, obj.BIMObjectProperties.cartesian_point_offset.split(",")))) + props = tool.Blender.get_object_bim_props(obj) + if props.blender_offset_type == "CARTESIAN_POINT" and props.cartesian_point_offset: + return np.array(tuple(map(float, props.cartesian_point_offset.split(",")))) @classmethod def get_element_type(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: @@ -1084,8 +1082,9 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def record_object_position(cls, obj: bpy.types.Object) -> None: # These are recorded separately because they have different numerical tolerances - obj.BIMObjectProperties.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes()) - obj.BIMObjectProperties.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes()) + props = tool.Blender.get_object_bim_props(obj) + props.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes()) + props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes()) @classmethod def remove_connection(cls, connection: ifcopenshell.entity_instance) -> None: @@ -1570,8 +1569,9 @@ class Geometry(bonsai.core.tool.Geometry): def get_blender_offset_type(cls, obj: bpy.types.Object) -> Optional[str]: props = tool.Georeference.get_georeference_props() if props.has_blender_offset: - if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE": - result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + props = tool.Blender.get_object_bim_props(obj) + if (result := props.blender_offset_type) == "NONE": + result = props.blender_offset_type = "OBJECT_PLACEMENT" return result @classmethod diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index ce9f856f80..5f68fc4b0f 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -77,13 +77,14 @@ class Ifc(bonsai.core.tool.Ifc): return False if element and (element.is_a("IfcTypeProduct") or element.is_a("IfcProject")): return False - if not obj.BIMObjectProperties.location_checksum: + oprops = tool.Blender.get_object_bim_props(obj) + if not oprops.location_checksum: return True # Let's be conservative - loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum)) + loc_check = np.frombuffer(eval(oprops.location_checksum)) loc_real = np.array(obj.matrix_world.translation).flatten() if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm return True - rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum)).reshape(3, 3) + rot_check = np.frombuffer(eval(oprops.rotation_checksum)).reshape(3, 3) rot_real = np.array(obj.matrix_world.to_3x3()) rot_dot = np.dot(rot_check, rot_real.T) angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1)) @@ -107,7 +108,7 @@ class Ifc(bonsai.core.tool.Ifc): props = None if isinstance(obj, bpy.types.Object): - props = obj.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) elif isinstance(obj, bpy.types.Material): props = obj.BIMStyleProperties else: diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index 93475c090c..e306271ebd 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -397,9 +397,9 @@ class IfcGit: bpy.ops.object.select_all(action="DESELECT") for obj in bpy.context.visible_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not (step_id := props.ifc_definition_id): continue - step_id = obj.BIMObjectProperties.ifc_definition_id if step_id in step_ids["modified"]: obj.color = (0.3, 0.3, 1.0, 1) obj.select_set(True) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 7fb5fc4788..7a5576a1e2 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -929,6 +929,7 @@ class Loader(bonsai.core.tool.Loader): @classmethod def apply_blender_offset_to_matrix_world(cls, obj: bpy.types.Object, matrix: np.ndarray) -> Matrix: + oprops = tool.Blender.get_object_bim_props(obj) if ( not obj.data and tool.Cad.is_x(matrix[0][3], 0) @@ -939,13 +940,13 @@ class Loader(bonsai.core.tool.Loader): # positionally significant and is left alone. This handles # scenarios where often spatial elements are left at 0,0,0 and # everything else is at map coordinates. - obj.BIMObjectProperties.blender_offset_type = "NOT_APPLICABLE" + oprops.blender_offset_type = "NOT_APPLICABLE" return Matrix(matrix.tolist()) if obj.data and obj.data.get("has_cartesian_point_offset", None): - obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + oprops.blender_offset_type = "CARTESIAN_POINT" if cartesian_point_offset := obj.data.get("cartesian_point_offset", None): - obj.BIMObjectProperties.cartesian_point_offset = cartesian_point_offset + oprops.cartesian_point_offset = cartesian_point_offset offset_xyz = list(map(float, cartesian_point_offset.split(","))) + [1.0] offset_xyz = matrix @ offset_xyz matrix[0][3] = offset_xyz[0] @@ -954,8 +955,8 @@ class Loader(bonsai.core.tool.Loader): props = tool.Georeference.get_georeference_props() if props.has_blender_offset: - if obj.BIMObjectProperties.blender_offset_type == "NONE": - obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + if oprops.blender_offset_type == "NONE": + oprops.blender_offset_type = "OBJECT_PLACEMENT" matrix = ifcopenshell.util.geolocation.global2local( matrix, float(props.blender_offset_x) * cls.unit_scale, diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 0596971e53..27cb0180bf 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -55,7 +55,15 @@ T = TypeVar("T") V_ = tool.Blender.V_ if TYPE_CHECKING: - from bonsai.bim.module.model.prop import BIMModelProperties, BIMDoorProperties + from bonsai.bim.module.model.prop import ( + BIMModelProperties, + BIMDoorProperties, + BIMArrayProperties, + BIMRoofProperties, + BIMWindowProperties, + BIMStairProperties, + BIMRailingProperties, + ) class Model(bonsai.core.tool.Model): @@ -67,6 +75,26 @@ class Model(bonsai.core.tool.Model): def get_door_props(cls, obj: bpy.types.Object) -> BIMDoorProperties: return obj.BIMDoorProperties + @classmethod + def get_window_props(cls, obj: bpy.types.Object) -> BIMWindowProperties: + return obj.BIMWindowProperties + + @classmethod + def get_stair_props(cls, obj: bpy.types.Object) -> BIMStairProperties: + return obj.BIMStairProperties + + @classmethod + def get_roof_props(cls, obj: bpy.types.Object) -> BIMRoofProperties: + return obj.BIMRoofProperties + + @classmethod + def get_railing_props(cls, obj: bpy.types.Object) -> BIMRailingProperties: + return obj.BIMRailingProperties + + @classmethod + def get_array_props(cls, obj: bpy.types.Object) -> BIMArrayProperties: + return obj.BIMArrayProperties + @classmethod def convert_si_to_unit(cls, value: T) -> T: if isinstance(value, (tuple, list)): @@ -1989,9 +2017,9 @@ class Model(bonsai.core.tool.Model): if z < 0 and y < 0: y = abs(y) z = abs(z) - if z < 0 and y >=0: + if z < 0 and y >= 0: vector = Vector((0, -1)) - + x_angle = vector.angle_signed(Vector((y, z))) return x_angle diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 6e92d8cf5a..4c824f8b62 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -409,7 +409,8 @@ class Root(bonsai.core.tool.Root): def set_object_name(cls, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: name = tool.Loader.get_name(element) if obj.name != name: - obj.BIMObjectProperties.is_renaming = True + props = tool.Blender.get_object_bim_props(obj) + props.is_renaming = True obj.name = name # The handler will trigger, and reset is_renaming to False @classmethod diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 4dd4cfbb99..046906387e 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -1158,7 +1158,7 @@ class Sequence(bonsai.core.tool.Sequence): bpy.context.scene.frame_start = 1 bpy.context.scene.frame_end = 2 for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue obj.color = (1.0, 1.0, 1.0, 1) obj.hide_viewport = False @@ -1317,7 +1317,7 @@ class Sequence(bonsai.core.tool.Sequence): @classmethod def clear_objects_animation(cls, include_blender_objects=True): for obj in bpy.data.objects: - if not include_blender_objects and not obj.BIMObjectProperties.ifc_definition_id: + if not include_blender_objects and not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue cls.clear_object_animation(obj) cls.clear_object_color(obj) @@ -1326,13 +1326,14 @@ class Sequence(bonsai.core.tool.Sequence): @classmethod def animate_objects(cls, settings, frames, animation_type=""): for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id: + element = tool.Ifc.get_entity(obj) + if not element: continue - if tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id).is_a("IfcSpace"): + if element.is_a("IfcSpace"): cls.hide_object(obj) continue cls.earliest_frame = None - product_frames = frames.get(obj.BIMObjectProperties.ifc_definition_id, []) + product_frames = frames.get(element.id(), []) for product_frame in product_frames: if product_frame["relationship"] == "input": cls.animate_input(obj, settings["start_frame"], product_frame, animation_type) diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 2d400229c5..4e6f8dc457 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -1180,9 +1180,9 @@ class Spatial(bonsai.core.tool.Spatial): SpatialDecompositionData.data["default_container"] = SpatialDecompositionData.default_container() project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + project_collection = tool.Blender.get_object_bim_props(project).collection obj = tool.Ifc.get_object(container) - if obj and (collection := obj.BIMObjectProperties.collection): + if obj and (collection := tool.Blender.get_object_bim_props(obj).collection): for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: diff --git a/src/bonsai/bonsai/tool/structural.py b/src/bonsai/bonsai/tool/structural.py index 30cbb00191..5796bf6f44 100644 --- a/src/bonsai/bonsai/tool/structural.py +++ b/src/bonsai/bonsai/tool/structural.py @@ -110,7 +110,8 @@ class Structural(bonsai.core.tool.Structural): def get_product_or_active_object(cls, product: str) -> Union[bpy.types.Object, None]: product = bpy.data.objects.get(product) if product else bpy.context.active_object try: - if product.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(product) + if props.ifc_definition_id: return product else: return None diff --git a/src/bonsai/bonsai/tool/surveyor.py b/src/bonsai/bonsai/tool/surveyor.py index 28856a19f8..7c203586f2 100644 --- a/src/bonsai/bonsai/tool/surveyor.py +++ b/src/bonsai/bonsai/tool/surveyor.py @@ -33,7 +33,7 @@ class Surveyor(bonsai.core.tool.Surveyor): M_TRANSLATION = (slice(0, 3), 3) matrix = np.array(obj.matrix_world) props = tool.Georeference.get_georeference_props() - if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE": + if props.has_blender_offset and tool.Blender.get_object_bim_props(obj).blender_offset_type != "NOT_APPLICABLE": unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) coordinate_offset = tool.Geometry.get_cartesian_point_offset(obj) if coordinate_offset is not None: diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 51334be3a2..b7eae11989 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -189,7 +189,7 @@ class System(bonsai.core.tool.System): container = ifcopenshell.util.element.get_container(element) if container: - collection = tool.Ifc.get_object(container).BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection ifc_importer.collections[container.GlobalId] = collection ifc_importer.place_objects_in_collections() diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index 308d140298..6d997bdd7f 100644 --- a/src/bonsai/test/bim/bootstrap.py +++ b/src/bonsai/test/bim/bootstrap.py @@ -192,12 +192,12 @@ def the_object_name_does_not_exist(name): def the_object_name_is_an_ifc_class(name, ifc_class): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert element.is_a(ifc_class), f'Object "{name}" is an {element.is_a()}' def the_object_name_is_not_an_ifc_element(name): - id = the_object_name_exists(name).BIMObjectProperties.ifc_definition_id + id = tool.Blender.get_ifc_definition_id(the_object_name_exists(name)) assert id == 0, f"The ID is {id}" @@ -229,7 +229,7 @@ def the_object_name_is_placed_in_the_collection_collection(name, collection): def the_object_name_has_a_type_representation_of_context(name, type, context): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) context, subcontext, target_view = context.split("/") assert ifcopenshell.util.representation.get_representation( element, context, subcontext or None, target_view or None @@ -238,7 +238,7 @@ def the_object_name_has_a_type_representation_of_context(name, type, context): def the_object_name_is_contained_in_container_name(name, container_name): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) container = ifcopenshell.util.element.get_container(element) if not container: assert False, f'Object "{name}" is not in any container' @@ -257,8 +257,8 @@ def i_delete_the_selected_objects(): def the_object_name1_and_name2_are_different_elements(name1, name2): ifc = an_ifc_file_exists() - element1 = ifc.by_id(the_object_name_exists(name1).BIMObjectProperties.ifc_definition_id) - element2 = ifc.by_id(the_object_name_exists(name2).BIMObjectProperties.ifc_definition_id) + element1 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name1))) + element2 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name2))) assert element1 != element2, f"Objects {name1} and {name2} have same elements {element1} and {element2}" @@ -284,7 +284,7 @@ def the_object_name1_has_no_boolean_difference_by_name2(name1, name2): def the_object_name_is_voided_by_void(name, void): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) for rel in element.HasOpenings: if rel.RelatedOpeningElement.Name == void: return True @@ -293,7 +293,7 @@ def the_object_name_is_voided_by_void(name, void): def the_object_name_is_not_voided_by_void(name, void): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) for rel in element.HasOpenings: if rel.RelatedOpeningElement.Name == void: assert False, "A void was found" @@ -301,21 +301,21 @@ def the_object_name_is_not_voided_by_void(name, void): def the_object_name_is_not_voided(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(element.HasOpenings): assert False, "An opening was found" def the_object_name_is_not_a_void(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(element.VoidsElements): assert False, "A void was found" def the_void_name_is_filled_by_filling(name, filling): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): return True assert False, "No filling found" @@ -323,14 +323,14 @@ def the_void_name_is_filled_by_filling(name, filling): def the_void_name_is_not_filled_by_filling(name, filling): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): assert False, "A filling was found" def the_object_name_is_not_a_filling(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(element.FillsVoids): assert False, "A filling was found" diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 1332c63e8e..2163489685 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -718,8 +718,8 @@ def the_collection_exclude_status_is(name: str, exclude: str) -> None: @then(parsers.parse('the object "{name1}" and "{name2}" are different elements')) def the_object_name1_and_name2_are_different_elements(name1, name2): ifc = an_ifc_file_exists() - element1 = ifc.by_id(the_object_name_exists(name1).BIMObjectProperties.ifc_definition_id) - element2 = ifc.by_id(the_object_name_exists(name2).BIMObjectProperties.ifc_definition_id) + element1 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name1))) + element2 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name2))) assert element1 != element2, f"Objects {name1} and {name2} have same elements {element1} and {element2}" @@ -732,7 +732,7 @@ def the_object_name_has_a_body_of_value(name, value): @then(parsers.parse('the object "{name}" has a "{type}" representation of "{context}"')) def the_object_name_has_a_representation_type_of_context(name, type, context): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) context, subcontext, target_view = context.split("/") rep = ifcopenshell.util.representation.get_representation(element, context, subcontext or None, target_view or None) assert rep @@ -808,7 +808,7 @@ def the_object_name_should_display_as_mode(name, mode): @then(parsers.parse('the object "{name}" is voided by "{void}"')) def the_object_name_is_voided_by_void(name, void): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert any((rel for rel in element.HasOpenings if rel.RelatedOpeningElement.Name == void)), "No void found" @@ -824,7 +824,7 @@ def the_object_name_is_not_voided_by_void(name, void): @then(parsers.parse('the object "{name}" is not voided')) def the_object_name_is_not_voided(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert not element.HasOpenings, "A void was found" @@ -832,7 +832,7 @@ def the_object_name_is_not_voided(name): def the_object_name_is_a_void(name): ifc = tool.Ifc.get() obj = the_object_name_exists(name) - element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(obj)) assert any((element.VoidsElements)), "No void was found" @@ -872,14 +872,14 @@ def the_object_name_is_not_visible(name): @then(parsers.parse('the object "{name}" is an "{ifc_class}"')) def the_object_name_is_an_ifc_class(name, ifc_class): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert element.is_a(ifc_class), f'Object "{name}" is an {element.is_a()}' @then(parsers.parse('the object "{name}" is not an IFC element')) def the_object_name_is_not_an_ifc_element(name): obj = the_object_name_exists(name) - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id == 0, f"The object {obj} has an ID of {ifc_definition_id}" @@ -897,14 +897,14 @@ def the_object_name_has_ifc_representation_data(name): @then(parsers.parse('the material "{name}" is an IFC material')) def the_material_name_is_an_ifc_material(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id != 0, f"The material {obj} has no ID: {ifc_definition_id}" @then(parsers.parse('the material "{name}" is not an IFC material')) def the_material_name_is_not_an_ifc_material(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id == 0, f"The material {obj} has an ID of {ifc_definition_id}" @@ -937,7 +937,7 @@ def the_object_name_has_number_vertices(name, number): @then(parsers.parse('the void "{name}" is filled by "{filling}"')) def the_void_name_is_filled_by_filling(name, filling): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert any((rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings)), "No filling found" @@ -954,7 +954,7 @@ def the_void_name_is_not_filled_by_filling(name, filling): @then(parsers.parse('the object "{name}" is not a filling')) def the_object_name_is_not_a_filling(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert not any(element.FillsVoids), "A filling was found" @@ -1010,8 +1010,9 @@ def prop_is_roughly_value(prop, value): def the_object_name_has_a_cartesian_point_offset_of_offset(name: str, offset: str) -> None: offset = replace_variables(offset) obj = the_object_name_exists(name) - assert obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT" - obj_offset = np.array(tuple(map(float, obj.BIMObjectProperties.cartesian_point_offset.split(",")))) + props = tool.Blender.get_object_props(obj) + assert props.blender_offset_type == "CARTESIAN_POINT" + obj_offset = np.array(tuple(map(float, props.cartesian_point_offset.split(",")))) offset = np.array(tuple(map(float, offset.split(",")))) assert np.allclose(obj_offset, offset) @@ -1169,7 +1170,7 @@ def the_object_name_bottom_left_corner_is_at_location(name, location): @then(parsers.parse('the object "{name}" is contained in "{container_name}"')) def the_object_name_is_contained_in_container_name(name, container_name): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) container = ifcopenshell.util.element.get_container(element) if not container: assert False, f'Object "{name}" is not in any container' @@ -1179,7 +1180,7 @@ def the_object_name_is_contained_in_container_name(name, container_name): @then(parsers.parse('the object "{name}" is contained in object "{container_name}"')) def the_object_name_is_contained_in_object_container_name(name: str, container_name: str) -> None: ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) container = ifcopenshell.util.element.get_container(element) if not container: assert False, f'Object "{name}" is not in any container' @@ -1190,7 +1191,7 @@ def the_object_name_is_contained_in_object_container_name(name: str, container_n @then(parsers.parse('the object "{name}" is aggregated by object "{aggregate_name}"')) def the_object_name_is_aggregated_by_object_aggregate_name(name: str, aggregate_name: str) -> None: ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) aggregate = ifcopenshell.util.element.get_aggregate(element) if not aggregate: assert False, f'Object "{name}" is not aggregated by any element' @@ -1201,7 +1202,7 @@ def the_object_name_is_aggregated_by_object_aggregate_name(name: str, aggregate_ @then(parsers.parse('the object "{name}" has no aggregate')) def the_object_name_has_no_aggregate(name: str) -> None: ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) aggregate = ifcopenshell.util.element.get_aggregate(element) if aggregate: assert False, f'Object "{name}" is aggregated by element "{aggregate}"' diff --git a/src/bonsai/test/tool/test_collector.py b/src/bonsai/test/tool/test_collector.py index aff221632f..8c0cd557c6 100644 --- a/src/bonsai/test/tool/test_collector.py +++ b/src/bonsai/test/tool/test_collector.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.aggregate import ifcopenshell.util.element import bonsai.core.tool import bonsai.tool as tool @@ -167,7 +168,8 @@ class TestAssign(NewIfc): tool.Ifc.link(building_element, building_obj) building_collection = bpy.data.collections.new("Foobar") bpy.context.scene.collection.children.link(building_collection) - building_obj.BIMObjectProperties.collection = building_collection + props = tool.Blender.get_object_bim_props(building_obj) + props.collection = building_collection building_collection.objects.link(building_obj) ifcopenshell.api.aggregate.assign_object( tool.Ifc.get(), diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index b004a92efb..2a2cb6b718 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -319,7 +319,8 @@ class TestGetDrawingCollection(NewFile): collection = bpy.data.collections.new("Collection") bpy.context.scene.collection.children.link(collection) collection.objects.link(obj) - obj.BIMObjectProperties.collection = collection + props = tool.Blender.get_object_bim_props(obj) + props.collection = collection collection.BIMCollectionProperties.obj = obj element = ifc.createIfcAnnotation() diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py index 63721bcdb7..c0d1819d9a 100644 --- a/src/bonsai/test/tool/test_geometry.py +++ b/src/bonsai/test/tool/test_geometry.py @@ -170,22 +170,25 @@ class TestGetTextLiteral(NewFile): class TestGetCartesianPointCoordinateOffset(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + oprops = tool.Blender.get_object_bim_props(obj) + oprops.blender_offset_type = "CARTESIAN_POINT" props = tool.Georeference.get_georeference_props() props.has_blender_offset = True - obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" + oprops.cartesian_point_offset = "1,2,3" assert np.allclose(subject.get_cartesian_point_offset(obj), np.array((1.0, 2.0, 3.0))) def test_get_null_if_not_a_cartesian_point_offset_type(self): obj = bpy.data.objects.new("Object", None) props = tool.Georeference.get_georeference_props() props.has_blender_offset = True - obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" + oprops = tool.Blender.get_object_bim_props(obj) + oprops.cartesian_point_offset = "1,2,3" assert subject.get_cartesian_point_offset(obj) is None def test_get_null_if_no_blender_offset(self): obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + oprops = tool.Blender.get_object_bim_props(obj) + oprops.blender_offset_type = "CARTESIAN_POINT" props = tool.Georeference.get_georeference_props() props.has_blender_offset = False assert subject.get_cartesian_point_offset(obj) is None @@ -307,15 +310,16 @@ class TestRecordObjectMaterials(NewFile): material.BIMStyleProperties.ifc_definition_id = style.id() obj.data.materials.append(material) subject.record_object_materials(obj) - assert tool.Geometry.get_mesh_props(obj).material_checksum == str([style.id()]) + assert tool.Geometry.get_mesh_props(obj.data).material_checksum == str([style.id()]) class TestRecordObjectPosition(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) + props = tool.Blender.get_object_bim_props(obj) subject.record_object_position(obj) - assert obj.BIMObjectProperties.location_checksum == repr(np.array(obj.matrix_world.translation).tobytes()) - assert obj.BIMObjectProperties.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3()).tobytes()) + assert props.location_checksum == repr(np.array(obj.matrix_world.translation).tobytes()) + assert props.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3()).tobytes()) class TestRemoveConnection(NewFile): diff --git a/src/bonsai/test/tool/test_ifc.py b/src/bonsai/test/tool/test_ifc.py index aedcea3465..c12af2a23f 100644 --- a/src/bonsai/test/tool/test_ifc.py +++ b/src/bonsai/test/tool/test_ifc.py @@ -129,14 +129,16 @@ class TestGetEntity(test.bim.bootstrap.NewFile): def test_attempting_without_a_file(self): obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.ifc_definition_id = 1 + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = 1 assert subject.get_entity(obj) is None def test_attempting_to_get_an_invalidly_linked_object(self): ifc = ifcopenshell.file() subject.set(ifc) obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.ifc_definition_id = 1 + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = 1 assert subject.get_entity(obj) is None diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index f01399e88c..c93532ab25 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -383,23 +383,26 @@ class TestUsingArrays(NewFile): bpy.ops.mesh.primitive_cube_add() obj = bpy.context.active_object + assert obj 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() bpy.ops.bim.enable_editing_array(item=0) - obj.BIMArrayProperties.count = 4 - obj.BIMArrayProperties.x = 4 - obj.BIMArrayProperties.sync_children = sync_children + props = tool.Model.get_array_props(obj) + props.count = 4 + props.x = 4 + props.sync_children = sync_children bpy.ops.bim.edit_array(item=0) if add_second_layer: bpy.ops.bim.add_array() bpy.ops.bim.enable_editing_array(item=1) - obj.BIMArrayProperties.count = 3 - obj.BIMArrayProperties.y = 4 - obj.BIMArrayProperties.sync_children = sync_children + props = tool.Model.get_array_props(obj) + props.count = 3 + props.y = 4 + props.sync_children = sync_children bpy.ops.bim.edit_array(item=1) def test_remove_array_last_to_first(self): diff --git a/src/bonsai/test/tool/test_surveyor.py b/src/bonsai/test/tool/test_surveyor.py index bfba0cb480..5d3684fdcf 100644 --- a/src/bonsai/test/tool/test_surveyor.py +++ b/src/bonsai/test/tool/test_surveyor.py @@ -54,6 +54,7 @@ class TestGetGlobalMatrix(test.bim.bootstrap.NewFile): props.blender_x_axis_abscissa = "0" props.blender_x_axis_ordinate = "1" obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + props = tool.Blender.get_object_bim_props(obj) + props.blender_offset_type = "OBJECT_PLACEMENT" matrix = ifcopenshell.util.geolocation.local2global(np.array(obj.matrix_world), 1.0, 2.0, 3.0, 0.0, 1.0) assert (subject.get_absolute_matrix(obj) == matrix).all() diff --git a/src/bonsai/test/tool/test_unit.py b/src/bonsai/test/tool/test_unit.py index a3bad12ab3..09db8b72bd 100644 --- a/src/bonsai/test/tool/test_unit.py +++ b/src/bonsai/test/tool/test_unit.py @@ -240,7 +240,7 @@ class TestImportUnitAttributes(NewFile): assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" assert props.unit_attributes["Prefix"].enum_value == "EXA" assert props.unit_attributes["Name"].enum_value == "AMPERE" - assert props.unit_attributes["Dimensions"] is None + assert "Dimensions" not in props.unit_attributes class TestImportUnits(NewFile): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py index fdc457367b..9de9dc5d55 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -442,7 +442,7 @@ class Usecase: panel_schema: list[list[int]] = self.settings["panel_schema"] panels: list[dict[str, Any]] = self.settings["panel_properties"] - accumulated_height = [0] * len(panel_schema[0]) + accumulated_height: list[float] = [0] * len(panel_schema[0]) built_panels: list[int] = [] window_items: list[ifcopenshell.entity_instance] = [] lining_items: list[ifcopenshell.entity_instance] = [] diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py index 6a52abd742..a673bf377c 100644 --- a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py +++ b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py @@ -96,7 +96,8 @@ class Patcher: angle_threshold = 0.3 for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id or not obj.data: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id or not obj.data: continue if not obj.data.polygons: continue