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.
This commit is contained in:
Andrej730
2025-07-15 16:35:15 +05:00
parent 1d0868eb18
commit 52762f828c
26 changed files with 338 additions and 184 deletions
+1
View File
@@ -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,
+2
View File
@@ -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()
+5 -5
View File
@@ -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)
+2 -1
View File
@@ -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:
+2 -2
View File
@@ -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")])
@@ -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__())
@@ -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()
@@ -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
@@ -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):
@@ -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
@@ -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"
@@ -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:
@@ -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())
+5 -12
View File
@@ -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):
-26
View File
@@ -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
+4
View File
@@ -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"
+142 -16
View File
@@ -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")
+102 -1
View File
@@ -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()
+2 -2
View File
@@ -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:
+17 -13
View File
@@ -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:
+8 -6
View File
@@ -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]:
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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"):
+1 -1
View File
@@ -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"):
+2 -2
View File
@@ -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()
+15 -12
View File
@@ -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):