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.
This commit is contained in:
Dion Moult
2025-03-11 18:37:53 +11:00
parent 570513b60b
commit bf54132805
7 changed files with 41 additions and 18 deletions
+1 -3
View File
@@ -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
+9 -5
View File
@@ -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:
@@ -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)
+3
View File
@@ -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):
+1 -7
View File
@@ -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(
+13
View File
@@ -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)
+12
View File
@@ -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