From bf541328051d5135dd37a559d3a0b9851c426efc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 18:37:53 +1100 Subject: [PATCH] Fix #6307. Warning: critical bug where saving a file broke undo state. Previously, changing props.ifc_file had an update hook to reload information about the IFC model. But this isn't always correct because there are two situations: 1. The ifc_file path changed because you saved the file for the first time or saved as, and this is merely recording a new saved path of the existing file object. 2. The ifc_file path changed because the user manually changed it or selected a new file. This could reference an entirely new file object. This is dangerous because we can't trust anything anymore, including our undo history. So the new default situation is that there is no magic hook. If you change props.ifc_file, that's all it changes ... just a path stored in Blender with not much significance. If the user runs select_ifc_file to explicitly relink the file, it now explicitly purges in that situation and clears the undo history. Basically now behaviour is explicit, not using magic hooks. --- src/bonsai/bonsai/bim/handler.py | 4 +--- src/bonsai/bonsai/bim/ifc.py | 14 +++++++++----- src/bonsai/bonsai/bim/module/project/operator.py | 5 ++--- src/bonsai/bonsai/bim/operator.py | 3 +++ src/bonsai/bonsai/bim/prop.py | 8 +------- src/bonsai/bonsai/tool/blender.py | 13 +++++++++++++ src/bonsai/bonsai/tool/ifc.py | 12 ++++++++++++ 7 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index e0679a18c2..2f46d51e66 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -24,17 +24,15 @@ import ifcopenshell.util.unit import ifcopenshell.api.owner.settings import bonsai.bim import bonsai.tool as tool -import bonsai.core.owner as core_owner from bpy.app.handlers import persistent from bonsai.bim.ifc import IfcStore -from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation from bonsai.bim.module.model.data import AuthoringData from bonsai.bim.module.aggregate.decorator import AggregateDecorator from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator from bonsai.bim.module.nest.decorator import NestDecorator from mathutils import Vector -from math import cos, degrees +from math import cos from typing import Union, Callable diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 281d39df8e..1cac1d5cb1 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -33,7 +33,7 @@ import bonsai.bim.handler import bonsai.tool as tool from pathlib import Path from bonsai.tool.brick import BrickStore -from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast +from typing import Set, Union, Optional, TypedDict, Callable, NotRequired IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object] @@ -106,10 +106,7 @@ class IfcStore: def get_file(): if IfcStore.file is None: props = tool.Blender.get_bim_props() - IfcStore.path = props.ifc_file - # Interpret relative paths as relative to .blend file. - if IfcStore.path and not os.path.isabs(IfcStore.path): - IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path)) + IfcStore.set_path(props.ifc_file) if IfcStore.path: try: IfcStore.load_file(IfcStore.path) @@ -117,6 +114,13 @@ class IfcStore: print(f"Failed to load file {IfcStore.path}. Error details: {e}") return IfcStore.file + @staticmethod + def set_path(value): + IfcStore.path = value + # Interpret relative paths as relative to .blend file. + if IfcStore.path and not os.path.isabs(IfcStore.path): + IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path)) + @staticmethod def get_cache(): if IfcStore.cache is None and IfcStore.path: diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 36abbe8ffd..8a26654cbe 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -985,8 +985,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): if not self.is_advanced and not self.should_start_fresh_session: bpy.ops.bim.convert_to_blender() - bim_props = tool.Blender.get_bim_props() - bim_props.ifc_file = filepath + tool.Ifc.set_path(filepath) if not tool.Ifc.get(): self.report( {"ERROR"}, @@ -1650,7 +1649,7 @@ class ExportIFC(bpy.types.Operator): output_file = os.path.relpath(output_file, bpy.path.abspath("//")) bim_props = tool.Blender.get_bim_props() if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): - bim_props.ifc_file = output_file + tool.Ifc.set_path(output_file) save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath) if save_blend_file: bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 1690b9497f..3e4f651daa 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -30,6 +30,7 @@ import webbrowser import ifcopenshell import bonsai.bim import bonsai.tool as tool +import bonsai.bim.handler from bonsai.bim import import_ifc from bonsai.bim.prop import StrProperty from bonsai.bim.ui import IFCFileSelector @@ -222,6 +223,8 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): if self.is_existing_ifc_file(): props = tool.Blender.get_bim_props() props.ifc_file = self.get_filepath() + bonsai.bim.handler.loadIfcStore(bpy.context.scene) + tool.Blender.clear_undo_history() return {"FINISHED"} def invoke(self, context, event): diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 9a69807c23..af378bb8f4 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -137,12 +137,6 @@ def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None: bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir -def update_ifc_file(self: "BIMProperties", context: bpy.types.Context) -> None: - bim_props = tool.Blender.get_bim_props() - if bim_props.ifc_file: - bonsai.bim.handler.loadIfcStore(context.scene) - - def update_section_color(self: "BIMProperties", context: bpy.types.Context) -> None: section_node_group = bpy.data.node_groups.get("Section Override") if section_node_group is None: @@ -506,7 +500,7 @@ class BIMProperties(PropertyGroup): ) has_blend_warning: BoolProperty(name="Has Blend Warning", default=False) pset_dir: StringProperty(default=os.path.join("psets") + os.path.sep, name="Default Psets Directory") - ifc_file: StringProperty(name="IFC File", update=update_ifc_file) + ifc_file: StringProperty(name="IFC File") last_transaction: StringProperty(name="Last Transaction") should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False) section_plane_colour: FloatVectorProperty( diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index da85bb734f..8ac081eb93 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1616,3 +1616,16 @@ class Blender(bonsai.core.tool.Blender): if 0 <= index < len(collection): return collection[index] return None + + @classmethod + def clear_undo_history(cls) -> None: + """Clears the Blender history, Bonsai history, and IfcOpenShell history""" + old_undo_steps = bpy.context.preferences.edit.undo_steps + bpy.context.preferences.edit.undo_steps = 2 + for i in range(3): + bpy.ops.ed.undo_push(message="Undo history cleared") + bpy.context.preferences.edit.undo_steps = old_undo_steps + tool.Ifc.clear_history() + old_history_size = tool.Ifc.get().history_size + tool.Ifc.get().set_history_size(0) + tool.Ifc.get().set_history_size(old_history_size) diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 83c05101ef..0474fa52f5 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -56,6 +56,12 @@ class Ifc(bonsai.core.tool.Ifc): def get(cls) -> ifcopenshell.file: return IfcStore.get_file() + @classmethod + def set_path(cls, value: str) -> None: + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = value + IfcStore.set_path(value) + @classmethod def get_path(cls) -> str: """Get absolute filepath to the IFC file, return empty string if file is not saved.""" @@ -66,6 +72,12 @@ class Ifc(bonsai.core.tool.Ifc): if IfcStore.get_file(): return IfcStore.get_file().schema + @classmethod + def clear_history(cls) -> None: + IfcStore.last_transaction = "" + IfcStore.history = [] + IfcStore.future = [] + @classmethod def is_edited(cls, obj: bpy.types.Object, *, ignore_scale: bool = False) -> bool: return (not ignore_scale and tool.Geometry.is_scaled(obj)) or obj in IfcStore.edited_objs