diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 3d865de16e..29be20faf4 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -115,11 +115,13 @@ classes = [ operator.EditBlenderCollection, operator.FileAssociate, operator.FileUnassociate, + operator.LoadBlendMetadataAndIFC, operator.OpenPath, operator.OpenUpstream, operator.OpenUri, operator.ReloadIfcFile, operator.RevertClippingPlaneCut, + operator.SaveBlendMetadataFile, operator.SelectDir, operator.SelectIfcFile, operator.SelectURIAttribute, @@ -129,12 +131,21 @@ classes = [ prop.StrProperty, operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty operator.BIM_OT_attribute_search_values, + operator.BIM_UL_tab_panels, + operator.BIM_OT_toggle_panel_visibility, + operator.BIM_OT_bookmark_panel, + operator.BIM_OT_manage_tab_panels, + operator.BIM_OT_manage_tab_visibility, + operator.BIM_OT_toggle_tab_visibility, + operator.BIM_OT_reset_ui_layout, prop.ObjProperty, prop.MultipleFileSelect, prop.Attribute, prop.ISODuration, prop.BIMAreaProperties, prop.BIMTabProperties, + prop.BIMTabVisibility, # Must be registered before BIMProperties + prop.BIMPanelProperties, # Must be registered before BIMProperties prop.BIMProperties, prop.IfcParameter, prop.PsetQto, @@ -272,6 +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) @@ -307,6 +319,10 @@ def register(): # RestrictedContext doesn't allow accessing scene attribute, postpone it for a bit. bpy.app.timers.register(tool.Blender.setup_user_data_dir, first_interval=0.1) + bpy.types.Scene.active_tab_name = bpy.props.StringProperty() + bpy.types.Scene.tab_panels = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + bpy.types.Scene.active_tab_panel_index = bpy.props.IntProperty() + def unregister(): global icons @@ -348,3 +364,7 @@ def unregister(): tool.Blender.remove_scene_panel_override(panel) bpy.app.translations.unregister("bonsai") + + del bpy.types.Scene.active_tab_name + del bpy.types.Scene.tab_panels + del bpy.types.Scene.active_tab_panel_index diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 873d368fad..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( @@ -784,3 +788,149 @@ def draw_filter( op.group_index = i 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"): + return panel_class.bim_tab_name + return "PROJECT" # Default fallback + + +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 + + +def get_tab_visibility(tab_name): + bim_props = tool.Blender.get_bim_props() + tab_vis = bim_props.tab_visibilities.get(tab_name) + return tab_vis.is_visible if tab_vis else True + + +def set_tab_visibility(tab_name, visible): + bim_props = tool.Blender.get_bim_props() + tab_vis = bim_props.tab_visibilities.get(tab_name) + if tab_vis: + tab_vis.is_visible = visible + else: + new_tab = bim_props.tab_visibilities.add() + new_tab.name = tab_name + new_tab.is_visible = visible + + +def get_panel_visibility(panel_id, current_tab=None): + panel_config = get_panel_config(panel_id) + if panel_config: + if current_tab == "BOOKMARK": + return panel_config.is_visible_in_bookmarks + else: + return panel_config.is_visible_in_tab + return True + + +def is_panel_bookmarked(panel_id): + panel_config = get_panel_config(panel_id) + if panel_config: + return panel_config.is_bookmarked + return False + + +def get_panel_config(panel_id, create_if_missing=False): + try: + 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() + prop.name = panel_id + prop.is_visible_in_tab = True + prop.is_visible_in_bookmarks = True + prop.is_bookmarked = False + return prop + except AttributeError: + pass + + 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: + panel_class = getattr(bpy.types, prop.name, None) + if panel_class: + tab_name = get_panel_tab_name(panel_class) + 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 + tab_vis.is_visible = True + + +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"): + continue + + panel_id = panel_class.bl_idname + + prop = bim_props.panel_properties.add() + prop.name = panel_id + prop.is_visible_in_tab = True + prop.is_visible_in_bookmarks = True + prop.is_bookmarked = False diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index a022af28d0..fc3b9ecbf6 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -2093,5 +2093,6 @@ class DecorationsHandler: if not DecoratorData.is_loaded: DecoratorData.load(self) - for obj, decorator in DecoratorData.data["object_decorators"]: + object_decorators = DecoratorData.data.get("object_decorators", []) + for obj, decorator in object_decorators: decorator.decorate(context, obj) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 8bad2e17d4..94d7279daa 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1056,6 +1056,20 @@ 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 + ): + filepath = self.get_filepath() + metadata_path = Path(str(filepath) + ".metadata.blend") + if metadata_path.exists() and metadata_path.is_file(): + try: + bpy.ops.bim.load_blend_metadata_and_ifc(filepath=filepath) + 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) @@ -1744,15 +1758,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper): bim_props = tool.Blender.get_bim_props() if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): 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) 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)}', + ) + except Exception as e: + self.report({"ERROR"}, f"Failed to save blend metadata file: {e}") + else: + 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) + self.report( + {"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() - self.report( - {"INFO"}, - f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved', - ) @classmethod def description(cls, context, properties): diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 6fe5b55604..fa45494e0f 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -331,6 +331,13 @@ class BIM_PT_project(Panel): col.prop(props, "ifc_file", text="") row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="") + if tool.Blender.get_addon_preferences().save_metadata_blend_file: + row = self.layout.row(align=True) + col = row.column() + col.enabled = False + metadata_filename = os.path.basename(props.ifc_file) + ".metadata.blend" + col.label(text=f"Saving session data to: {metadata_filename}") + class BIM_PT_new_project_wizard(Panel): bl_label = "New Project Wizard" diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 517bfe34f1..d3e4a307ee 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -33,6 +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 bpy_extras.io_utils import ImportHelper from bonsai.bim import import_ifc from bonsai.bim.prop import StrProperty @@ -236,6 +245,150 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper): return ImportHelper.invoke(self, context, event) +class SaveBlendMetadataFile(bpy.types.Operator): + bl_idname = "bim.save_blend_metadata_file" + bl_label = "Save Blend Metadata File" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """ + Save the current blend file as a metadata-only file (no geometry), preserving settings, window arrangement, geometry nodes, etc. + """ + 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"} + + blendmetadata_path = ifc_file + ".metadata.blend" + + # Save a temporary copy of the current blend file + temp_path = bpy.path.abspath("//__temp_blendmetadata.blend") + bpy.ops.wm.save_as_mainfile(filepath=temp_path, copy=True) + + cleanup_script = f""" +import bpy + +def remove_ifc_collections(): + \"\"\"Remove all collections that start with 'IfcProject' and their contents.\"\"\" + collections_to_remove = [] + + # Find all IfcProject collections + for collection in bpy.data.collections: + if collection.name.startswith('IfcProject'): + collections_to_remove.append(collection) + + # First, use bim.override_outliner_delete to properly remove IFC data + for collection in collections_to_remove: + # Collect all objects in the collection and its children + objects_to_delete = [] + + def collect_objects(col): + for obj in col.objects: + if obj not in objects_to_delete: + objects_to_delete.append(obj) + for child in col.children: + collect_objects(child) + + collect_objects(collection) + + # Use bim.override_outliner_delete for each object to clean up IFC data + for obj in objects_to_delete: + # Select only this object + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # Try to use Bonsai's delete operator which cleans up IFC data + try: + bpy.ops.bim.override_outliner_delete() + except: + # If that fails, use standard delete + bpy.data.objects.remove(obj, do_unlink=True) + + # Remove any remaining child collections + for child in list(collection.children): + try: + bpy.data.collections.remove(child, do_unlink=True) + except: + pass + + # Remove the IfcProject collections themselves + for collection in collections_to_remove: + try: + bpy.data.collections.remove(collection, do_unlink=True) + except: + pass + +# Remove IFC-related data +remove_ifc_collections() + +# Note: Scene-level BIM properties are saved with the metadata.blend file but will be +# overwritten when the IFC is loaded. Screen-level properties (BIMTabProperties, +# BIMPanelProperties, BIMAreaProperties) preserve UI customization settings. + +# Save the metadata file +bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}') +""" + + import tempfile + + 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 + ) + + # Print the output from the background process (includes debug info) + if result.stdout: + print("\n=== Background Blender Output ===") + print(result.stdout) + if result.stderr: + print("\n=== Background Blender Errors ===") + print(result.stderr) + + try: + os.remove(temp_path) + os.remove(script_path) + except Exception: + pass + + return {"FINISHED"} + + +class LoadBlendMetadataAndIFC(bpy.types.Operator): + bl_idname = "bim.load_blend_metadata_and_ifc" + bl_label = "Load Blend Metadata and IFC" + bl_options = {"REGISTER", "UNDO"} + filepath: bpy.props.StringProperty(name="IFC File Path", default="") + + def execute(self, context): + ifc_file = self.filepath + 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"} + + metadata_path = ifc_file + ".metadata.blend" + # Open the metadata blend file + bpy.ops.wm.open_mainfile(filepath=metadata_path) + # After loading metadata, clear blend warning (no geometry loaded yet) + props = tool.Blender.get_bim_props() + props.has_blend_warning = False + # Load the IFC file into the current session (preserve layout) + bpy.ops.bim.load_project(filepath=ifc_file, should_start_fresh_session=False) + 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? @@ -1605,3 +1758,244 @@ class BIM_OT_attribute_remove_subitem(bpy.types.Operator): attr.is_null = True return {"FINISHED"} + + +class BIM_UL_tab_panels(bpy.types.UIList): + """UIList for Tab Panels""" + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + row = layout.row(align=True) + row.label(text=item["bl_label"]) + + row.operator( + "bim.toggle_panel_visibility", + text="", + icon="HIDE_OFF" if item.get("visible", True) else "HIDE_ON", + ).action = f"TOGGLE_VISIBILITY_{item.name}" + + row.operator( + "bim.bookmark_panel", + text="", + icon="SOLO_ON" if item.get("bookmarked", False) else "SOLO_OFF", + ).action = f"BOOKMARK_{item.name}" + + +class BIM_OT_toggle_panel_visibility(bpy.types.Operator): + """Toggle Panel Visibility""" + + bl_idname = "bim.toggle_panel_visibility" + bl_label = "Toggle Panel Visibility" + bl_options = {"REGISTER", "UNDO"} + + action: bpy.props.StringProperty() + + 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 + ) + is_bookmark_tab = active_tab == "BOOKMARK" + + panel_config = get_panel_config(panel_name, create_if_missing=True) + if panel_config: + if is_bookmark_tab: + panel_config.is_visible_in_bookmarks = not panel_config.is_visible_in_bookmarks + new_value = panel_config.is_visible_in_bookmarks + 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"} + + +class BIM_OT_bookmark_panel(bpy.types.Operator): + """Bookmark Panel""" + + bl_idname = "bim.bookmark_panel" + bl_label = "Bookmark Panel" + bl_options = {"REGISTER", "UNDO"} + + action: bpy.props.StringProperty() + + 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"} + + +class BIM_OT_manage_tab_panels(bpy.types.Operator): + """Manage Tab Panels""" + + bl_idname = "bim.manage_tab_panels" + bl_label = "Manage Tab Panels" + bl_options = {"REGISTER", "UNDO"} + + 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 + + panel_config = get_panel_config(panel_name, create_if_missing=True) + if panel_config: + if self.tab_name == "BOOKMARK": + item["visible"] = panel_config.is_visible_in_bookmarks + else: + item["visible"] = panel_config.is_visible_in_tab + item["bookmarked"] = panel_config.is_bookmarked + else: + item["visible"] = True + item["bookmarked"] = False + + return context.window_manager.invoke_popup(self) + + def draw(self, context): + layout = self.layout + layout.label(text=f"Manage Panels for {self.tab_name} Tab") + + row = layout.row() + row.template_list("BIM_UL_tab_panels", "", context.scene, "tab_panels", context.scene, "active_tab_panel_index") + + def execute(self, context): + for item in context.scene.tab_panels: + panel_config = get_panel_config(item.name, create_if_missing=True) + if panel_config: + if self.tab_name == "BOOKMARK": + panel_config.is_visible_in_bookmarks = item["visible"] + else: + 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"} + + +class BIM_OT_manage_tab_visibility(bpy.types.Operator): + """Manage Tab Visibility""" + + bl_idname = "bim.manage_tab_visibility" + bl_label = "Manage Tab Visibility" + bl_options = {"REGISTER", "UNDO"} + + def draw(self, context): + layout = self.layout + row = layout.row() + row = self.layout.row(align=True) + row.alignment = "RIGHT" + + row.operator("bim.reset_ui_layout", icon="FILE_REFRESH", text="") + row = layout.row() + row = self.layout.row(align=True) + row.alignment = "CENTER" + + for tab_name in get_tab_names(): + row = layout.row() + row.label(text=tab_name) + is_visible = get_tab_visibility(tab_name) + icon = "HIDE_OFF" if is_visible else "HIDE_ON" + op = row.operator("bim.toggle_tab_visibility", text="", icon=icon) + op.tab_name = tab_name + + def execute(self, context): + return {"FINISHED"} + + def invoke(self, context, event): + return context.window_manager.invoke_popup(self) + + +class BIM_OT_toggle_tab_visibility(bpy.types.Operator): + """Toggle Tab Visibility""" + + bl_idname = "bim.toggle_tab_visibility" + bl_label = "Toggle Tab Visibility" + bl_options = {"REGISTER", "UNDO"} + + tab_name: bpy.props.StringProperty() + + def execute(self, context): + if self.tab_name in get_tab_names(): + current_visibility = get_tab_visibility(self.tab_name) + set_tab_visibility(self.tab_name, not current_visibility) + + for area in bpy.context.window.screen.areas: + if area.type == "PROPERTIES": + area.tag_redraw() + + self.report({"INFO"}, f"Toggled visibility for {self.tab_name}.") + return {"FINISHED"} + + +class BIM_OT_reset_ui_layout(bpy.types.Operator): + """Reset UI Layout to Default""" + + bl_idname = "bim.reset_ui_layout" + bl_label = "Reset UI Layout" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + + for tab_name in get_tab_names(): + set_tab_visibility(tab_name, True) + + get_all_tab_panels()["BOOKMARK"] = [{}] + + for tab_name, panels in get_all_tab_panels().items(): + for panel in panels: + panel_name = panel.get("bl_idname", "") + if not panel_name: + continue + + show_prop_name = f"show_{panel_name.lower()}" + if hasattr(context.scene, show_prop_name): + setattr(context.scene, show_prop_name, True) + + bookmark_prop_name = f"bookmark_{panel_name.lower()}" + if hasattr(context.scene, bookmark_prop_name): + setattr(context.scene, bookmark_prop_name, False) + + for area in bpy.context.window.screen.areas: + if area.type == "PROPERTIES": + area.tag_redraw() + + self.report({"INFO"}, "UI layout reset to default.") + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 3ca57b27cc..f9c7f57782 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -493,8 +493,9 @@ def get_tab( ("SCHEDULING", "Costing and Scheduling", "", "NLA", 6), ("FM", "Facility Management", "", "PACKAGE", 7), ("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8), + ("BOOKMARK", "Bookmark", "", "SOLO_ON", 9), None, - ("BLENDER", "Blender Properties", "", "BLENDER", 9), + ("BLENDER", "Blender Properties", "", "BLENDER", 10), ] return get_tab.enum_items @@ -529,6 +530,26 @@ class BIMTabProperties(PropertyGroup): inactive_tab: bool +class BIMTabVisibility(PropertyGroup): + name: StringProperty(name="Tab Name") + is_visible: BoolProperty(name="Is Visible", default=True) + + if TYPE_CHECKING: + name: str + is_visible: bool + + +class BIMPanelProperties(PropertyGroup): + is_visible_in_tab: BoolProperty(name="Is Visible in Tab", default=True) + is_visible_in_bookmarks: BoolProperty(name="Is Visible in Bookmarks", default=True) + is_bookmarked: BoolProperty(name="Is Bookmarked", default=False) + + if TYPE_CHECKING: + is_visible_in_tab: bool + is_visible_in_bookmarks: bool + is_bookmarked: bool + + class BIMProperties(PropertyGroup): is_dirty: BoolProperty(name="Is Dirty", default=False) schema_dir: StringProperty( @@ -613,6 +634,9 @@ class BIMProperties(PropertyGroup): name="Time Unit", default="HOUR", ) + tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities") + panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties") + if TYPE_CHECKING: is_dirty: bool schema_dir: str @@ -627,6 +651,8 @@ class BIMProperties(PropertyGroup): volume_unit: str mass_unit: str time_unit: str + tab_visibilities: bpy.types.bpy_prop_collection[BIMTabVisibility] + panel_properties: bpy.types.bpy_prop_collection[BIMPanelProperties] class IfcParameter(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 05c836ca93..6c2baaa0fb 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -36,6 +36,19 @@ import bonsai.bim import bonsai.tool as tool from ifcopenshell.util.file import IfcHeaderExtractor from bonsai.bim.prop import Attribute +from bonsai.bim.helper import ( + get_tab_names, + get_panel_tab_name, + should_show_panel, + get_tab_visibility, + set_tab_visibility, + get_panel_visibility, + is_panel_bookmarked, + get_panel_config, + get_all_tab_panels, + initialize_tab_visibilities, + initialize_panel_properties, +) from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty from bonsai.bim.module.pset.prop import IfcProperty from bonsai.bim.module.model.prop import ( @@ -139,8 +152,8 @@ class IFCFileSelector: class BIM_PT_section_plane(Panel): - bl_label = "Temporary Section Cutaways" bl_idname = "BIM_PT_section_plane" + bl_label = "Temporary Section Cutaways" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "output" @@ -163,8 +176,8 @@ class BIM_PT_section_plane(Panel): class BIM_PT_section_with_cappings(Panel): - bl_label = "Section Cutaways With Cappings" bl_idname = "BIM_PT_section_with_cappings" + bl_label = "Section Cutaways With Cappings" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "output" @@ -689,7 +702,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): description="Show mass and time units section in the new project wizard panel", default=False, ) - + chain_filter_with_set_operations: BoolProperty( name="NEW filter mode: Enable chained filters with set operations", description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values", @@ -701,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 @@ -740,6 +764,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): mass_time_units_in_wizard: bool chain_filter_with_set_operations: bool default_filter_with_set_operations_for_globalid_and_class: bool + save_metadata_blend_file: bool + user_ui_customization: bool def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -930,10 +956,16 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row = box.row(align=True) row.prop(self, "default_filter_with_set_operations_for_globalid_and_class") row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/comment/27030" + layout.prop(self, "save_metadata_blend_file") + if self.save_metadata_blend_file: + row = layout.row() + row.separator() + row.prop(self, "user_ui_customization") # Scene panel groups class BIM_PT_tabs(Panel): + bl_idname = "BIM_PT_tabs" bl_label = "Bonsai" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -946,51 +978,76 @@ class BIM_PT_tabs(Panel): UIData.load() is_ifc_project = bool(tool.Ifc.get()) aprops = tool.Blender.get_area_props(context) + addon_prefs = tool.Blender.get_addon_preferences() ifc_icon = f"{UIData.data['tabs_icon_color_mode']}_ifc" - row = self.layout.row() - row.alignment = "CENTER" - row.operator( - "bim.set_tab", - text="", - emboss=aprops.tab == "PROJECT", - icon_value=bonsai.bim.icons[ifc_icon].icon_id, - ).tab = "PROJECT" - self.draw_tab_entry(row, "FILE_3D", "OBJECT", is_ifc_project, aprops.tab == "OBJECT") - self.draw_tab_entry(row, "MATERIAL", "GEOMETRY", is_ifc_project, aprops.tab == "GEOMETRY") - self.draw_tab_entry(row, "DOCUMENTS", "DRAWINGS", is_ifc_project, aprops.tab == "DRAWINGS") - self.draw_tab_entry(row, "NETWORK_DRIVE", "SERVICES", is_ifc_project, aprops.tab == "SERVICES") - self.draw_tab_entry(row, "EDITMODE_HLT", "STRUCTURE", is_ifc_project, aprops.tab == "STRUCTURE") - self.draw_tab_entry(row, "NLA", "SCHEDULING", is_ifc_project, aprops.tab == "SCHEDULING") - self.draw_tab_entry(row, "PACKAGE", "FM", True, aprops.tab == "FM") - self.draw_tab_entry(row, "COMMUNITY", "QUALITY", True, aprops.tab == "QUALITY") - row.operator("bim.switch_tab", text="", emboss=False, icon="UV_SYNC_SELECT") + split = self.layout.split(factor=0.9) + col_left = split.column(align=True) + row_left = col_left.row(align=True) + row_left.alignment = "CENTER" + if get_tab_visibility("PROJECT"): + row_left.operator( + "bim.set_tab", + text="", + emboss=aprops.tab == "PROJECT", + icon_value=bonsai.bim.icons[ifc_icon].icon_id, + ).tab = "PROJECT" + if get_tab_visibility("OBJECT"): + self.draw_tab_entry(row_left, "FILE_3D", "OBJECT", is_ifc_project, aprops.tab == "OBJECT") + if get_tab_visibility("GEOMETRY"): + self.draw_tab_entry(row_left, "MATERIAL", "GEOMETRY", is_ifc_project, aprops.tab == "GEOMETRY") + if get_tab_visibility("DRAWINGS"): + self.draw_tab_entry(row_left, "DOCUMENTS", "DRAWINGS", is_ifc_project, aprops.tab == "DRAWINGS") + if get_tab_visibility("SERVICES"): + self.draw_tab_entry(row_left, "NETWORK_DRIVE", "SERVICES", is_ifc_project, aprops.tab == "SERVICES") + if get_tab_visibility("STRUCTURE"): + self.draw_tab_entry(row_left, "EDITMODE_HLT", "STRUCTURE", is_ifc_project, aprops.tab == "STRUCTURE") + if get_tab_visibility("SCHEDULING"): + self.draw_tab_entry(row_left, "NLA", "SCHEDULING", is_ifc_project, aprops.tab == "SCHEDULING") + if get_tab_visibility("FM"): + 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") + ): + 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") + row_left = col_left.row(align=True) # Yes, that's right. - row = self.layout.row() - row.alignment = "CENTER" - row.scale_y = 0.2 - for tab in [ - "PROJECT", - "OBJECT", - "GEOMETRY", - "DRAWINGS", - "SERVICES", - "STRUCTURE", - "SCHEDULING", - "FM", - "QUALITY", - "SWITCH", - ]: + row_left.alignment = "CENTER" + row_left.scale_y = 0.2 + + 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 aprops.tab == tab: - row.prop(aprops, "active_tab", text="", icon="BLANK1") - else: - row.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False) + if get_tab_visibility(tab): + if aprops.tab == tab: + row_left.prop(aprops, "active_tab", text="", icon="BLANK1") + else: + row_left.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False) + row_left.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False) # space for Switch + col_right = split.column(align=True) + row_right = col_right.row(align=True) + row_right.alignment = "RIGHT" + + if addon_prefs.save_metadata_blend_file and addon_prefs.user_ui_customization: + row_right.operator("bim.manage_tab_visibility", icon="PREFERENCES", text="") row = self.layout.row(align=True) row.prop(aprops, "tab", text="") + if addon_prefs.save_metadata_blend_file and addon_prefs.user_ui_customization: + for tab in get_tab_names(): + if get_tab_visibility(tab): + if aprops.tab == tab: + row.operator("bim.manage_tab_panels", text="", icon="PREFERENCES").tab_name = tab + if bonsai.REINSTALLED_BBIM_VERSION: box = self.layout.box() box.alert = True @@ -1061,14 +1118,18 @@ class BIM_PT_tabs(Panel): class BIM_PT_tab_new_project_wizard(Panel): + bl_idname = "BIM_PT_tab_new_project_wizard" bl_label = "New Project Wizard" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "PROJECT" @classmethod def poll(cls, context): - if not tool.Blender.is_tab(context, "PROJECT"): + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if not tool.Blender.is_tab(context, cls.bim_tab_name): return False bim_props = tool.Blender.get_bim_props() pprops = tool.Project.get_project_props() @@ -1083,94 +1144,128 @@ class BIM_PT_tab_new_project_wizard(Panel): class BIM_PT_tab_project_info(Panel): + bl_idname = "BIM_PT_tab_project_info" bl_label = "Project Info" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "PROJECT" @classmethod def poll(cls, context): - if not tool.Blender.is_tab(context, "PROJECT"): + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): return False - bim_props = tool.Blender.get_bim_props() - pprops = tool.Project.get_project_props() - if pprops.is_loading: - return True - elif tool.Ifc.get() or bim_props.ifc_file: - return True - return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + bim_props = tool.Blender.get_bim_props() + pprops = tool.Project.get_project_props() + if pprops.is_loading: + return True + elif tool.Ifc.get() or bim_props.ifc_file: + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): - pass + layout = self.layout + layout.label(text="This is the Project Info panel.") class BIM_PT_tab_spatial(Panel): + bl_idname = "BIM_PT_tab_spatial" bl_label = "Spatial" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "PROJECT" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "PROJECT") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_project_setup(Panel): + bl_idname = "BIM_PT_tab_project_setup" bl_label = "Project Setup" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "PROJECT" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "PROJECT") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_stakeholders(Panel): + bl_idname = "BIM_PT_tab_stakeholders" bl_label = "Stakeholders" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bim_tab_name = "PROJECT" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "PROJECT") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_collaboration(Panel): + bl_idname = "BIM_PT_tab_collaboration" bl_label = "Collaboration" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "QUALITY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "QUALITY") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_grouping_and_filtering(Panel): + bl_idname = "BIM_PT_tab_grouping_and_filtering" bl_label = "Grouping and Filtering" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_options = {"HEADER_LAYOUT_EXPAND"} + bim_tab_name = "PROJECT" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "PROJECT") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass @@ -1185,197 +1280,281 @@ class BIM_PT_tab_grouping_and_filtering(Panel): class BIM_PT_tab_geometry(Panel): + bl_idname = "BIM_PT_tab_geometry" bl_label = "Geometry" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "PROJECT" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "PROJECT") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_status(Panel): + bl_idname = "BIM_PT_tab_status" bl_label = "Status" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SCHEDULING" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_qto(Panel): + bl_idname = "BIM_PT_tab_qto" bl_label = "Quantity Take-off" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SCHEDULING" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_resources(Panel): + bl_idname = "BIM_PT_tab_resources" bl_label = "Resources" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SCHEDULING" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_cost(Panel): + bl_idname = "BIM_PT_tab_cost" bl_label = "Cost" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SCHEDULING" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_sequence(Panel): + bl_idname = "BIM_PT_tab_sequence" bl_label = "Construction Scheduling" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SCHEDULING" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SCHEDULING") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_structural(Panel): + bl_idname = "BIM_PT_tab_structural" bl_label = "Structural" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "STRUCTURE" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "STRUCTURE") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_services(Panel): + bl_idname = "BIM_PT_tab_services" bl_label = "Services" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SERVICES" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SERVICES") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_lighting(Panel): + bl_idname = "BIM_PT_tab_lighting" bl_label = "Lighting" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SERVICES" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SERVICES") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_zones(Panel): + bl_idname = "BIM_PT_tab_zones" bl_label = "Zones" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SERVICES" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SERVICES") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_solar_analysis(Panel): + bl_idname = "BIM_PT_tab_solar_analysis" bl_label = "Solar Analysis" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "SERVICES" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "SERVICES") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_quality_control(Panel): + bl_idname = "BIM_PT_tab_quality_control" bl_label = "Quality Control" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "QUALITY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "QUALITY") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_clash_detection(Panel): + bl_idname = "BIM_PT_tab_clash_detection" bl_label = "Clash Detection" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" + bim_tab_name = "QUALITY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "QUALITY") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_sandbox(Panel): + bl_idname = "BIM_PT_tab_sandbox" bl_label = "Sandbox" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bim_tab_name = "QUALITY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "QUALITY") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): row = self.layout.row() @@ -1384,17 +1563,21 @@ class BIM_PT_tab_sandbox(Panel): # Object panel groups class BIM_PT_tab_object_metadata(Panel): + bl_idname = "BIM_PT_tab_object_metadata" bl_label = "Object" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "OBJECT" @classmethod def poll(cls, context): + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False props = tool.Project.get_project_props() - return ( - tool.Blender.is_tab(context, "OBJECT") + if ( + tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get() and (obj := context.active_object) # Hide links empty handles. @@ -1403,254 +1586,326 @@ class BIM_PT_tab_object_metadata(Panel): or not obj.instance_collection or not any(l.empty_handle == obj for l in props.links) ) - ) + ): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_placement(Panel): + bl_idname = "BIM_PT_tab_placement" bl_label = "Placement" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return ( - tool.Blender.is_tab(context, "GEOMETRY") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if ( + tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get() and (obj := context.active_object) and tool.Ifc.get_entity(obj) - ) + ): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_representations(Panel): + bl_idname = "BIM_PT_tab_representations" bl_label = "Representations" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return ( - tool.Blender.is_tab(context, "GEOMETRY") - and tool.Ifc.get() - and tool.Geometry.get_active_or_representation_obj() - ) + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_geometric_relationships(Panel): + bl_idname = "BIM_PT_tab_geometric_relationships" bl_label = "Geometric Relationships" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 bl_options = {"DEFAULT_CLOSED"} + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_parametric_geometry(Panel): + bl_idname = "BIM_PT_tab_parametric_geometry" bl_label = "Parametric Geometry" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 bl_options = {"DEFAULT_CLOSED"} + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return ( - tool.Blender.is_tab(context, "GEOMETRY") - and tool.Ifc.get() - and (obj := context.active_object) - and tool.Ifc.get_entity(obj) - ) + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_object_materials(Panel): + bl_idname = "BIM_PT_tab_object_materials" bl_label = "Object Materials" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return ( - tool.Blender.is_tab(context, "GEOMETRY") - and tool.Ifc.get() - and (obj := context.active_object) - and tool.Ifc.get_entity(obj) - ) + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_materials(Panel): + bl_idname = "BIM_PT_tab_materials" bl_label = "Materials" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_styles(Panel): + bl_idname = "BIM_PT_tab_styles" bl_label = "Styles" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_profiles(Panel): + bl_idname = "BIM_PT_tab_profiles" bl_label = "Profiles" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "GEOMETRY" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_sheets(Panel): + bl_idname = "BIM_PT_tab_sheets" bl_label = "Sheets" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "DRAWINGS" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_drawings(Panel): + bl_idname = "BIM_PT_tab_drawings" bl_label = "Drawings" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "DRAWINGS" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_schedules(Panel): + bl_idname = "BIM_PT_tab_schedules" bl_label = "Schedules" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "DRAWINGS" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_references(Panel): + bl_idname = "BIM_PT_tab_references" bl_label = "References" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "DRAWINGS" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_misc(Panel): - bl_label = "Misc." + bl_idname = "BIM_PT_tab_misc" + bl_label = "Misc" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 bl_options = {"DEFAULT_CLOSED"} + bim_tab_name = "OBJECT" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "OBJECT") and tool.Ifc.get() + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name) and tool.Ifc.get(): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_handover(Panel): - bl_label = "Commissioning and Handover" + bl_idname = "BIM_PT_tab_handover" + bl_label = "Handover" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 1 + bim_tab_name = "FM" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "FM") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass class BIM_PT_tab_operations(Panel): - bl_label = "Operations and Maintenance" + bl_idname = "BIM_PT_tab_operations" + bl_label = "Operations" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_order = 2 + bim_tab_name = "FM" @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "FM") + if not should_show_panel(cls.bl_idname, cls.bim_tab_name, context): + return False + if tool.Blender.is_tab(context, cls.bim_tab_name): + return True + return tool.Blender.is_tab(context, "BOOKMARK") def draw(self, context): pass diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 0f063b8473..f0455a4ba1 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -2123,7 +2123,15 @@ class Drawing(bonsai.core.tool.Drawing): value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) value = '"' + str(value).replace('"', '\\"') + '"' command = command.replace(variable, value) - text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2])) + # Defensive: skip if command[2:-2] is None or 'None' + command_content = command[2:-2] + if command_content is None or str(command_content).strip().lower() == 'none': + text = text.replace(original_command, "") + else: + try: + text = text.replace(original_command, ifcopenshell.util.selector.format(command_content)) + except Exception: + text = text.replace(original_command, "") for variable in re.findall("{{.*?}}", text): value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) if isinstance(value, (list, tuple)):