From 08dfcea47cf02c8fa59e852442fd8216833bc8d3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 15 Mar 2026 21:38:20 +1100 Subject: [PATCH 1/7] Fix Python signatures in operator descriptions. Closes #7797. Closes #7230. Co-Authored-By: Claude Opus 4.6 --- src/bonsai/bonsai/bim/module/cad/operator.py | 2 ++ src/bonsai/bonsai/bim/module/cad/workspace.py | 36 +++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cad/operator.py b/src/bonsai/bonsai/bim/module/cad/operator.py index 3b6ef8fc69..bef1d851af 100644 --- a/src/bonsai/bonsai/bim/module/cad/operator.py +++ b/src/bonsai/bonsai/bim/module/cad/operator.py @@ -37,6 +37,7 @@ messages = { class CadTrimExtend(bpy.types.Operator): bl_idname = "bim.cad_trim_extend" bl_label = "CAD Trim / Extend" + bl_description = "Extends/reduces element to 3D cursor" @classmethod def poll(cls, context): @@ -82,6 +83,7 @@ class CadTrimExtend(bpy.types.Operator): class CadMitre(bpy.types.Operator): bl_idname = "bim.cad_mitre" bl_label = "CAD Mitre" + bl_description = "Joins two non-parallel paths at their intersection" @classmethod def poll(cls, context): diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 20faa4dcc7..61a013fcdb 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -106,23 +106,23 @@ class CadTool(WorkSpaceTool): ) row = layout.row(align=True) - add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context) + add_layout_hotkey_operator(row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator( - row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context + row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) + add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) + add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context) + add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context) + add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context) + add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context) + add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__.split("\n", 1)[1].strip(), ui_context) elif ( isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES) @@ -132,15 +132,15 @@ class CadTool(WorkSpaceTool): layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context ) row = layout.row(align=True) - add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context) + add_layout_hotkey_operator(row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator( - row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context + row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context) + add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) + add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context) else: if ( @@ -168,19 +168,19 @@ class CadTool(WorkSpaceTool): add_layout_hotkey_operator(row, "Set Gable Roof Angle", "S_R", "Set Gable Roof Angle", ui_context) row = layout.row(align=True) - add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context) + add_layout_hotkey_operator(row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator( - row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context + row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) + add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) + add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context) + add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__.split("\n", 1)[1].strip(), ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context) + add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__.split("\n", 1)[1].strip(), ui_context) class CadHotkey(bpy.types.Operator): From 2edd1a5044ba12f5543a04bdb37ef27c405441ba Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 15 Mar 2026 21:39:57 +1100 Subject: [PATCH 2/7] Black --- src/bonsai/bonsai/bim/module/bsdd/ui.py | 1 + src/bonsai/bonsai/bim/module/cad/workspace.py | 68 +++++++++++++++---- src/bonsai/bonsai/core/material.py | 4 +- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index f49f48abb9..c6966c01d8 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -83,6 +83,7 @@ class BIM_PT_bsdd(Panel): row = self.layout.row() row.operator("bim.load_bsdd_dictionaries") + class BIM_UL_bsdd_dictionaries(UIList): def draw_item( self, diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 61a013fcdb..24fb98ba3f 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -106,23 +106,37 @@ class CadTool(WorkSpaceTool): ) row = layout.row(align=True) - add_layout_hotkey_operator(row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator( row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__.split("\n", 1)[1].strip(), ui_context + ) elif ( isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES) @@ -132,15 +146,21 @@ class CadTool(WorkSpaceTool): layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context ) row = layout.row(align=True) - add_layout_hotkey_operator(row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator( row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context + ) else: if ( @@ -168,19 +188,37 @@ class CadTool(WorkSpaceTool): add_layout_hotkey_operator(row, "Set Gable Roof Angle", "S_R", "Set Gable Roof Angle", ui_context) row = layout.row(align=True) - add_layout_hotkey_operator(row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Extend", "S_E", bpy.ops.bim.cad_trim_extend.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator( row, "Join", "S_T", bpy.ops.bim.cad_mitre.__doc__.split("\n", 1)[1].strip(), ui_context ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__.split("\n", 1)[1].strip(), ui_context + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, + "2-Point Arc", + "S_C", + bpy.ops.bim.cad_arc_from_2_points.__doc__.split("\n", 1)[1].strip(), + ui_context, + ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__.split("\n", 1)[1].strip(), ui_context) + add_layout_hotkey_operator( + row, + "3-Point Arc", + "S_V", + bpy.ops.bim.cad_arc_from_3_points.__doc__.split("\n", 1)[1].strip(), + ui_context, + ) class CadHotkey(bpy.types.Operator): diff --git a/src/bonsai/bonsai/core/material.py b/src/bonsai/bonsai/core/material.py index 4fd254b6f2..0a08f5e0bf 100644 --- a/src/bonsai/bonsai/core/material.py +++ b/src/bonsai/bonsai/core/material.py @@ -143,7 +143,9 @@ def assign_material( material_tool.add_material_to_set(material_set=material, material=default_material) elif material_tool.is_a_material_set(assigned_material): material_tool.add_material_to_set(material_set=assigned_material, material=material) - material_tool.ensure_material_assigned(elements=[element], material_type=element_material_type, material=material) + material_tool.ensure_material_assigned( + elements=[element], material_type=element_material_type, material=material + ) def unassign_material(ifc: type[tool.Ifc], material_tool: type[tool.Material], objects: list[bpy.types.Object]) -> None: From c30d24c4d994e53d665ae50ce7510f286554e8d5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 15 Mar 2026 23:29:57 +1100 Subject: [PATCH 3/7] Fix regression where changing logic to occur in filesystem selector caused headless test to fail. See d4388ec76 --- src/bonsai/bonsai/bim/module/drawing/operator.py | 3 +-- src/bonsai/test/bim/feature/drawing.feature | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index fa17be9fb5..34407adc9d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3886,8 +3886,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)) ifc_file = tool.Ifc.get() - params = {"check_existing": False} - image = load_image(abs_path.name, str(abs_path.parent), **params) + image = load_image(abs_path.name, str(abs_path.parent), check_existing=False) mesh = bpy.data.meshes.new(image_filepath.stem) obj = bpy.data.objects.new(image_filepath.stem, mesh) diff --git a/src/bonsai/test/bim/feature/drawing.feature b/src/bonsai/test/bim/feature/drawing.feature index b7b78e748d..8038b68a5d 100644 --- a/src/bonsai/test/bim/feature/drawing.feature +++ b/src/bonsai/test/bim/feature/drawing.feature @@ -313,6 +313,6 @@ Scenario: Create sheet - with a drawing added to it Scenario: Add reference image Given an empty IFC project And I save IFC project - When I press "bim.add_reference_image(filepath='{cwd}/test/files/image.jpg')" + When I press "bim.add_reference_image(filepath='{cwd}/test/files/image.jpg', x_length=1, y_length=0.565)" Then the object "IfcAnnotation/image" exists And the object "IfcAnnotation/image" dimensions are "1.0,0.565,0." From c3a87c8f9c3e7ecc83c26ab6de090031c7fc936b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 15 Mar 2026 23:30:10 +1100 Subject: [PATCH 4/7] Add basic text editing feature tests --- src/bonsai/test/bim/feature/drawing.feature | 75 +++++++++++++++++++++ src/bonsai/test/bim/test_feature.py | 2 +- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/bonsai/test/bim/feature/drawing.feature b/src/bonsai/test/bim/feature/drawing.feature index 8038b68a5d..cff150b3b4 100644 --- a/src/bonsai/test/bim/feature/drawing.feature +++ b/src/bonsai/test/bim/feature/drawing.feature @@ -310,6 +310,81 @@ Scenario: Create sheet - with a drawing added to it When I click "OUTPUT" Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall" +Scenario: Enable editing text + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I save IFC project + And I look at the "Drawings" panel + And I click "IMPORT" + And I click "ADD" + And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')" + And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list + And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list + And I press "bim.add_annotation" + And the object "IfcAnnotation/TEXT" is selected + And I look at the "BIM_PT_text" panel + When I click "Enable Editing Text" + Then I see "Literals:" + And I don't see "FontSize" + +Scenario: Disable editing text + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I save IFC project + And I look at the "Drawings" panel + And I click "IMPORT" + And I click "ADD" + And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')" + And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list + And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list + And I press "bim.add_annotation" + And the object "IfcAnnotation/TEXT" is selected + And I look at the "BIM_PT_text" panel + And I click "Enable Editing Text" + When I click "CANCEL" + Then I see "FontSize" + And I don't see "Literals:" + +Scenario: Edit text - no changes + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I save IFC project + And I look at the "Drawings" panel + And I click "IMPORT" + And I click "ADD" + And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')" + And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list + And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list + And I press "bim.add_annotation" + And the object "IfcAnnotation/TEXT" is selected + And I look at the "BIM_PT_text" panel + And I click "Enable Editing Text" + When I click "Edit Text" + Then I see "FontSize" + And I don't see "Literals:" + +Scenario: Edit text - change literal + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I save IFC project + And I look at the "Drawings" panel + And I click "IMPORT" + And I click "ADD" + And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')" + And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list + And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list + And I press "bim.add_annotation" + And the object "IfcAnnotation/TEXT" is selected + And I look at the "BIM_PT_text" panel + And I click "Enable Editing Text" + And I set the "Literal" property to "Hello World" + When I click "Edit Text" + Then I see "Hello World" + Scenario: Add reference image Given an empty IFC project And I save IFC project diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 262dce65ed..fe6fbcba15 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -378,7 +378,7 @@ def i_look_at_the_panel_panel(panel: str) -> None: # Option to provide explicit panel name if panel names overlap. panel_class = getattr(bpy.types, panel, None) - if panel_class is None: + if panel_class is None or panel_class.bl_rna.base.name not in ("Panel", "Operator", "Menu", "UIList"): global ui_name_cache create_ui_name_cache() if panel not in ui_name_cache: From 868bb5c39e67799ea9804d177986036d39425cdd Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 14 Mar 2026 10:09:53 -0500 Subject: [PATCH 5/7] Allow bulk annotation product assignment Closes #7787: Previously bim.assign_selected_as_product required exactly 2 objects. With multiple annotations referencing the same product, users had to repeat the operation once per annotation. Now any number of IfcAnnotations can be selected alongside a single product object and all are assigned in one operation and one undo step. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/drawing/operator.py | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 34407adc9d..58474d8ce5 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3364,57 +3364,55 @@ class OrderTextLiteralDown(bpy.types.Operator): return {"FINISHED"} -# Ifc Operator is unnecessary, because suboperator is handling IFC changes. -class AssignSelectedObjectAsProduct(bpy.types.Operator): +class AssignSelectedObjectAsProduct(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_selected_as_product" bl_label = "Assign Selected Object As Product" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): - if len(context.selected_objects) != 2: - cls.poll_message_set("2 objects need to be selected") + if len(context.selected_objects) < 2: + cls.poll_message_set("At least 2 objects need to be selected") return False return True - def execute(self, context): - assert bpy.context.view_layer + def _execute(self, context): objs = context.selected_objects[:] - obj1, obj2 = objs - element1 = tool.Ifc.get_entity(obj1) - element2 = tool.Ifc.get_entity(obj2) - assert element1 and element2 + ifc_objs = [(o, tool.Ifc.get_entity(o)) for o in objs if tool.Ifc.get_entity(o)] - # Check if at least one object is an IfcAnnotation - is_annotation1 = element1.is_a("IfcAnnotation") - is_annotation2 = element2.is_a("IfcAnnotation") + annotations = [(o, e) for o, e in ifc_objs if e.is_a("IfcAnnotation")] + non_annotations = [(o, e) for o, e in ifc_objs if not e.is_a("IfcAnnotation")] - if not (is_annotation1 or is_annotation2): - self.report({"ERROR"}, "At least one of the selected objects must be IfcAnnotation.") + if not annotations: + self.report({"ERROR"}, "At least one selected object must be an IfcAnnotation.") return {"CANCELLED"} - # If both are annotations, use the currently active object as relating product - if is_annotation1 and is_annotation2: + if len(non_annotations) == 1: + # One product, one or more annotations — assign all annotations to the product. + product = non_annotations[0][1] + elif len(non_annotations) == 0 and len(annotations) == 2: + # Both objects are annotations — use the non-active one as the relating product. active_obj = context.active_object - if active_obj == obj1: - other_selected_object = obj1 - bpy.context.view_layer.objects.active = obj2 + if annotations[0][0] == active_obj: + annotation_obj, annotation = annotations[0] + product = annotations[1][1] else: - other_selected_object = obj2 - bpy.context.view_layer.objects.active = obj1 - # If only one is an annotation, make it the active object - elif is_annotation1: - other_selected_object = obj2 - bpy.context.view_layer.objects.active = obj1 + annotation_obj, annotation = annotations[1] + product = annotations[0][1] + core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product) + tool.Blender.update_viewport() + return else: - other_selected_object = obj1 - bpy.context.view_layer.objects.active = obj2 + self.report( + {"ERROR"}, + "Select exactly one product object and one or more IfcAnnotation objects.", + ) + return {"CANCELLED"} - assert (active_obj := context.active_object) - props = tool.Drawing.get_object_assigned_product_props(active_obj) - props.relating_product = other_selected_object - bpy.ops.bim.edit_assigned_product() - return {"FINISHED"} + for annotation_obj, _ in annotations: + core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation_obj, product=product) + + tool.Blender.update_viewport() class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator): From 8c9e89ace88699e52c7e1f1b3bca5ef6cabdba27 Mon Sep 17 00:00:00 2001 From: Dirk Olbrich Date: Sun, 22 Feb 2026 15:57:09 +0100 Subject: [PATCH 6/7] Bonsai - change add_grid operator namespace to bim --- src/bonsai/bonsai/bim/module/model/grid.py | 2 +- src/bonsai/bonsai/bim/module/spatial/ui.py | 2 +- src/bonsai/test/bim/feature/model.feature | 2 +- src/bonsai/test/bim/feature/project.feature | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/grid.py b/src/bonsai/bonsai/bim/module/model/grid.py index 8ceaa605dc..dd4e919e30 100644 --- a/src/bonsai/bonsai/bim/module/model/grid.py +++ b/src/bonsai/bonsai/bim/module/model/grid.py @@ -82,7 +82,7 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None: class BIM_OT_add_object(Operator, tool.Ifc.Operator): - bl_idname = "mesh.add_grid" + bl_idname = "bim.add_grid" bl_label = "Grid" bl_description = "Add IfcGrid." bl_options = {"REGISTER", "UNDO"} diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index dd0510eede..3a4a771e01 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -251,7 +251,7 @@ class BIM_PT_grids(Panel): bl_options = {"HEADER_LAYOUT_EXPAND"} def draw(self, context): - self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids") + self.layout.row().operator("bim.add_grid", icon="ADD", text="Add Grids") def draw_header(self, context): props = tool.Spatial.get_grid_props() diff --git a/src/bonsai/test/bim/feature/model.feature b/src/bonsai/test/bim/feature/model.feature index 9ca843c1c5..ad5b25bd07 100644 --- a/src/bonsai/test/bim/feature/model.feature +++ b/src/bonsai/test/bim/feature/model.feature @@ -57,7 +57,7 @@ Scenario: Add one type from the Construction Type Browser Scenario: Add grid Given an empty IFC project - When I press "mesh.add_grid" + When I press "bim.add_grid" Then the object "IfcGrid/Grid" is an "IfcGrid" And the object "IfcGridAxis/A" is an "IfcGridAxis" And the object "IfcGridAxis/B" is an "IfcGridAxis" diff --git a/src/bonsai/test/bim/feature/project.feature b/src/bonsai/test/bim/feature/project.feature index f536a17220..0db6041b56 100644 --- a/src/bonsai/test/bim/feature/project.feature +++ b/src/bonsai/test/bim/feature/project.feature @@ -914,7 +914,7 @@ Scenario: Export IFC - with moved object location synchronised Scenario: Export IFC - with moved grid axis location synchronised Given an empty IFC project - And I press "mesh.add_grid" + And I press "bim.add_grid" When the object "IfcGridAxis/01" is moved to "1,0,0" And I save IFC project And I load previously saved IFC project From 1999d93f9a5572b19f8aaa4c96a294588c5a66f9 Mon Sep 17 00:00:00 2001 From: falken10vdl <33285113+falken10vdl@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:30:49 +0100 Subject: [PATCH 7/7] Add GenerateUVMap operator and integrate into ExploreTool (#7695) Co-authored-by: Dion Moult --- .../bonsai/bim/module/project/__init__.py | 1 + .../bonsai/bim/module/project/workspace.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index db642c18c2..67710db8ba 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -76,6 +76,7 @@ classes = ( operator.UnlinkIfc, operator.UnloadLink, workspace.ExploreHotkey, + workspace.GenerateUVMap, prop.LibraryBreadcrumb, prop.LibraryElement, prop.FilterCategory, diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index a01a144b31..89467cbbee 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -87,6 +87,9 @@ class ExploreTool(bpy.types.WorkSpaceTool): op.hotkey = "S_S" op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them" + op = row.operator("bim.generate_uv_map", text="Generate UV Map", icon="UV") + op.description = "Generate UV map for selected mesh." + class ExploreHotkey(bpy.types.Operator): bl_idname = "bim.explore_hotkey" @@ -153,3 +156,20 @@ class ExploreHotkey(bpy.types.Operator): def hotkey_A_H(self) -> None: bpy.ops.bim.hide_queried_linked_element(unhide_all=True) + + +class GenerateUVMap(bpy.types.Operator): + bl_idname = "bim.generate_uv_map" + bl_label = "Generate UV Map" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + description: bpy.props.StringProperty() + + def execute(self, context): + obj = context.active_object + if not obj or not hasattr(obj, "data") or not hasattr(obj.data, "polygons"): + self.report({"ERROR"}, "No valid mesh selected.") + return {"CANCELLED"} + tool.Loader.load_generated_uv_map(obj.data) + self.report({"INFO"}, "Generated UV map for selected mesh.") + return {"FINISHED"} \ No newline at end of file