diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 6a3802c44f..29be20faf4 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -240,8 +240,6 @@ def on_register(scene): is_registering = False - - # Classes that need to be registered after modules (due to cross-module dependencies) late_classes = ( ui.DefaultParameters, # Register before BIM_ADDON_preferences @@ -285,7 +283,7 @@ def register(): bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) - + if hasattr(bpy.types, "UI_MT_button_context_menu"): bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu) bpy.types.STATUSBAR_HT_header.append(ui.draw_statusbar) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 6ba13dbb4d..3b3dd502e4 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -539,7 +539,9 @@ def draw_filter( if preferences.chain_filter_with_set_operations: show_mode_toggle = j > 0 else: - show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode + show_mode_toggle = ( + preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 + ) # PR 7315 mode if show_mode_toggle: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( @@ -758,7 +760,9 @@ def draw_filter( if preferences.chain_filter_with_set_operations: show_mode_toggle = j > 0 else: - show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode + show_mode_toggle = ( + preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 + ) # PR 7315 mode if show_mode_toggle: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( @@ -785,19 +789,22 @@ def draw_filter( op.index = j op.module = module + # ============================================================================ # UI Panel Visibility Helpers # ============================================================================ + def get_tab_names(): from bonsai.bim.prop import get_tab + enum_items = get_tab(None, None) # Exclude None separators and the BLENDER tab (not part of BIM tab system) return [item[0] for item in enum_items if item is not None and item[0] != "BLENDER"] def get_panel_tab_name(panel_class): - if hasattr(panel_class, 'bim_tab_name'): + if hasattr(panel_class, "bim_tab_name"): return panel_class.bim_tab_name return "PROJECT" # Default fallback @@ -805,10 +812,10 @@ def get_panel_tab_name(panel_class): def should_show_panel(panel_id, panel_tab_name, context): if tool.Blender.is_tab(context, "BOOKMARK"): return is_panel_bookmarked(panel_id) and get_panel_visibility(panel_id, "BOOKMARK") - + if tool.Blender.is_tab(context, panel_tab_name): return get_tab_visibility(panel_tab_name) and get_panel_visibility(panel_id, panel_tab_name) - + return False @@ -851,11 +858,11 @@ def get_panel_config(panel_id, create_if_missing=False): bim_props = tool.Blender.get_bim_props() except (AttributeError, AssertionError): return None - + for prop in bim_props.panel_properties: if prop.name == panel_id: return prop - + if create_if_missing: try: prop = bim_props.panel_properties.add() @@ -866,15 +873,13 @@ def get_panel_config(panel_id, create_if_missing=False): return prop except AttributeError: pass - - return None + return None def get_all_tab_panels(force_refresh=False): panels = {tab_name: [] for tab_name in get_tab_names() if tab_name != "BOOKMARK"} panels["BOOKMARK"] = [] - bim_props = tool.Blender.get_bim_props() for prop in bim_props.panel_properties: @@ -884,25 +889,25 @@ def get_all_tab_panels(force_refresh=False): if tab_name and tab_name != "BOOKMARK": bl_label = getattr(panel_class, "bl_label", prop.name) panels[tab_name].append({"bl_idname": prop.name, "bl_label": bl_label}) - + if prop.is_bookmarked: panel_class = getattr(bpy.types, prop.name, None) if panel_class: bl_label = getattr(panel_class, "bl_label", prop.name) panels["BOOKMARK"].append({"bl_idname": prop.name, "bl_label": bl_label}) - + if not panels["BOOKMARK"]: panels["BOOKMARK"] = [{}] - + return panels def initialize_tab_visibilities(): bim_props = tool.Blender.get_bim_props() - + if len(bim_props.tab_visibilities) > 0: return - + for tab_name in get_tab_names(): tab_vis = bim_props.tab_visibilities.add() tab_vis.name = tab_name @@ -910,20 +915,20 @@ def initialize_tab_visibilities(): def initialize_panel_properties(): - + bim_props = tool.Blender.get_bim_props() - + if len(bim_props.panel_properties) > 0: return - + for attr_name in dir(bpy.types): if attr_name.startswith("BIM_PT_tab_"): panel_class = getattr(bpy.types, attr_name) - if not hasattr(panel_class, 'bl_idname'): + if not hasattr(panel_class, "bl_idname"): continue - + panel_id = panel_class.bl_idname - + prop = bim_props.panel_properties.add() prop.name = panel_id prop.is_visible_in_tab = True diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 607a1625c4..94d7279daa 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1056,9 +1056,11 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): return tooltip def execute(self, context): - if (tool.Blender.get_addon_preferences().save_metadata_blend_file - and self.should_start_fresh_session - and not self.is_advanced): + if ( + tool.Blender.get_addon_preferences().save_metadata_blend_file + and self.should_start_fresh_session + and not self.is_advanced + ): filepath = self.get_filepath() metadata_path = Path(str(filepath) + ".metadata.blend") if metadata_path.exists() and metadata_path.is_file(): @@ -1067,7 +1069,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): return {"FINISHED"} except Exception as e: self.report({"WARNING"}, f"Failed to load metadata file, using regular load: {e}") - + @persistent def load_handler(*args): bpy.app.handlers.load_post.remove(load_handler) @@ -1757,12 +1759,15 @@ class ExportIFC(bpy.types.Operator, ExportHelper): if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): tool.Ifc.set_path(output_file) bim_props.is_dirty = False - + if tool.Blender.get_addon_preferences().save_metadata_blend_file: try: bpy.ops.bim.save_blend_metadata_file() blendmetadata_path = output_file + ".metadata.blend" - self.report({"INFO"}, f'IFC Project "{os.path.basename(output_file)}" And Metadata File Saved to: {os.path.basename(blendmetadata_path)}') + self.report( + {"INFO"}, + f'IFC Project "{os.path.basename(output_file)}" And Metadata File Saved to: {os.path.basename(blendmetadata_path)}', + ) except Exception as e: self.report({"ERROR"}, f"Failed to save blend metadata file: {e}") else: @@ -1773,7 +1778,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper): {"INFO"}, f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved', ) - + bonsai.bim.handler.refresh_ui_data() @classmethod diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 7eb1ff16fd..d3e4a307ee 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -33,7 +33,15 @@ import bonsai.bim import bonsai.tool as tool import bonsai.bim.handler from enum import Enum -from bonsai.bim.helper import get_all_tab_panels, get_tab_visibility, set_tab_visibility, get_tab_names, get_panel_config, initialize_panel_properties, initialize_tab_visibilities +from bonsai.bim.helper import ( + get_all_tab_panels, + get_tab_visibility, + set_tab_visibility, + get_tab_names, + get_panel_config, + initialize_panel_properties, + initialize_tab_visibilities, +) from bpy_extras.io_utils import ImportHelper from bonsai.bim import import_ifc from bonsai.bim.prop import StrProperty @@ -236,6 +244,7 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper): return res return ImportHelper.invoke(self, context, event) + class SaveBlendMetadataFile(bpy.types.Operator): bl_idname = "bim.save_blend_metadata_file" bl_label = "Save Blend Metadata File" @@ -254,7 +263,7 @@ class SaveBlendMetadataFile(bpy.types.Operator): blendmetadata_path = ifc_file + ".metadata.blend" # Save a temporary copy of the current blend file - temp_path = bpy.path.abspath('//__temp_blendmetadata.blend') + temp_path = bpy.path.abspath("//__temp_blendmetadata.blend") bpy.ops.wm.save_as_mainfile(filepath=temp_path, copy=True) cleanup_script = f""" @@ -323,18 +332,17 @@ bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}') """ import tempfile - with tempfile.NamedTemporaryFile('w', suffix='.py', delete=False) as script_file: + + with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as script_file: script_file.write(cleanup_script) script_path = script_file.name blender_exe = bpy.app.binary_path import subprocess - result = subprocess.run([ - blender_exe, - temp_path, - '--background', - '--python', script_path - ], capture_output=True, text=True) + + result = subprocess.run( + [blender_exe, temp_path, "--background", "--python", script_path], capture_output=True, text=True + ) # Print the output from the background process (includes debug info) if result.stdout: @@ -352,6 +360,7 @@ bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}') return {"FINISHED"} + class LoadBlendMetadataAndIFC(bpy.types.Operator): bl_idname = "bim.load_blend_metadata_and_ifc" bl_label = "Load Blend Metadata and IFC" @@ -363,7 +372,7 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator): if not ifc_file: props = tool.Blender.get_bim_props() ifc_file = getattr(props, "ifc_file", None) - + if not ifc_file: self.report({"WARNING"}, "No IFC file path set.") return {"CANCELLED"} @@ -379,6 +388,7 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator): self.report({"INFO"}, f"Loaded metadata and IFC: {metadata_path}, {ifc_file}") return {"FINISHED"} + # TODO: Unused operator. # Is there a need for this or 'DIR_PATH' propety subtype does almost the same, # but also has alt+click? @@ -1781,9 +1791,11 @@ class BIM_OT_toggle_panel_visibility(bpy.types.Operator): def execute(self, context): panel_name = self.action.replace("TOGGLE_VISIBILITY_", "") - active_tab = getattr(context.scene, "active_tab_name", None) or getattr(tool.Blender.get_bim_props(), "tab", None) + active_tab = getattr(context.scene, "active_tab_name", None) or getattr( + tool.Blender.get_bim_props(), "tab", None + ) is_bookmark_tab = active_tab == "BOOKMARK" - + panel_config = get_panel_config(panel_name, create_if_missing=True) if panel_config: if is_bookmark_tab: @@ -1792,16 +1804,16 @@ class BIM_OT_toggle_panel_visibility(bpy.types.Operator): else: panel_config.is_visible_in_tab = not panel_config.is_visible_in_tab new_value = panel_config.is_visible_in_tab - + for item in context.scene.tab_panels: if item.name == panel_name: item["visible"] = new_value break - + for area in bpy.context.window.screen.areas: if area.type == "PROPERTIES": area.tag_redraw() - + tab_context = "Bookmarks" if is_bookmark_tab else "Tab" self.report({"INFO"}, f"Toggled visibility for {panel_name} in {tab_context}.") return {"FINISHED"} @@ -1819,19 +1831,19 @@ class BIM_OT_bookmark_panel(bpy.types.Operator): def execute(self, context): panel_name = self.action.replace("BOOKMARK_", "") panel_config = get_panel_config(panel_name, create_if_missing=True) - + if panel_config: panel_config.is_bookmarked = not panel_config.is_bookmarked - + for item in context.scene.tab_panels: if item.name == panel_name: item["bookmarked"] = panel_config.is_bookmarked break - + for area in bpy.context.window.screen.areas: if area.type == "PROPERTIES": area.tag_redraw() - + self.report({"INFO"}, f"Toggled bookmark for {panel_name}.") return {"FINISHED"} @@ -1846,20 +1858,20 @@ class BIM_OT_manage_tab_panels(bpy.types.Operator): tab_name: bpy.props.StringProperty() def invoke(self, context, event): - + context.scene.active_tab_name = self.tab_name context.scene.tab_panels.clear() - + initialize_tab_visibilities() initialize_panel_properties() all_panels = get_all_tab_panels(force_refresh=True) - + for panel_data in all_panels.get(self.tab_name, []): panel_name = panel_data.get("bl_idname", "") panel_label = panel_data.get("bl_label", "") if not panel_name or not panel_label: continue - + item = context.scene.tab_panels.add() item.name = panel_name item["bl_label"] = panel_label @@ -1894,8 +1906,6 @@ class BIM_OT_manage_tab_panels(bpy.types.Operator): panel_config.is_visible_in_tab = item["visible"] panel_config.is_bookmarked = item["bookmarked"] - - self.report({"INFO"}, f"Panels for {self.tab_name} managed successfully.") return {"FINISHED"} @@ -1955,9 +1965,6 @@ class BIM_OT_toggle_tab_visibility(bpy.types.Operator): return {"FINISHED"} - - - class BIM_OT_reset_ui_layout(bpy.types.Operator): """Reset UI Layout to Default""" diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index f0d3f8e5c0..f9c7f57782 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -633,7 +633,7 @@ class BIMProperties(PropertyGroup): ], name="Time Unit", default="HOUR", - ) + ) tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities") panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties") diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 47d51e5484..6c2baaa0fb 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -702,16 +702,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): description="Show mass and time units section in the new project wizard panel", default=False, ) - save_metadata_blend_file: BoolProperty( - name="Save non ifc data to .metadata.blend File", - description="Save session data (window layout, settings) to a .metadata.blend file alongside the IFC file. This file is automatically loaded when opening the project.", - default=False, - ) - user_ui_customization: BoolProperty( - name="User UI Customization", - description="Enable user interface customization features (hide/show tabs and panels, bookmark panels) and save the session settings as part of the .metadata.blend file", - default=False, - ) chain_filter_with_set_operations: BoolProperty( name="NEW filter mode: Enable chained filters with set operations", @@ -724,6 +714,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): default=False, ) + save_metadata_blend_file: BoolProperty( + name="Save non ifc data to .metadata.blend File", + description="Save session data (window layout, settings) to a .metadata.blend file alongside the IFC file. This file is automatically loaded when opening the project.", + default=False, + ) + user_ui_customization: BoolProperty( + name="User UI Customization", + description="Enable user interface customization features (hide/show tabs and panels, bookmark panels) and save the session settings as part of the .metadata.blend file", + default=False, + ) + if TYPE_CHECKING: svg2pdf_command: str svg2dxf_command: str @@ -1007,7 +1008,11 @@ class BIM_PT_tabs(Panel): self.draw_tab_entry(row_left, "PACKAGE", "FM", True, aprops.tab == "FM") if get_tab_visibility("QUALITY"): self.draw_tab_entry(row_left, "COMMUNITY", "QUALITY", True, aprops.tab == "QUALITY") - if addon_prefs.save_metadata_blend_file and addon_prefs.user_ui_customization and get_tab_visibility("BOOKMARK"): + if ( + addon_prefs.save_metadata_blend_file + and addon_prefs.user_ui_customization + and get_tab_visibility("BOOKMARK") + ): self.draw_tab_entry(row_left, "SOLO_ON", "BOOKMARK", True, aprops.tab == "BOOKMARK") row_left.operator("bim.switch_tab", text="", emboss=False, icon="UV_SYNC_SELECT") @@ -1018,7 +1023,7 @@ class BIM_PT_tabs(Panel): if not (addon_prefs.save_metadata_blend_file and addon_prefs.user_ui_customization): row_left.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False) - + for tab in get_tab_names(): # Draw a little underscore below the active tab icon. if get_tab_visibility(tab):