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):