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.
This commit is contained in:
Dion Moult
2026-02-03 18:09:15 +11:00
parent fe395f9024
commit 1445f89f19
4 changed files with 19 additions and 199 deletions
@@ -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"
+8 -5
View File
@@ -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:
-1
View File
@@ -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
+4 -35
View File
@@ -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: