Compare commits

...

3 Commits

Author SHA1 Message Date
Ryan Schultz 6b9dbebccc Decouple should_start_fresh_session from use_relative_path
so IFC files always open in a clean session
2026-06-08 20:16:45 -05:00
Ryan Schultz 7bcbe1b908 Fix relative path error when IFC not under blend dir
When use_relative_path is enabled, Path.relative_to() raises
ValueError if the IFC file is on a different path than the
.blend file. Fall back to the absolute path in that case.

Generated with the assistance of an AI coding tool.
2026-06-08 20:16:45 -05:00
Ryan Schultz 1ccd74e78a Closes #7765: Default IFC file path to relative and auto-convert on blend save
Previously, saving an IFC file stored an absolute path in `bim_props.ifc_file` by default. Users had to manually check "Use Relative Path" each time. This made projects less portable — moving or sharing a project folder broke the stored path, requiring manual relinking.

What changed:

-   `use_relative_project_path` now defaults to `True` in `BIMProjectProperties`, `LoadProject`, `ExportIFC`, and `SelectIfcFile`. Relative paths are opt-out rather than opt-in.

-   Added a `save_post` handler in `handler.py` (registered in `__init__.py`) that fires whenever the user saves the `.blend` file. If `use_relative_project_path` is enabled and the stored `ifc_file` path is still absolute, it converts it to a path relative to the blend file's directory. `IfcStore.path` is kept absolute internally so file loading continues to work correctly.

Generated with the assistance of an AI coding tool.
2026-06-08 20:16:44 -05:00
6 changed files with 34 additions and 11 deletions
+2
View File
@@ -271,6 +271,7 @@ def register():
parametric_lifecycle.install_parametric_lifecycle_handlers()
bpy.app.handlers.load_post.append(handler.load_post)
bpy.app.handlers.load_post.append(handler.loadIfcStore)
bpy.app.handlers.save_post.append(handler.save_post)
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
bpy.types.Scene.BIMSnapProperties = bpy.props.PointerProperty(type=prop.BIMSnapProperties)
bpy.types.Scene.BIMSnapGroups = bpy.props.PointerProperty(type=prop.BIMSnapGroups)
@@ -329,6 +330,7 @@ def unregister():
parametric_lifecycle.uninstall_parametric_lifecycle_handlers()
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
bpy.app.handlers.save_post.remove(handler.save_post)
del bpy.types.Scene.BIMProperties
del bpy.types.Collection.BIMCollectionProperties
del bpy.types.Object.BIMObjectProperties
+23
View File
@@ -432,6 +432,29 @@ def subscribe_to_viewport_shading_changes():
)
@persistent
def save_post(scene) -> None:
"""After saving the .blend file, convert the stored IFC path to relative if enabled."""
pprops = tool.Project.get_project_props()
if not pprops.use_relative_project_path:
return
bim_props = tool.Blender.get_bim_props()
ifc_path = bim_props.ifc_file
if not ifc_path or not os.path.isabs(ifc_path):
return
blend_dir = bpy.path.abspath("//")
if not blend_dir:
return
from pathlib import Path
from bonsai.bim.ifc import IfcStore
try:
rel_path = str(Path(ifc_path).relative_to(blend_dir))
except ValueError:
return # IFC file is not under the blend directory; keep absolute path
bim_props.ifc_file = rel_path
IfcStore.set_path(ifc_path) # keep IfcStore.path absolute for loading
def _apply_save_file_invariants(scene: bpy.types.Scene) -> None:
"""Invariants enforced on every load_post: msgbus subscription, IFC owner
settings, scene-bound caches, load-transient parametric state, and the
@@ -964,11 +964,11 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path",
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
default=False,
default=True,
)
should_start_fresh_session: bpy.props.BoolProperty(
name="Should Start Fresh Session",
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
description="Clear current Blender session before loading IFC",
default=True,
)
import_without_ifc_data: bpy.props.BoolProperty(
@@ -1076,9 +1076,6 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
bpy.app.handlers.load_post.remove(load_handler)
self.finish_loading_project(context)
if self.use_relative_path:
self.should_start_fresh_session = False
if self.should_start_fresh_session:
# WARNING: wm.read_homefile clears context which could lead to some
# operators to fail:
@@ -1143,8 +1140,6 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
return ImportHelper.invoke(self, context, event)
def draw(self, context):
if self.use_relative_path:
self.should_start_fresh_session = False
self.layout.prop(self, "is_advanced")
self.layout.prop(self, "should_start_fresh_session")
self.layout.prop(self, "import_without_ifc_data")
@@ -1875,7 +1870,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version")
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
if TYPE_CHECKING:
filter_glob: str
+1 -1
View File
@@ -444,7 +444,7 @@ class BIMProjectProperties(PropertyGroup):
items=get_parent_libaries,
)
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=True)
should_save_metadata_for_this_file: BoolProperty(
name="Save Session Data for This File",
description="Enable saving session data (window layout, settings) to a metadata blend file for this specific IFC file",
+1 -1
View File
@@ -219,7 +219,7 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_options = {"REGISTER", "UNDO"}
bl_description = f"Select a different IFC file.\n{tool.Blender.operator_invoke_filepath_hotkeys_description}"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filename_ext = ".ifc"
def execute(self, context):
+4 -1
View File
@@ -94,7 +94,10 @@ class IFCFileSelector:
filepath = self.get_filepath_abs()
if self.use_relative_path:
filepath = filepath.relative_to(bpy.path.abspath("//"))
try:
filepath = filepath.relative_to(bpy.path.abspath("//"))
except ValueError:
pass # IFC file is not under the blend directory; keep absolute path
return filepath.as_posix().replace("\\", "/")
def draw(self, context: bpy.types.Context) -> None: