From 52762f828cf065f3eab4374648e71c35a96f827d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 15 Jul 2025 16:35:15 +0500 Subject: [PATCH] Save all properties from Preferences UI as actual preferences Previously some of them were saved only for current .blend file, moving them to preferences will make it more consistent with usual Blender UX. If needed we'll be able to add some way to fine-grain them later. There's also a temporary patch that's going to migrate old .blend-props to new preferences-props to make process less disruptive. --- src/bonsai/bonsai/bim/__init__.py | 1 + src/bonsai/bonsai/bim/handler.py | 2 + src/bonsai/bonsai/bim/ifc.py | 10 +- src/bonsai/bonsai/bim/import_ifc.py | 3 +- src/bonsai/bonsai/bim/module/drawing/data.py | 4 +- .../bonsai/bim/module/drawing/decoration.py | 10 +- .../bonsai/bim/module/drawing/operator.py | 15 +- src/bonsai/bonsai/bim/module/drawing/prop.py | 50 ------ .../bonsai/bim/module/drawing/scheduler.py | 5 +- src/bonsai/bonsai/bim/module/material/data.py | 9 +- src/bonsai/bonsai/bim/module/model/prop.py | 10 -- .../bonsai/bim/module/search/operator.py | 4 +- src/bonsai/bonsai/bim/module/type/operator.py | 4 +- src/bonsai/bonsai/bim/operator.py | 17 +- src/bonsai/bonsai/bim/prop.py | 26 --- src/bonsai/bonsai/bim/schema.py | 4 + src/bonsai/bonsai/bim/ui.py | 158 ++++++++++++++++-- src/bonsai/bonsai/tool/blender.py | 103 +++++++++++- src/bonsai/bonsai/tool/debug.py | 4 +- src/bonsai/bonsai/tool/drawing.py | 30 ++-- src/bonsai/bonsai/tool/model.py | 14 +- src/bonsai/bonsai/tool/polyline.py | 4 +- src/bonsai/bonsai/tool/project.py | 2 +- src/bonsai/bonsai/tool/pset_template.py | 2 +- src/bonsai/test/tool/test_debug.py | 4 +- src/bonsai/test/tool/test_model.py | 27 +-- 26 files changed, 338 insertions(+), 184 deletions(-) diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 16975d08a6..19e07725ef 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -146,6 +146,7 @@ classes = [ ui.BIM_UL_clipping_plane, ui.BIM_UL_generic, ui.BIM_UL_topics, + ui.DocPreferences, ui.BIM_ADDON_preferences, # Tabs panel ui.BIM_PT_tabs, diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index ff7669dc91..abeab146e8 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -417,3 +417,5 @@ def load_post(scene): scene.tool_settings.use_snap = True # Match default Bonsai snaps scene.tool_settings.snap_elements_base = {"EDGE", "EDGE_PERPENDICULAR", "VERTEX", "EDGE_MIDPOINT", "FACE"} + + tool.Blender.sync_old_preferences() diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 886b59f068..fa988f9de0 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -126,11 +126,11 @@ class IfcStore: @staticmethod def get_cache() -> ifcopenshell.geom.serializers.hdf5 | None: if IfcStore.cache is None and IfcStore.path: - props = tool.Blender.get_bim_props() + prefs = tool.Blender.get_addon_preferences() ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() - os.makedirs(props.cache_dir, exist_ok=True) - IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5") + os.makedirs(prefs.cache_dir, exist_ok=True) + IfcStore.cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5") cache_path = Path(IfcStore.cache_path) cache_settings = ifcopenshell.geom.settings() serializer_settings = ifcopenshell.geom.serializer_settings() @@ -170,8 +170,8 @@ class IfcStore: assert IfcStore.file ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() - props = tool.Blender.get_bim_props() - new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5") + prefs = tool.Blender.get_addon_preferences() + new_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5") IfcStore.cache = None try: shutil.move(IfcStore.cache_path, new_cache_path) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 42e1f55621..706edc42a7 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -231,7 +231,8 @@ class IfcImporter: self.progress = 0 self.material_creator = MaterialCreator(ifc_import_settings, self) - classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe + prefs = tool.Blender.get_addon_preferences() + classes_to_wireframe_str = prefs.doc.classes_to_wireframe self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")] def profile_code(self, message: str) -> None: diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index 65633748ca..bd5adde0fb 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -86,8 +86,8 @@ class SheetsData: project = tool.Ifc.get().by_type("IfcProject")[0] titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") if not titleblocks_dir: - props = tool.Drawing.get_document_props() - titleblocks_dir = props.titleblocks_dir + prefs = tool.Blender.get_addon_preferences() + titleblocks_dir = prefs.doc.titleblocks_dir titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir) if os.path.exists(titleblocks_dir): files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")]) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index cb309211e9..d1dfce3403 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -426,8 +426,8 @@ class BaseDecorator: # font_size = 16 <-- this is a good default # TODO: need to synchronize it better with svg - props = tool.Drawing.get_document_props() - magic_font_scale = props.magic_font_scale + prefs = tool.Blender.get_addon_preferences() + magic_font_scale = prefs.doc.magic_font_scale font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5 pos = pos - line_no * font_size_px * rotation_matrix[1] @@ -1661,7 +1661,7 @@ class CutDecorator: all_vertex_i_offset = 0 selected_vertex_i_offset = 0 - classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut + classes_no_cut_str = self.addon_prefs.doc.classes_no_cut classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")] for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]: @@ -1959,8 +1959,8 @@ class DecorationsHandler: for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): self.decorators[object_type] = self.decorators["FALL"] self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"] - props = tool.Drawing.get_document_props() - if drawing_font := props.drawing_font: + prefs = tool.Blender.get_addon_preferences() + if drawing_font := prefs.doc.drawing_font: drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font) if drawing_font_path.is_file(): font_id = blf.load(drawing_font_path.__str__()) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 9346bbde78..670df8b436 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -874,6 +874,7 @@ class CreateDrawing(bpy.types.Operator): cached_linework -= edited_guids bim_props = tool.Blender.get_bim_props() + prefs = tool.Blender.get_addon_preferences() files = {bim_props.ifc_file: tool.Ifc.get()} props = tool.Project.get_project_props() @@ -890,7 +891,7 @@ class CreateDrawing(bpy.types.Operator): # Don't use draw.main() just whilst we're prototyping and experimenting # TODO: hash paths are never used ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest() - ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5") + ifc_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5") self.serialiser.setFile(ifc) drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc) @@ -1848,8 +1849,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): def poll(cls, context): props = tool.Drawing.get_document_props() # Won't be visible in UI anyway. - bim_props = tool.Blender.get_bim_props() - if not props.sheets or not bim_props.data_dir: + prefs = tool.Blender.get_addon_preferences() + if not props.sheets or not prefs.data_dir: return False if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") @@ -1954,8 +1955,8 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): if not tool.Drawing.get_active_sheet_item(is_sheet=True): cls.poll_message_set("No sheet selected.") return False - bim_props = tool.Blender.get_bim_props() - return props.sheets and bim_props.data_dir + prefs = tool.Blender.get_addon_preferences() + return props.sheets and prefs.data_dir def invoke(self, context, event): # opening all sheets on shift+click @@ -2741,8 +2742,8 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): if not props.schedules: cls.poll_message_set("No schedule selected.") return False - bim_props = tool.Blender.get_bim_props() - return props.schedules and props.sheets and bim_props.data_dir + prefs = tool.Blender.get_addon_preferences() + return props.schedules and props.sheets and prefs.data_dir def _execute(self, context): props = tool.Drawing.get_document_props() diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index b88b60c3a0..97857390ac 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -423,39 +423,6 @@ class DocProperties(PropertyGroup): active_sheet_index: IntProperty(name="Active Sheet Index") drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle) should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations) - sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory") - layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory") - titleblocks_dir: StringProperty( - default=os.path.join("layouts", "titleblocks") + os.path.sep, name="Default Titleblocks Directory" - ) - drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory") - stylesheet_path: StringProperty( - default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet" - ) - schedules_stylesheet_path: StringProperty( - default=os.path.join("drawings", "assets", "schedule.css"), name="Default Stylesheet for Schedules" - ) - markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers") - symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols") - patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns") - shadingstyles_path: StringProperty( - default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles" - ) - shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style") - drawing_font: StringProperty(default="OpenGost Type B TT.ttf", name="Drawing Font") - magic_font_scale: bpy.props.FloatProperty(default=0.004118616, name="Font Scale Factor") - imperial_precision: StringProperty(default="1/32", name="Imperial Precision") - tolerance: bpy.props.FloatProperty(default=0.00001, name="A tolerance used when selecting objects") - classes_to_wireframe: StringProperty( - default="IfcVirtualElement", - name="Classes to Wireframe", - description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace", - ) - classes_no_cut: StringProperty( - default="IfcVirtualElement, IfcSpace", - name="Classes that are not cut", - description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace", - ) if TYPE_CHECKING: should_use_underlay_cache: bool @@ -480,23 +447,6 @@ class DocProperties(PropertyGroup): active_sheet_index: int drawing_styles: bpy.types.bpy_prop_collection_idprop[DrawingStyle] should_draw_decorations: bool - sheets_dir: str - layouts_dir: str - titleblocks_dir: str - drawings_dir: str - stylesheet_path: str - schedules_stylesheet_path: str - markers_path: str - symbols_path: str - patterns_path: str - shadingstyles_path: str - shadingstyle_default: str - drawing_font: str - magic_font_scale: float - imperial_precision: str - tolerance: float - classes_to_wireframe: str - classes_no_cut: str def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]: drawing_id = self.active_drawing_id diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py index ce2b711071..6eefa35030 100644 --- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py +++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py @@ -74,10 +74,11 @@ class Scheduler: self.schedule_xlsx(infile, outfile) def parse_css(self, infile: str) -> None: - props = tool.Drawing.get_document_props() + prefs = tool.Blender.get_addon_preferences() + stylesheet_path = os.path.splitext(infile)[0] + ".css" if not os.path.exists(stylesheet_path): - stylesheet_rel_path = props.schedules_stylesheet_path + stylesheet_rel_path = prefs.doc.schedules_stylesheet_path ifc_file_path = os.path.dirname(tool.Ifc.get_path()) stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path if not os.path.exists(stylesheet_path): diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index ccd04e164d..1cb7cec9dc 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -293,9 +293,9 @@ class ObjectMaterialData: if item.is_a("IfcMaterialLayer"): total_thickness = item.LayerThickness unit_system = bpy.context.scene.unit_settings.system - props = tool.Drawing.get_document_props() + prefs = tool.Blender.get_addon_preferences() if unit_system == "IMPERIAL": - precision = props.imperial_precision + precision = prefs.doc.imperial_precision else: precision = None formatted_thickness = format_distance( @@ -330,11 +330,12 @@ class ObjectMaterialData: elif cls.material.is_a("IfcMaterialLayerSet"): layers = cls.material.MaterialLayers thickness = sum([l.LayerThickness for l in layers or []]) - props = tool.Drawing.get_document_props() + prefs = tool.Blender.get_addon_preferences() + assert bpy.context.scene unit_system = bpy.context.scene.unit_settings.system precision = None if unit_system == "IMPERIAL": - precision = props.imperial_precision + precision = prefs.doc.imperial_precision return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True) @classmethod diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 73eb8ea0f9..f4215fc214 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -191,14 +191,6 @@ class BIMModelProperties(PropertyGroup): menu_relating_type_id: bpy.props.IntProperty() icon_id: bpy.props.IntProperty() updating: bpy.props.BoolProperty(default=False) - occurrence_name_style: bpy.props.EnumProperty( - items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")], - name="Occurrence Name Style", - ) - occurrence_name_function: bpy.props.StringProperty( - name="Occurrence Name Function", - description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style", - ) getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id} extrusion_depth: bpy.props.FloatProperty(name="Extrusion Depth", min=0.001, default=42.0, subtype="DISTANCE") cardinal_point: bpy.props.EnumProperty( @@ -305,8 +297,6 @@ class BIMModelProperties(PropertyGroup): menu_relating_type_id: int icon_id: int updating: bool - occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"] - occurrence_name_function: str extrusion_depth: float cardinal_point: Literal[ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19" diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index ec985a1e4d..605fa7857e 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -815,8 +815,8 @@ class SelectSimilar(Operator, tool.Ifc.Operator): def execute(self, context): self.calculated_sum = 0 # reset if run before key = "predefined_type" if self.key == "PredefinedType" else self.key - dprops = tool.Drawing.get_document_props() - tolerance = dprops.tolerance + prefs = tool.Blender.get_addon_preferences() + tolerance = prefs.doc.tolerance formatted_tolerance = f"{tolerance:.{max(0, -int(f'{tolerance:.1e}'.split('e')[-1])) if tolerance < 1 else 1}f}" if self.calculate_sum: diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index f8c5c1ae6b..b34d2e3078 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -57,13 +57,13 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator): related_objects = [bpy.data.objects[self.related_object]] else: related_objects = tool.Blender.get_selected_objects() - model_props = tool.Model.get_model_props() + prefs = tool.Blender.get_addon_preferences() for obj in related_objects: element = tool.Ifc.get_entity(obj) if not element or not element.is_a("IfcObject"): continue core.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) - if model_props.occurrence_name_style == "TYPE": + if prefs.occurrence_name_style == "TYPE": obj.name = tool.Model.generate_occurrence_name(relating_type, element.is_a()) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 301431a146..85de0534ee 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -235,19 +235,12 @@ class SelectDir(bpy.types.Operator, ImportHelper): bl_description = "Open a file browser to choose the directory" data_path: bpy.props.StringProperty(name="Data Path") + if TYPE_CHECKING: + data_path: str + def execute(self, context): - crumbs = self.data_path.split(".") - if crumbs[0] == "preferences": - crumbs.pop(0) - data = tool.Blender.get_addon_preferences() - else: - data = context - while crumbs: - crumb = crumbs.pop(0) - if crumbs: - data = getattr(data, crumb) - else: - setattr(data, crumb, os.path.dirname(self.filepath)) + data, attr = tool.Blender.resolve_data_path_to_data_attr(self.data_path) + setattr(data, attr, os.path.dirname(self.filepath)) return {"FINISHED"} def invoke(self, context, event): diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index d2aea11142..7c0ea1518a 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -19,7 +19,6 @@ import os import bpy import json -import platformdirs import ifcopenshell import ifcopenshell.util.pset import ifcopenshell.util.unit @@ -128,20 +127,6 @@ def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None bonsai.bim.schema.ifc.schema_dir = bim_props.schema_dir -def update_data_dir(self: "BIMProperties", context: bpy.types.Context) -> None: - import bonsai.bim.schema - - bim_props = tool.Blender.get_bim_props() - bonsai.bim.schema.ifc.data_dir = bim_props.data_dir - - -def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None: - import bonsai.bim.schema - - bim_props = tool.Blender.get_bim_props() - bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir - - def update_section_color(self: "BIMProperties", context: bpy.types.Context) -> None: section_node_group = bpy.data.node_groups.get("Section Override") if section_node_group is None: @@ -521,16 +506,7 @@ class BIMProperties(PropertyGroup): schema_dir: StringProperty( default=os.path.join(cwd, "schema") + os.path.sep, name="Schema Directory", update=update_schema_dir ) - data_dir: StringProperty( - default=(platformdirs.user_data_path("bonsai", roaming=True, ensure_exists=True) / "data").__str__(), - name="Data Directory", - update=update_data_dir, - ) - cache_dir: StringProperty( - default=platformdirs.user_cache_dir("bonsai"), name="Cache Directory", update=update_cache_dir - ) has_blend_warning: BoolProperty(name="Has Blend Warning", default=False) - pset_dir: StringProperty(default=os.path.join("psets") + os.path.sep, name="Default Psets Directory") ifc_file: StringProperty(name="IFC File") last_transaction: StringProperty(name="Last Transaction") should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False) @@ -586,8 +562,6 @@ class BIMProperties(PropertyGroup): if TYPE_CHECKING: is_dirty: bool schema_dir: str - data_dir: str - cache_dir: str has_blend_warning: bool pset_dir: str ifc_file: str diff --git a/src/bonsai/bonsai/bim/schema.py b/src/bonsai/bonsai/bim/schema.py index cae1e32146..6cfa5e2630 100644 --- a/src/bonsai/bonsai/bim/schema.py +++ b/src/bonsai/bonsai/bim/schema.py @@ -24,6 +24,10 @@ import bpy_restrict_state class IfcSchema: + data_dir: str + cache_dir: str + schema_dir: str + def __init__(self, schema_identifier="IFC4"): if schema_identifier not in ("IFC2X3", "IFC4", "IFC4X3_ADD2"): schema_identifier = "IFC4" diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 1532506748..39c610f72d 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -19,6 +19,7 @@ import os import bpy import platform +import platformdirs import bonsai.bim.helper from pathlib import Path from bpy.types import Panel @@ -35,7 +36,7 @@ import bonsai.bim import bonsai.tool as tool from ifcopenshell.util.file import IfcHeaderExtractor from bonsai.bim.prop import Attribute -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Literal from natsort import natsorted @@ -219,6 +220,98 @@ class BIM_UL_topics(bpy.types.UIList): layout.label(text="", translate=False) +class DocPreferences(bpy.types.PropertyGroup): + sheets_dir: StringProperty( + default=os.path.join("sheets") + os.path.sep, + name="Default Sheets Directory", + ) + layouts_dir: StringProperty( + default=os.path.join("layouts") + os.path.sep, + name="Default Layouts Directory", + ) + titleblocks_dir: StringProperty( + default=os.path.join("layouts", "titleblocks") + os.path.sep, + name="Default Titleblocks Directory", + ) + drawings_dir: StringProperty( + default=os.path.join("drawings") + os.path.sep, + name="Default Drawings Directory", + ) + stylesheet_path: StringProperty( + default=os.path.join("drawings", "assets", "default.css"), + name="Default Stylesheet", + ) + schedules_stylesheet_path: StringProperty( + default=os.path.join("drawings", "assets", "schedule.css"), + name="Default Stylesheet for Schedules", + ) + markers_path: StringProperty( + default=os.path.join("drawings", "assets", "markers.svg"), + name="Default Markers", + ) + symbols_path: StringProperty( + default=os.path.join("drawings", "assets", "symbols.svg"), + name="Default Symbols", + ) + patterns_path: StringProperty( + default=os.path.join("drawings", "assets", "patterns.svg"), + name="Default Patterns", + ) + shadingstyles_path: StringProperty( + default=os.path.join("drawings", "assets", "shading_styles.json"), + name="Default Shading Styles", + ) + shadingstyle_default: StringProperty( + default="Blender Default", + name="Default Shading Style", + ) + drawing_font: StringProperty( + default="OpenGost Type B TT.ttf", + name="Drawing Font", + ) + magic_font_scale: bpy.props.FloatProperty( + default=0.004118616, + name="Font Scale Factor", + ) + imperial_precision: StringProperty( + default="1/32", + name="Imperial Precision", + ) + tolerance: bpy.props.FloatProperty( + default=0.00001, + name="A tolerance used when selecting objects", + ) + classes_to_wireframe: StringProperty( + default="IfcVirtualElement", + name="Classes to Wireframe", + description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace", + ) + classes_no_cut: StringProperty( + default="IfcVirtualElement, IfcSpace", + name="Classes that are not cut", + description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace", + ) + + if TYPE_CHECKING: + sheets_dir: str + layouts_dir: str + titleblocks_dir: str + drawings_dir: str + stylesheet_path: str + schedules_stylesheet_path: str + markers_path: str + symbols_path: str + patterns_path: str + shadingstyles_path: str + shadingstyle_default: str + drawing_font: str + magic_font_scale: float + imperial_precision: str + tolerance: float + classes_to_wireframe: str + classes_no_cut: str + + class BIM_ADDON_preferences(bpy.types.AddonPreferences): bl_idname = tool.Blender.get_blender_addon_package_name() svg2pdf_command: StringProperty( @@ -347,6 +440,39 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False ) should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False) + occurrence_name_style: bpy.props.EnumProperty( + items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")], + name="Occurrence Name Style", + ) + occurrence_name_function: bpy.props.StringProperty( + name="Occurrence Name Function", + description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style", + ) + + def update_data_dir(self, context: bpy.types.Context) -> None: + import bonsai.bim.schema + + bonsai.bim.schema.ifc.data_dir = self.data_dir + + def update_cache_dir(self, context: bpy.types.Context) -> None: + import bonsai.bim.schema + + bonsai.bim.schema.ifc.cache_dir = self.cache_dir + + data_dir: StringProperty( + default=(platformdirs.user_data_path("bonsai", roaming=True, ensure_exists=True) / "data").__str__(), + name="Data Directory", + update=update_data_dir, + ) + cache_dir: StringProperty( + default=platformdirs.user_cache_dir("bonsai"), name="Cache Directory", update=update_cache_dir + ) + + pset_dir: StringProperty( + default=os.path.join("psets") + os.path.sep, + name="Default Psets Directory", + ) + doc: bpy.props.PointerProperty(type=DocPreferences) if TYPE_CHECKING: svg2pdf_command: str @@ -374,6 +500,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bsdd_load_test_dictionaries: bool should_disable_undo_on_save: bool should_stream: bool + occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"] + occurrence_name_function: str + data_dir: str + cache_dir: str + pset_dir: str + doc: DocPreferences def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -414,32 +546,26 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): layout.prop(self, "spatial_elements_unselectable") def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: - # TODO: move props to preferences. - props = tool.Model.get_model_props() - layout.prop(props, "occurrence_name_style") - if props.occurrence_name_style == "CUSTOM": - layout.prop(props, "occurrence_name_function") + layout.prop(self, "occurrence_name_style") + if self.occurrence_name_style == "CUSTOM": + layout.prop(self, "occurrence_name_function") def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: - # TODO: move props to preferences. - props = tool.Blender.get_bim_props() row = layout.row(align=True) - row.prop(props, "data_dir") - row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir" + row.prop(self, "data_dir") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.data_dir" row = layout.row(align=True) - row.prop(props, "cache_dir") - row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir" + row.prop(self, "cache_dir") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.cache_dir" row = layout.row(align=True) row.prop(self, "tmp_dir") row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir" def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: - # TODO: move props to preferences. - props = tool.Blender.get_bim_props() - layout.prop(props, "pset_dir") - dprops = tool.Drawing.get_document_props() + layout.prop(self, "pset_dir") + dprops = self.doc layout.prop(dprops, "sheets_dir") layout.prop(dprops, "layouts_dir") layout.prop(dprops, "titleblocks_dir") diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 18dbb6db55..8404b3c4c9 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1550,6 +1550,51 @@ class Blender(bonsai.core.tool.Blender): return repr(bpy_prop) return repr(bpy_struct) + @classmethod + def resolve_data_path_to_data_attr(cls, data_path: str) -> tuple[bpy.types.bpy_struct, str]: + """ + :param data_path: Non-full data path to attribute. + Examples: + - `preferences.prop_group.string_prop` (`preferences` would mean addon preferences) + - `scene.string_prop` (`scene` can be any member of `Context`) + + :return: Resolved tuple of Blender Struct and property name. + Examples: + - `(preferences.prop_group, "string_prop)` + - `(scene, "string_prop)` + + """ + # Get data to modify. + base_path, _, data_path_ = data_path.partition(".") + if base_path == "preferences": + data = tool.Blender.get_addon_preferences() + data_path = data_path_ + else: + data = bpy.context + + # Get property group if available. + base_path, _, attr = data_path.rpartition(".") + if base_path: + data = data.path_resolve(base_path) + return data, attr + + @classmethod + @contextlib.contextmanager + def preserve_prop_value(cls, bpy_object: bpy.types.bpy_struct, prop_name: str): + if bpy_object.is_property_set(prop_name): + prop_value = getattr(bpy_object, prop_name) + else: + prop_value = ... + try: + yield + except: + raise + finally: + if prop_value is ...: + bpy_object.property_unset(prop_name) + return + setattr(bpy_object, prop_name, prop_value) + @classmethod def set_prop_from_path(cls, bpy_object: bpy.types.bpy_struct, prop_path: str, value: Any) -> None: """Set `data_block` property value using path from `path_from_id`.""" @@ -1618,7 +1663,7 @@ class Blender(bonsai.core.tool.Blender): @classmethod def get_user_data_dir(cls) -> Path: - props = cls.get_bim_props() + props = cls.get_addon_preferences() return Path(props.data_dir) @classmethod @@ -1865,3 +1910,59 @@ class Blender(bonsai.core.tool.Blender): # Remove file if crash didn't happened. path.unlink() + + @classmethod + def sync_old_preferences(cls) -> None: + # Added on 25.07.15. + # TODO: deprecate later. + settings_remap = { + "scene.BIMBSDDProperties.load_preview_dictionaries": "preferences.bsdd_load_preview_dictionaries", + "scene.BIMBSDDProperties.load_inactive_dictionaries": "preferences.bsdd_load_inactive_dictionaries", + "scene.BIMBSDDProperties.load_test_dictionaries": "preferences.bsdd_load_test_dictionaries", + "scene.BIMProjectProperties.should_disable_undo_on_save": "preferences.should_disable_undo_on_save", + "scene.BIMProjectProperties.should_stream": "preferences.should_stream", + "scene.BIMModelProperties.occurrence_name_style": "preferences.occurrence_name_style", + "scene.BIMModelProperties.occurrence_name_function": "preferences.occurrence_name_function", + "scene.BIMProperties.pset_dir": "preferences.pset_dir", + "scene.BIMProperties.data_dir": "preferences.data_dir", + "scene.BIMProperties.cache_dir": "preferences.cache_dir", + "scene.DocProperties.sheets_dir": "preferences.doc.sheets_dir", + "scene.DocProperties.layouts_dir": "preferences.doc.layouts_dir", + "scene.DocProperties.titleblocks_dir": "preferences.doc.titleblocks_dir", + "scene.DocProperties.drawings_dir": "preferences.doc.drawings_dir", + "scene.DocProperties.stylesheet_path": "preferences.doc.stylesheet_path", + "scene.DocProperties.schedules_stylesheet_path": "preferences.doc.schedules_stylesheet_path", + "scene.DocProperties.markers_path": "preferences.doc.markers_path", + "scene.DocProperties.symbols_path": "preferences.doc.symbols_path", + "scene.DocProperties.patterns_path": "preferences.doc.patterns_path", + "scene.DocProperties.shadingstyles_path": "preferences.doc.shadingstyles_path", + "scene.DocProperties.shadingstyle_default": "preferences.doc.shadingstyle_default", + "scene.DocProperties.drawing_font": "preferences.doc.drawing_font", + "scene.DocProperties.magic_font_scale": "preferences.doc.magic_font_scale", + "scene.DocProperties.imperial_precision": "preferences.doc.imperial_precision", + "scene.DocProperties.tolerance": "preferences.doc.tolerance", + "scene.DocProperties.classes_to_wireframe": "preferences.doc.classes_to_wireframe", + "scene.DocProperties.classes_no_cut": "preferences.doc.classes_no_cut", + } + + props_updated = False + for old_path, path in settings_remap.items(): + data, attr = cls.resolve_data_path_to_data_attr(path) + # User already overridden the value. + if data.is_property_set(attr): + continue + + data_old, attr_old = cls.resolve_data_path_to_data_attr(old_path) + # User was only using default value previously. + if attr_old not in data_old: + continue + + old_value = data_old[attr_old] + print(f"Updating {path} based on previous value from {old_path} - '{old_value}'.") + setattr(data, attr, old_value) + props_updated = True + + # Doesn't seem to save on exit if edited from Python API, so we do it manually. + assert bpy.context.preferences + if props_updated and bpy.context.preferences.use_preferences_save: + bpy.ops.wm.save_userpref() diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 7b25c33401..079892a9b5 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -57,8 +57,8 @@ class Debug(bonsai.core.tool.Debug): @classmethod def purge_hdf5_cache(cls) -> None: - props = tool.Blender.get_bim_props() - cache_dir = props.cache_dir + prefs = tool.Blender.get_addon_preferences() + cache_dir = prefs.cache_dir filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")] for f in filelist: try: diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index abc3b5b32e..cf4f764b2e 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1314,34 +1314,37 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_default_layout_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] - props = tool.Drawing.get_document_props() + prefs = tool.Blender.get_addon_preferences() layouts_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") or props.layouts_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") or prefs.doc.layouts_dir ) return os.path.join(layouts_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod def get_default_sheet_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] - props = tool.Drawing.get_document_props() - sheets_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") or props.sheets_dir + prefs = tool.Blender.get_addon_preferences() + sheets_dir = ( + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") or prefs.doc.sheets_dir + ) return os.path.join(sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod def get_default_titleblock_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] - props = tool.Drawing.get_document_props() + prefs = tool.Blender.get_addon_preferences() titleblocks_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") or props.titleblocks_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") + or prefs.doc.titleblocks_dir ) return os.path.join(titleblocks_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @classmethod def get_default_drawing_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] - props = tool.Drawing.get_document_props() + prefs = tool.Blender.get_addon_preferences() drawings_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") or props.drawings_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") or prefs.doc.drawings_dir ) return os.path.join(drawings_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @@ -1353,19 +1356,20 @@ class Drawing(bonsai.core.tool.Drawing): RESOURCE_TYPES = ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles") @classmethod - def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]: + def get_default_drawing_resource_path(cls, resource: ResourceType) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] - props = tool.Drawing.get_document_props() + doc_prefs = tool.Blender.get_addon_preferences().doc resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr( - props, f"{resource.lower()}_path" + doc_prefs, f"{resource.lower()}_path" ) if resource_path: + assert isinstance(resource_path, str) return resource_path.replace("\\", "/") @classmethod def get_default_shading_style(cls) -> str: - dprops = tool.Drawing.get_document_props() - return dprops.shadingstyle_default + prefs = tool.Blender.get_addon_preferences() + return prefs.doc.shadingstyle_default @classmethod def setup_shading_styles_path(cls, resource_path: str) -> None: diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 127b137769..f1c9487cf8 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -49,7 +49,7 @@ from bonsai.bim import import_ifc from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData from bonsai.bim.module.model.opening import FilledOpeningGenerator from ifcopenshell.util.shape_builder import ShapeBuilder, np_to_3d -from typing import Optional, Union, TypeVar, Any, Literal, TYPE_CHECKING, TypedDict +from typing import Optional, Union, TypeVar, Any, Literal, TYPE_CHECKING, TypedDict, assert_never from collections.abc import Iterable, Sequence T = TypeVar("T") @@ -275,17 +275,19 @@ class Model(bonsai.core.tool.Model): @classmethod def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str) -> str: - props = cls.get_model_props() - if props.occurrence_name_style == "CLASS": + prefs = tool.Blender.get_addon_preferences() + if prefs.occurrence_name_style == "CLASS": return ifc_class[3:] - elif props.occurrence_name_style == "TYPE": + elif prefs.occurrence_name_style == "TYPE": return element_type.Name or "Unnamed" - elif props.occurrence_name_style == "CUSTOM": + elif prefs.occurrence_name_style == "CUSTOM": try: # Power users gonna power - return eval(props.occurrence_name_function) or "Instance" + return eval(prefs.occurrence_name_function) or "Instance" except: return "Instance" + else: + assert_never(prefs.occurrence_name_style) @classmethod def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index cc869f2459..ec02cd2c3c 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -474,8 +474,8 @@ class Polyline(bonsai.core.tool.Polyline): else: unit_scale = tool.Blender.get_unit_scale() if bpy.context.scene.unit_settings.system == "IMPERIAL": - dprops = tool.Drawing.get_document_props() - precision = dprops.imperial_precision + prefs = tool.Blender.get_addon_preferences() + precision = prefs.doc.imperial_precision if is_area: unit_scale = 1 else: diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 4fb0f507e7..2137e39141 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -69,7 +69,7 @@ class Project(bonsai.core.tool.Project): @classmethod def load_pset_templates(cls) -> None: - props = tool.Blender.get_bim_props() + props = tool.Blender.get_addon_preferences() pset_dir = tool.Ifc.resolve_uri(props.pset_dir) if os.path.isdir(pset_dir): for path in Path(pset_dir).glob("*.ifc"): diff --git a/src/bonsai/bonsai/tool/pset_template.py b/src/bonsai/bonsai/tool/pset_template.py index 54d35790ab..3da6dad2e5 100644 --- a/src/bonsai/bonsai/tool/pset_template.py +++ b/src/bonsai/bonsai/tool/pset_template.py @@ -133,7 +133,7 @@ class PsetTemplate(bonsai.core.tool.PsetTemplate): for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"): paths.append((f, "Global Pset Template")) - props = tool.Blender.get_bim_props() + props = tool.Blender.get_addon_preferences() pset_dir = Path(tool.Ifc.resolve_uri(props.pset_dir)) if pset_dir.is_dir(): for path in Path(pset_dir).glob("*.ifc"): diff --git a/src/bonsai/test/tool/test_debug.py b/src/bonsai/test/tool/test_debug.py index 4ac57f2567..24e875071b 100644 --- a/src/bonsai/test/tool/test_debug.py +++ b/src/bonsai/test/tool/test_debug.py @@ -55,8 +55,8 @@ class TestLoadExpress(NewFile): class TestPurgeHdf5Cache(NewFile): def test_run(self): - props = tool.Blender.get_bim_props() - cache_dir = Path(props.cache_dir) + prefs = tool.Blender.get_addon_preferences() + cache_dir = Path(prefs.cache_dir) test_file = cache_dir / "test.h5" test_file.parent.mkdir(parents=True, exist_ok=True) test_file.touch() diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index 670c0cfc37..35d5c6bc57 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -44,26 +44,29 @@ class TestGenerateOccurrenceName(NewFile): def test_generating_based_on_class(self): ifc = ifcopenshell.file() element_type = ifc.createIfcWallType(Name="Foobar") - props = tool.Model.get_model_props() - props.occurrence_name_style = "CLASS" - assert subject.generate_occurrence_name(element_type, "IfcWall") == "Wall" + prefs = tool.Blender.get_addon_preferences() + with tool.Blender.preserve_prop_value(prefs, "occurrence_name_style"): + prefs.occurrence_name_style = "CLASS" + assert subject.generate_occurrence_name(element_type, "IfcWall") == "Wall" def test_generating_based_on_type_name(self): ifc = ifcopenshell.file() element_type = ifc.createIfcWallType() - props = tool.Model.get_model_props() - props.occurrence_name_style = "TYPE" - assert subject.generate_occurrence_name(element_type, "IfcWall") == "Unnamed" - element_type.Name = "Foobar" - assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar" + prefs = tool.Blender.get_addon_preferences() + with tool.Blender.preserve_prop_value(prefs, "occurrence_name_style"): + prefs.occurrence_name_style = "TYPE" + assert subject.generate_occurrence_name(element_type, "IfcWall") == "Unnamed" + element_type.Name = "Foobar" + assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar" def test_generating_based_on_a_custom_function(self): ifc = ifcopenshell.file() element_type = ifc.createIfcWallType() - props = tool.Model.get_model_props() - props.occurrence_name_style = "CUSTOM" - props.occurrence_name_function = '"Foobar"' - assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar" + prefs = tool.Blender.get_addon_preferences() + with tool.Blender.preserve_prop_value(prefs, "occurrence_name_style"): + prefs.occurrence_name_style = "CUSTOM" + prefs.occurrence_name_function = '"Foobar"' + assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar" class TestGetBooleans(NewFile):