mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 14:41:25 +00:00
Merge pull request #6638 from falken10vdl/USER_UI_CUSTOMIZATION
User UI customization This can be enabled in Extras at user discretion.
This commit is contained in:
@@ -115,11 +115,13 @@ classes = [
|
|||||||
operator.EditBlenderCollection,
|
operator.EditBlenderCollection,
|
||||||
operator.FileAssociate,
|
operator.FileAssociate,
|
||||||
operator.FileUnassociate,
|
operator.FileUnassociate,
|
||||||
|
operator.LoadBlendMetadataAndIFC,
|
||||||
operator.OpenPath,
|
operator.OpenPath,
|
||||||
operator.OpenUpstream,
|
operator.OpenUpstream,
|
||||||
operator.OpenUri,
|
operator.OpenUri,
|
||||||
operator.ReloadIfcFile,
|
operator.ReloadIfcFile,
|
||||||
operator.RevertClippingPlaneCut,
|
operator.RevertClippingPlaneCut,
|
||||||
|
operator.SaveBlendMetadataFile,
|
||||||
operator.SelectDir,
|
operator.SelectDir,
|
||||||
operator.SelectIfcFile,
|
operator.SelectIfcFile,
|
||||||
operator.SelectURIAttribute,
|
operator.SelectURIAttribute,
|
||||||
@@ -129,12 +131,21 @@ classes = [
|
|||||||
prop.StrProperty,
|
prop.StrProperty,
|
||||||
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
||||||
operator.BIM_OT_attribute_search_values,
|
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.ObjProperty,
|
||||||
prop.MultipleFileSelect,
|
prop.MultipleFileSelect,
|
||||||
prop.Attribute,
|
prop.Attribute,
|
||||||
prop.ISODuration,
|
prop.ISODuration,
|
||||||
prop.BIMAreaProperties,
|
prop.BIMAreaProperties,
|
||||||
prop.BIMTabProperties,
|
prop.BIMTabProperties,
|
||||||
|
prop.BIMTabVisibility, # Must be registered before BIMProperties
|
||||||
|
prop.BIMPanelProperties, # Must be registered before BIMProperties
|
||||||
prop.BIMProperties,
|
prop.BIMProperties,
|
||||||
prop.IfcParameter,
|
prop.IfcParameter,
|
||||||
prop.PsetQto,
|
prop.PsetQto,
|
||||||
@@ -272,6 +283,7 @@ def register():
|
|||||||
bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||||
bpy.types.Camera.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)
|
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||||
|
|
||||||
if hasattr(bpy.types, "UI_MT_button_context_menu"):
|
if hasattr(bpy.types, "UI_MT_button_context_menu"):
|
||||||
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
|
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
|
||||||
bpy.types.STATUSBAR_HT_header.append(ui.draw_statusbar)
|
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.
|
# 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.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():
|
def unregister():
|
||||||
global icons
|
global icons
|
||||||
@@ -348,3 +364,7 @@ def unregister():
|
|||||||
tool.Blender.remove_scene_panel_override(panel)
|
tool.Blender.remove_scene_panel_override(panel)
|
||||||
|
|
||||||
bpy.app.translations.unregister("bonsai")
|
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
|
||||||
|
|||||||
@@ -539,7 +539,9 @@ def draw_filter(
|
|||||||
if preferences.chain_filter_with_set_operations:
|
if preferences.chain_filter_with_set_operations:
|
||||||
show_mode_toggle = j > 0
|
show_mode_toggle = j > 0
|
||||||
else:
|
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:
|
if show_mode_toggle:
|
||||||
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
|
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
|
||||||
op = row.operator(
|
op = row.operator(
|
||||||
@@ -758,7 +760,9 @@ def draw_filter(
|
|||||||
if preferences.chain_filter_with_set_operations:
|
if preferences.chain_filter_with_set_operations:
|
||||||
show_mode_toggle = j > 0
|
show_mode_toggle = j > 0
|
||||||
else:
|
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:
|
if show_mode_toggle:
|
||||||
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
|
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
|
||||||
op = row.operator(
|
op = row.operator(
|
||||||
@@ -784,3 +788,149 @@ def draw_filter(
|
|||||||
op.group_index = i
|
op.group_index = i
|
||||||
op.index = j
|
op.index = j
|
||||||
op.module = module
|
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
|
||||||
|
|||||||
@@ -2093,5 +2093,6 @@ class DecorationsHandler:
|
|||||||
if not DecoratorData.is_loaded:
|
if not DecoratorData.is_loaded:
|
||||||
DecoratorData.load(self)
|
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)
|
decorator.decorate(context, obj)
|
||||||
|
|||||||
@@ -1056,6 +1056,20 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
return tooltip
|
return tooltip
|
||||||
|
|
||||||
def execute(self, context):
|
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
|
@persistent
|
||||||
def load_handler(*args):
|
def load_handler(*args):
|
||||||
bpy.app.handlers.load_post.remove(load_handler)
|
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()
|
bim_props = tool.Blender.get_bim_props()
|
||||||
if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"):
|
if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"):
|
||||||
tool.Ifc.set_path(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)
|
|
||||||
bim_props.is_dirty = False
|
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()
|
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
|
@classmethod
|
||||||
def description(cls, context, properties):
|
def description(cls, context, properties):
|
||||||
|
|||||||
@@ -331,6 +331,13 @@ class BIM_PT_project(Panel):
|
|||||||
col.prop(props, "ifc_file", text="")
|
col.prop(props, "ifc_file", text="")
|
||||||
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", 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):
|
class BIM_PT_new_project_wizard(Panel):
|
||||||
bl_label = "New Project Wizard"
|
bl_label = "New Project Wizard"
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ import bonsai.bim
|
|||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
import bonsai.bim.handler
|
import bonsai.bim.handler
|
||||||
from enum import Enum
|
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 bpy_extras.io_utils import ImportHelper
|
||||||
from bonsai.bim import import_ifc
|
from bonsai.bim import import_ifc
|
||||||
from bonsai.bim.prop import StrProperty
|
from bonsai.bim.prop import StrProperty
|
||||||
@@ -236,6 +245,150 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
return ImportHelper.invoke(self, context, event)
|
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.
|
# TODO: Unused operator.
|
||||||
# Is there a need for this or 'DIR_PATH' propety subtype does almost the same,
|
# Is there a need for this or 'DIR_PATH' propety subtype does almost the same,
|
||||||
# but also has alt+click?
|
# but also has alt+click?
|
||||||
@@ -1605,3 +1758,244 @@ class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
|
|||||||
attr.is_null = True
|
attr.is_null = True
|
||||||
|
|
||||||
return {"FINISHED"}
|
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"}
|
||||||
|
|||||||
@@ -493,8 +493,9 @@ def get_tab(
|
|||||||
("SCHEDULING", "Costing and Scheduling", "", "NLA", 6),
|
("SCHEDULING", "Costing and Scheduling", "", "NLA", 6),
|
||||||
("FM", "Facility Management", "", "PACKAGE", 7),
|
("FM", "Facility Management", "", "PACKAGE", 7),
|
||||||
("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8),
|
("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8),
|
||||||
|
("BOOKMARK", "Bookmark", "", "SOLO_ON", 9),
|
||||||
None,
|
None,
|
||||||
("BLENDER", "Blender Properties", "", "BLENDER", 9),
|
("BLENDER", "Blender Properties", "", "BLENDER", 10),
|
||||||
]
|
]
|
||||||
return get_tab.enum_items
|
return get_tab.enum_items
|
||||||
|
|
||||||
@@ -529,6 +530,26 @@ class BIMTabProperties(PropertyGroup):
|
|||||||
inactive_tab: bool
|
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):
|
class BIMProperties(PropertyGroup):
|
||||||
is_dirty: BoolProperty(name="Is Dirty", default=False)
|
is_dirty: BoolProperty(name="Is Dirty", default=False)
|
||||||
schema_dir: StringProperty(
|
schema_dir: StringProperty(
|
||||||
@@ -613,6 +634,9 @@ class BIMProperties(PropertyGroup):
|
|||||||
name="Time Unit",
|
name="Time Unit",
|
||||||
default="HOUR",
|
default="HOUR",
|
||||||
)
|
)
|
||||||
|
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
|
||||||
|
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
is_dirty: bool
|
is_dirty: bool
|
||||||
schema_dir: str
|
schema_dir: str
|
||||||
@@ -627,6 +651,8 @@ class BIMProperties(PropertyGroup):
|
|||||||
volume_unit: str
|
volume_unit: str
|
||||||
mass_unit: str
|
mass_unit: str
|
||||||
time_unit: str
|
time_unit: str
|
||||||
|
tab_visibilities: bpy.types.bpy_prop_collection[BIMTabVisibility]
|
||||||
|
panel_properties: bpy.types.bpy_prop_collection[BIMPanelProperties]
|
||||||
|
|
||||||
|
|
||||||
class IfcParameter(PropertyGroup):
|
class IfcParameter(PropertyGroup):
|
||||||
|
|||||||
+360
-105
File diff suppressed because it is too large
Load Diff
@@ -2123,7 +2123,15 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
||||||
value = '"' + str(value).replace('"', '\\"') + '"'
|
value = '"' + str(value).replace('"', '\\"') + '"'
|
||||||
command = command.replace(variable, value)
|
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):
|
for variable in re.findall("{{.*?}}", text):
|
||||||
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
||||||
if isinstance(value, (list, tuple)):
|
if isinstance(value, (list, tuple)):
|
||||||
|
|||||||
Reference in New Issue
Block a user