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/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..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", "Extends/reduces element to 3D cursor", 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, "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, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) + 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, "Offset", "S_O", bpy.ops.bim.cad_offset.__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, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__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, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__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, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__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, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__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__.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", "Extends/reduces element to 3D cursor", 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, "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, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context) + 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, "Offset", "S_O", bpy.ops.bim.cad_offset.__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__.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", "Extends/reduces element to 3D cursor", 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, "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, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) + 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, "Offset", "S_O", bpy.ops.bim.cad_offset.__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, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__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, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_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__.split("\n", 1)[1].strip(), + ui_context, + ) class CadHotkey(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index fa17be9fb5..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): @@ -3886,8 +3884,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/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/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 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/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: diff --git a/src/bonsai/test/bim/feature/drawing.feature b/src/bonsai/test/bim/feature/drawing.feature index b7b78e748d..cff150b3b4 100644 --- a/src/bonsai/test/bim/feature/drawing.feature +++ b/src/bonsai/test/bim/feature/drawing.feature @@ -310,9 +310,84 @@ 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 - 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." 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 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: