From 1445f89f19e7ca41eab1408fd349d919519b0378 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 3 Feb 2026 18:09:15 +1100 Subject: [PATCH] See #7629. Partially reimplement bulk text editing. This isn't complete yet, but it hopefully demonstrates a preferred implementation: * Logic in core, not operator * Loop done in core, without needing to call other core functions, so the overhead of enabling and disabling editing per object is removed. No more Blender logic, just straight editing in IFC. * Reuse existing function to grab text attributes instead of reimplementing it twice. * Remove dead code, there seems to be a function apply_to_selected_objects which was completely unused and duplicated code twice. --- .../bonsai/bim/module/drawing/operator.py | 165 +----------------- src/bonsai/bonsai/core/drawing.py | 13 +- src/bonsai/bonsai/core/tool.py | 1 - src/bonsai/bonsai/tool/drawing.py | 39 +---- 4 files changed, 19 insertions(+), 199 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 8c29bfbf41..4c52993aa0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3185,166 +3185,15 @@ class EditText(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - obj = context.active_object - props = tool.Drawing.get_text_props(obj) - - captured_apply_settings = { - "apply_font_size_to_all": props.apply_font_size_to_all, - "apply_newline_to_all": props.apply_newline_to_all, - "font_size": props.font_size, - "newline_at": props.newline_at, - "literals": [], - } - - for i, literal in enumerate(props.literals): - literal_data = { - "attributes": [ - (attr.string_value, attr.enum_value if attr.data_type == "enum" else attr.string_value) - for attr in literal.attributes - ], - "box_alignment": literal.box_alignment[:] if hasattr(literal, "box_alignment") else None, - "element_value_rows": [ - { - "category": row.category, - "element_key": row.element_key, - "formatted_value": row.formatted_value, - "separator": row.separator, - } - for row in literal.element_value_rows - ], - "product_used": literal.product_used.name if literal.product_used else None, - } - - if i < len(props.literal_apply_settings): - apply_settings = props.literal_apply_settings[i] - literal_data["apply_text_to_all"] = apply_settings.apply_text_to_all - literal_data["apply_path_to_all"] = apply_settings.apply_path_to_all - literal_data["apply_box_alignment_to_all"] = apply_settings.apply_box_alignment_to_all - else: - literal_data["apply_text_to_all"] = False - literal_data["apply_path_to_all"] = False - literal_data["apply_box_alignment_to_all"] = False - - captured_apply_settings["literals"].append(literal_data) - - obj["_bonsai_element_value_rows_backup"] = json.dumps(captured_apply_settings["literals"]) - - core.edit_text(tool.Drawing, obj=obj) - - self.apply_to_selected_objects_with_captured_data(context, obj, captured_apply_settings) - + apply_objs = [ + obj + for obj in tool.Blender.get_selected_objects() + if (element := tool.Ifc.get_entity(obj)) + and tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]) + ] + core.edit_text(tool.Drawing, attribute_obj=tool.Blender.get_active_object(), apply_objs=apply_objs) tool.Blender.update_viewport() - return {"FINISHED"} - - def apply_to_selected_objects(self, context, active_obj, active_props): - """Apply changes to other selected text objects based on toggle settings""" - selected_objects = [obj for obj in context.selected_objects if obj != active_obj] - - for obj in selected_objects: - element = tool.Ifc.get_entity(obj) - if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]): - continue - - obj_props = tool.Drawing.get_text_props(obj) - needs_update = False - - if active_props.apply_font_size_to_all: - obj_props.font_size = active_props.font_size - needs_update = True - - if active_props.apply_newline_to_all: - obj_props.newline_at = active_props.newline_at - needs_update = True - - for i, active_literal in enumerate(active_props.literals): - if i >= len(obj_props.literals): - continue - - obj_props.ensure_literal_apply_settings(len(obj_props.literals)) - obj_literal = obj_props.literals[i] - - if i < len(active_props.literal_apply_settings): - active_settings = active_props.literal_apply_settings[i] - - if active_settings.apply_text_to_all: - if len(active_literal.attributes) > 0 and len(obj_literal.attributes) > 0: - obj_literal.attributes[0].string_value = active_literal.attributes[0].string_value - needs_update = True - - if active_settings.apply_path_to_all: - if len(active_literal.attributes) > 1 and len(obj_literal.attributes) > 1: - if ( - active_literal.attributes[1].data_type == "enum" - and obj_literal.attributes[1].data_type == "enum" - ): - obj_literal.attributes[1].enum_value = active_literal.attributes[1].enum_value - else: - obj_literal.attributes[1].string_value = active_literal.attributes[1].string_value - needs_update = True - - if active_settings.apply_box_alignment_to_all: - obj_literal.box_alignment = active_literal.box_alignment[:] - needs_update = True - - if needs_update: - core.edit_text(tool.Drawing, obj=obj) - - def apply_to_selected_objects_with_captured_data(self, context, active_obj, captured_data): - """Apply changes to other selected text objects using captured apply settings""" - selected_objects = [obj for obj in context.selected_objects if obj != active_obj] - - for obj in selected_objects: - element = tool.Ifc.get_entity(obj) - if not element: - continue - if not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]): - continue - - obj_props = tool.Drawing.get_text_props(obj) - - if len(obj_props.literals) == 0: - core.enable_editing_text(tool.Drawing, obj=obj) - obj_props.ensure_literal_apply_settings(len(obj_props.literals)) - - needs_update = False - - if captured_data["apply_font_size_to_all"]: - obj_props.font_size = captured_data["font_size"] - needs_update = True - - if captured_data["apply_newline_to_all"]: - obj_props.newline_at = captured_data["newline_at"] - needs_update = True - - for i, captured_literal in enumerate(captured_data["literals"]): - if i >= len(obj_props.literals): - continue - - obj_literal = obj_props.literals[i] - - if captured_literal["apply_text_to_all"]: - if len(captured_literal["attributes"]) > 0 and len(obj_literal.attributes) > 0: - new_value = captured_literal["attributes"][0][0] # [0] = string_value - obj_literal.attributes[0].string_value = new_value - needs_update = True - - if captured_literal["apply_path_to_all"]: - if len(captured_literal["attributes"]) > 1 and len(obj_literal.attributes) > 1: - new_value = captured_literal["attributes"][1][1] # [1] = enum_value or string_value - if obj_literal.attributes[1].data_type == "enum": - obj_literal.attributes[1].enum_value = new_value - else: - obj_literal.attributes[1].string_value = new_value - needs_update = True - - if captured_literal["apply_box_alignment_to_all"] and captured_literal["box_alignment"]: - obj_literal.box_alignment = captured_literal["box_alignment"] - needs_update = True - - if needs_update: - core.edit_text(tool.Drawing, obj=obj) - class EnableEditingText(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.enable_editing_text" diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index b56b51751f..eb2c47f1dc 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -38,11 +38,14 @@ def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> drawing.disable_editing_text(obj) -def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None: - drawing.synchronise_ifc_and_text_attributes(obj) - drawing.update_text_size_pset(obj) - drawing.update_text_annotation_properties(obj) - drawing.disable_editing_text(obj) +def edit_text(drawing: type[tool.Drawing], attribute_obj: bpy.types.Object, apply_objs: list[bpy.types.Object]) -> None: + literal_attributes = drawing.export_text_literal_attributes(attribute_obj) + for obj in apply_objs: + drawing.edit_text_literals(obj, literal_attributes) + # TODO: font size should be part of a separate set of formatting controls, not part of text editing + drawing.update_text_size_pset(obj) + drawing.update_text_annotation_properties(obj) + drawing.disable_editing_text(obj) def enable_editing_assigned_product(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None: diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 19395fa449..a34de4d4fe 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -402,7 +402,6 @@ class Drawing: def setup_shading_styles_path(cls, resource_path): pass def show_decorations(cls): pass def sync_object_placement(cls, obj): pass - def synchronise_ifc_and_text_attributes(cls, obj): pass def update_embedded_svg_location(cls, uri, old_location, new_location): pass def update_text_annotation_properties(cls, obj): pass def update_text_size_pset(cls, obj): pass diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 51d06c1c18..a0a45b403a 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -835,43 +835,12 @@ class Drawing(bonsai.core.tool.Drawing): return props.is_editing_sheets @classmethod - def synchronise_ifc_and_text_attributes(cls, obj: bpy.types.Object) -> None: + def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None: assert (element := tool.Ifc.get_entity(obj)) assert (rep := cls.get_annotation_representation(element)) - - old_literals = cls.get_text_literal(obj, return_list=True) - assert isinstance(old_literals, list) - literals_attributes = cls.export_text_literal_attributes(obj) - props = cls.get_text_props(obj) - defined_ifc_ids = [l.ifc_definition_id for l in props.literals] - ifc_file = tool.Ifc.get() - - added_literals: list[ifcopenshell.entity_instance] = [] - new_literals: list[ifcopenshell.entity_instance] = [] - for ifc_definition_id, attributes in zip(defined_ifc_ids, literals_attributes): - # making sure all literals from text edit exist in ifc - if ifc_definition_id == 0: - literal = cls.add_literal(**attributes) - added_literals.append(literal) - else: - literal = ifc_file.by_id(ifc_definition_id) - ifcopenshell.api.drawing.edit_text_literal( - ifc_file, - text_literal=literal, - attributes=attributes, - ) - new_literals.append(literal) - - removed_literals = set(old_literals) - set(new_literals) - - # Add new literals and keep the order as defined in text props. - items = [i for i in rep.Items if i not in removed_literals] + added_literals - items.sort(key=lambda x: new_literals.index(x) if x in new_literals else -1) - rep.Items = items - - # Remove from ifc the literals that were removed during the edit. - for literal in removed_literals: - ifcopenshell.util.element.remove_deep2(ifc_file, literal) + for literal in cls.get_text_literal(obj, return_list=True): + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), literal) + rep.Items = [cls.add_literal(**a) for a in literal_attributes] @classmethod def add_literal(cls, **attributes: str) -> ifcopenshell.entity_instance: