From 653b88ba18d63f1a7c2fc980635a3b847ef8a0c9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 7 May 2025 18:49:11 +0500 Subject: [PATCH] Bonsai - make undo errors visible to user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In theory there are possible undo errors, when some step wasn't registered in undo system and undo then won't be able to undo change from IFC. The problem is, those errors could be very dangerous but they occur in background, undetected by users, they're only visible as console traceback. After this commit we'll try to detect them and set `last_error`, which will show usual Bonsai error panel in Properties. @theoryshaw just in case, as you will probably find this kind of error 😁 Example traceback user might see in system console (or in their error report from "Copy Error Message"). ``` last_error: Undo transaction operations: - {'action': 'create', 'value': {'id': 1560, 'type': 'IfcGrid', 'GlobalId': '05Y8D_Ja12HfiEbmc$D3WN', 'OwnerHistory': None, 'Name': None, 'Description': None, 'ObjectType': None, 'ObjectPlacement': None, 'Representation': None, 'UAxes': None, 'VAxes': None, 'WAxes': None, 'PredefinedType': None}} - {'action': 'edit', 'id': 1560, 'index': 2, 'old': None, 'new': 'Grid'} - {'action': 'edit', 'id': 1221, 'index': 4, 'old': ({'id': 1297}, {'id': 1247}, {'id': 1220}, {'id': 1272}), 'new': ({'id': 1272}, {'id': 1247}, {'id': 1220}, {'id': 1560}, {'id': 1297})} - {'action': 'create', 'value': {'id': 1561, 'type': 'IfcCartesianPoint', 'Coordinates': (0.0, 0.0, 0.0)}} - {'action': 'create', 'value': {'id': 1562, 'type': 'IfcDirection', 'DirectionRatios': (0.0, 0.0, 1.0)}} - {'action': 'create', 'value': {'id': 1563, 'type': 'IfcDirection', 'DirectionRatios': (1.0, 0.0, 0.0)}} - {'action': 'create', 'value': {'id': 1564, 'type': 'IfcAxis2Placement3D', 'Location': {'id': 1561}, 'Axis': {'id': 1562}, 'RefDirection': {'id': 1563}}} - {'action': 'create', 'value': {'id': 1565, 'type': 'IfcLocalPlacement', 'PlacementRelTo': None, 'RelativePlacement': {'id': 1564}}} - {'action': 'edit', 'id': 1565, 'index': 0, 'old': None, 'new': {'id': 66}} - {'action': 'edit', 'id': 1560, 'index': 5, 'old': None, 'new': {'id': 1565}} - {'action': 'create', 'value': {'id': 1566, 'type': 'IfcGridAxis', 'AxisTag': 'A', 'AxisCurve': None, 'SameSense': True}} - {'action': 'edit', 'id': 1560, 'index': 7, 'old': None, 'new': ({'id': 1566},)} - {'action': 'create', 'value': {'id': 1567, 'type': 'IfcCartesianPoint', 'Coordinates': (-2.0, 0.0)}} - {'action': 'create', 'value': {'id': 1568, 'type': 'IfcCartesianPoint', 'Coordinates': (22.0, 0.0)}} - {'action': 'create', 'value': {'id': 1569, 'type': 'IfcPolyline', 'Points': ({'id': 1567}, {'id': 1568})}} - {'action': 'edit', 'id': 1566, 'index': 1, 'old': None, 'new': {'id': 1569}} - {'action': 'create', 'value': {'id': 1570, 'type': 'IfcGridAxis', 'AxisTag': 'B', 'AxisCurve': None, 'SameSense': True}} - {'action': 'edit', 'id': 1560, 'index': 7, 'old': ({'id': 1566},), 'new': ({'id': 1566}, {'id': 1570})} - {'action': 'create', 'value': {'id': 1571, 'type': 'IfcCartesianPoint', 'Coordinates': (-2.0, 10.0)}} - {'action': 'create', 'value': {'id': 1572, 'type': 'IfcCartesianPoint', 'Coordinates': (22.0, 10.0)}} - {'action': 'create', 'value': {'id': 1573, 'type': 'IfcPolyline', 'Points': ({'id': 1571}, {'id': 1572})}} - {'action': 'edit', 'id': 1570, 'index': 1, 'old': None, 'new': {'id': 1573}} - {'action': 'create', 'value': {'id': 1574, 'type': 'IfcGridAxis', 'AxisTag': 'C', 'AxisCurve': None, 'SameSense': True}} - {'action': 'edit', 'id': 1560, 'index': 7, 'old': ({'id': 1566}, {'id': 1570}), 'new': ({'id': 1566}, {'id': 1570}, {'id': 1574})} - {'action': 'create', 'value': {'id': 1575, 'type': 'IfcCartesianPoint', 'Coordinates': (-2.0, 20.0)}} - {'action': 'create', 'value': {'id': 1576, 'type': 'IfcCartesianPoint', 'Coordinates': (22.0, 20.0)}} - {'action': 'create', 'value': {'id': 1577, 'type': 'IfcPolyline', 'Points': ({'id': 1575}, {'id': 1576})}} - {'action': 'edit', 'id': 1574, 'index': 1, 'old': None, 'new': {'id': 1577}} - {'action': 'create', 'value': {'id': 1578, 'type': 'IfcGridAxis', 'AxisTag': '01', 'AxisCurve': None, 'SameSense': True}} - {'action': 'edit', 'id': 1560, 'index': 8, 'old': None, 'new': ({'id': 1578},)} - {'action': 'create', 'value': {'id': 1579, 'type': 'IfcCartesianPoint', 'Coordinates': (0.0, -2.0)}} - {'action': 'create', 'value': {'id': 1580, 'type': 'IfcCartesianPoint', 'Coordinates': (0.0, 22.0)}} - {'action': 'create', 'value': {'id': 1581, 'type': 'IfcPolyline', 'Points': ({'id': 1579}, {'id': 1580})}} - {'action': 'edit', 'id': 1578, 'index': 1, 'old': None, 'new': {'id': 1581}} - {'action': 'create', 'value': {'id': 1582, 'type': 'IfcGridAxis', 'AxisTag': '02', 'AxisCurve': None, 'SameSense': True}} - {'action': 'edit', 'id': 1560, 'index': 8, 'old': ({'id': 1578},), 'new': ({'id': 1578}, {'id': 1582})} - {'action': 'create', 'value': {'id': 1583, 'type': 'IfcCartesianPoint', 'Coordinates': (10.0, -2.0)}} - {'action': 'create', 'value': {'id': 1584, 'type': 'IfcCartesianPoint', 'Coordinates': (10.0, 22.0)}} - {'action': 'create', 'value': {'id': 1585, 'type': 'IfcPolyline', 'Points': ({'id': 1583}, {'id': 1584})}} - {'action': 'edit', 'id': 1582, 'index': 1, 'old': None, 'new': {'id': 1585}} - {'action': 'create', 'value': {'id': 1586, 'type': 'IfcGridAxis', 'AxisTag': '03', 'AxisCurve': None, 'SameSense': True}} - {'action': 'edit', 'id': 1560, 'index': 8, 'old': ({'id': 1578}, {'id': 1582}), 'new': ({'id': 1578}, {'id': 1582}, {'id': 1586})} - {'action': 'create', 'value': {'id': 1587, 'type': 'IfcCartesianPoint', 'Coordinates': (20.0, -2.0)}} - {'action': 'create', 'value': {'id': 1588, 'type': 'IfcCartesianPoint', 'Coordinates': (20.0, 22.0)}} - {'action': 'create', 'value': {'id': 1589, 'type': 'IfcPolyline', 'Points': ({'id': 1587}, {'id': 1588})}} - {'action': 'edit', 'id': 1586, 'index': 1, 'old': None, 'new': {'id': 1589}} Traceback (most recent call last): File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 352, in undo transaction.rollback() File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 167, in rollback element = self.file.by_id(operation["id"]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 510, in by_id return self[id] ~~~~^^^^ File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 497, in __getitem__ return entity_instance(self.wrapped_data.by_id(key), self) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\ifcopenshell_wrapper.py", line 8884, in by_id return _ifcopenshell_wrapper.file_by_id(self, id) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RuntimeError: Instance #1565 not found The above exception was the direct cause of the following exception: Traceback (most recent call last): File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\bim\ifc.py", line 426, in callback tool.Ifc.get().undo() File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 354, in undo raise UndoSystemError("Error during transaction undo.", transaction) from e ifcopenshell.file.UndoSystemError: Error during transaction undo. ``` --- src/bonsai/bonsai/bim/ifc.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 1cac1d5cb1..d88816ef50 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -31,9 +31,10 @@ import ifcopenshell.ifcopenshell_wrapper import bonsai import bonsai.bim.handler import bonsai.tool as tool +from ifcopenshell.file import UndoSystemError from pathlib import Path from bonsai.tool.brick import BrickStore -from typing import Set, Union, Optional, TypedDict, Callable, NotRequired +from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, Literal IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object] @@ -417,6 +418,32 @@ class IfcStore: props = tool.Blender.get_object_bim_props(obj) props.ifc_definition_id = 0 + @staticmethod + def get_ifc_file_undo_callback(callback_type: Literal["UNDO", "REDO"]): + def callback(_) -> None: + try: + if callback_type == "UNDO": + tool.Ifc.get().undo() + else: + tool.Ifc.get().redo() + except Exception as e: + # Persistent callbacks errors are not visible from UI and we set `last_error` + # to make it visible. + error_msg = "" + # In theory it should always be UndoSystemError, but just to be safe. + if isinstance(e, UndoSystemError): + error_msg += "Undo transaction operations:\n" + transaction = e.transaction + for operation in transaction.operations: + error_msg += f"- {str(operation)}\n" + # Show it in system console too, not just in last message. + print(error_msg) + error_msg += traceback.format_exc() + bonsai.last_error = error_msg + raise + + return callback + @staticmethod def execute_ifc_operator( operator: tool.Ifc.Operator, @@ -446,7 +473,9 @@ class IfcStore: if tool.Ifc.get(): tool.Ifc.get().end_transaction() IfcStore.add_transaction_operation( - operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo() + operator, + rollback=IfcStore.get_ifc_file_undo_callback("UNDO"), + commit=IfcStore.get_ifc_file_undo_callback("REDO"), ) if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages. BrickStore.end_transaction()