From 37c05f763e68fafc005fbb44a71046e012cfadea Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 4 Aug 2025 13:43:16 +0500 Subject: [PATCH] Fix IFC undo data loss in 645298e #6973 There was a contradiction - modal operators were treated as special and were always creating new transactions (`is_top_level_operator = method == "MODAL"`), but those transactions were only closed if modal returned `FINISHED` or `CANCELLED`, so in most case previous transactions were just overridden, so IFC undo data was lost. Ping @Moult just in case --- src/bonsai/bonsai/bim/ifc.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index fa988f9de0..ae680ea15a 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -445,8 +445,11 @@ class IfcStore: return callback - @staticmethod + modal_in_progress = False + + @classmethod def execute_ifc_operator( + cls, operator: tool.Ifc.Operator, context: bpy.types.Context, event=None, @@ -456,16 +459,33 @@ class IfcStore: props = tool.Blender.get_bim_props() props.is_dirty = True # Modals don't nest, and Blender handles the loop that continuously calls modal() - is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL") + # So for modal operators we emulate nesting - first modal call is starting transaction + # and modal call with FINISHED/CANCELLED result finishes it. + + # Example call chain for modal operators: + # Operator1.execute called + # -> Operator.execute called nested Operator2 + # -> Operator2.execute returned RUNNING_MODAL + # -> Operator2.execute is finished + # -> Operator1.execute is finished + # -> Operator2.modal is called and returned PASS_THROUGH (going to another modal loop) + # -> Operator2.modal is finished + # -> Operator2.modal called again and last two steps repeat until FINISHED or CANCELLED + is_top_level_operator = not bool(IfcStore.current_transaction) and not cls.modal_in_progress if is_top_level_operator: IfcStore.begin_transaction(operator) - if tool.Ifc.get(): - tool.Ifc.get().begin_transaction() + if ifc_file := tool.Ifc.get(): + assert ( + ifc_file.transaction is None + ), "Trying to override existing transaction, possible IFC undo data loss." + ifc_file.begin_transaction() if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages. BrickStore.begin_transaction() # This empty transaction ensures that each operator has at least one transaction IfcStore.add_transaction_operation(operator, rollback=lambda data: True, commit=lambda data: True) + if method == "MODAL": + cls.modal_in_progress = True else: operator.transaction_key = IfcStore.current_transaction @@ -483,6 +503,9 @@ class IfcStore: IfcStore.end_transaction(operator) bonsai.bim.handler.refresh_ui_data() + if method == "MODAL": + cls.modal_in_progress = False + try: if method == "EXECUTE": result = getattr(operator, "_execute")(context) @@ -503,8 +526,10 @@ class IfcStore: if method == "MODAL": if result == {"FINISHED"}: + is_top_level_operator = True end_top_level_operator() elif result == {"CANCELLED"}: + is_top_level_operator = True # Please read the docs: https://docs.blender.org/api/current/bpy.types.Operator.html # > "when an operator returns {'CANCELLED'}, no undo step will be created". # This means that if your modal edits IFC data, then the user