User 'data' folder #5564

Bonsai now creates a user data folder that allows you overriding Bonsai data files. E.g. you can put you own `default.css` as `data/assets/default.css` in that folder and it will override the `default.css` Bonsai is using for the drawings. Previously you could have change internal `default.css` but Bonsai would always restore the original one on every update / reinstallation.

You can also store your .ifc files in data/libraries or data/templates and Bonsai will ensure they're loaded and you won't need to worry about Bonsai removing them later.

E.g. on Windows this path is `C:\Users\xxx\AppData\Roaming\bonsai\bonsai\data`/

You can also find path in the preferences - https://i.imgur.com/kwMc0JU.png
This commit is contained in:
Andrej730
2025-01-20 18:46:53 +05:00
parent c08d25de42
commit 5e930972d6
22 changed files with 86 additions and 67 deletions
+1 -1
View File
@@ -616,7 +616,7 @@ class AddBcfViewpoint(bpy.types.Operator):
old_file_format = blender_render.image_settings.file_format
blender_render.image_settings.file_format = "PNG"
old_filepath = blender_render.filepath
blender_render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
blender_render.filepath = tool.Blender.get_data_dir_path("snapshot.png").__str__()
bpy.ops.render.opengl(write_still=True)
with open(blender_render.filepath, "rb") as f:
snapshot = f.read()
@@ -266,7 +266,7 @@ class ExecuteIfcClash(bpy.types.Operator):
context.scene.render.resolution_x = 480
context.scene.render.resolution_y = 270
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
context.scene.render.filepath = tool.Blender.get_data_dir_path("shapshot.png").__str__()
bpy.ops.render.opengl(write_still=True)
with open(context.scene.render.filepath, "rb") as f:
return ("snapshot.png", f.read())
@@ -22,6 +22,7 @@ import math
import bmesh
import bonsai.tool as tool
import ifcopenshell.util.element
from pathlib import Path
from mathutils import Vector, Matrix
from typing import Optional
@@ -43,7 +44,7 @@ class Annotator:
font = bpy.data.fonts.get("OpenGost TypeB TT")
if not font:
font = bpy.data.fonts.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
)
font.name = "OpenGost Type B TT"
obj.data.font = font
+1 -2
View File
@@ -82,8 +82,7 @@ class SheetsData:
@classmethod
def titleblocks(cls):
files = Path(os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
files = [str(f.stem) for f in files]
files = [p.stem for p in tool.Blender.get_data_dir_paths(Path("templates") / "titleblocks", "*.svg")]
if tool.Ifc.get():
project = tool.Ifc.get().by_type("IfcProject")[0]
@@ -28,6 +28,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
from pathlib import Path
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
@@ -2022,9 +2023,9 @@ class DecorationsHandler:
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
if os.path.exists(drawing_font_path):
font_id = blf.load(drawing_font_path)
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__())
for decorator in self.decorators.values():
decorator.font_id = font_id
@@ -276,7 +276,6 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
self.svg_writer.scale = self.scale
self.svg_writer.data_dir = context.scene.BIMProperties.data_dir
self.svg_writer.camera = self.camera
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
self.svg_writer.camera_projection = tuple(
@@ -1667,7 +1666,6 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_drawing(reference, drawing, sheet)
tool.Drawing.import_sheets()
@@ -1697,7 +1695,6 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
sheet = tool.Drawing.get_reference_document(reference)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.remove_drawing(reference, sheet)
tool.Ifc.run("document.remove_reference", reference=reference)
@@ -1760,7 +1757,6 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = scene.BIMProperties.data_dir
references = sheet_builder.build(sheet)
raster_references = [tool.Ifc.get_relative_uri(r) for r in references["RASTER"]]
@@ -2184,7 +2180,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
json_path = Path(tool.Ifc.resolve_uri(rel_path))
if not json_path.exists():
ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json"
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / "shading_styles.json")
print(
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
f"Default BBIM resource will be copied from {ootb_resource}"
@@ -2582,7 +2578,6 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_document(reference, schedule, sheet)
tool.Drawing.import_sheets()
@@ -2649,7 +2644,6 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_document(reference, extref, sheet)
tool.Drawing.import_sheets()
@@ -3079,7 +3073,6 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
attributes={"Location": tool.Drawing.get_default_titleblock_path(titleblock)},
)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.change_titleblock(sheet, titleblock)
tool.Drawing.import_sheets()
@@ -22,6 +22,7 @@ import bpy
import string
import svgwrite
import openpyxl
import bonsai.tool as tool
from bonsai.bim.module.drawing.svgwriter import SvgWriter
from odf.opendocument import load as load_ods
@@ -77,7 +78,7 @@ class Scheduler:
ifc_file_path = os.path.dirname(IfcStore.path)
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
if not os.path.exists(stylesheet_path):
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css")
with open(stylesheet_path, "r") as stylesheet:
css = stylesheet.read()
@@ -26,6 +26,7 @@ import urllib.parse
import xml.etree.ElementTree as ET
import bonsai.tool as tool
import ifcopenshell.util.geolocation
from pathlib import Path
from xml.dom import minidom
from mathutils import Vector
import re
@@ -38,7 +39,6 @@ XLINK = "{http://www.w3.org/1999/xlink}"
class SheetBuilder:
def __init__(self):
self.data_dir = None
self.scale = "NTS"
def create(self, layout_path: str, titleblock_name: str) -> None:
@@ -49,7 +49,9 @@ class SheetBuilder:
root.attrib["version"] = "1.1"
sheet_dir = os.path.dirname(layout_path)
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
ootb_titleblock_path = tool.Blender.get_data_dir_path(
Path("templates") / "titleblock" / (titleblock_name + ".svg")
)
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
os.makedirs(sheet_dir, exist_ok=True)
@@ -241,7 +243,7 @@ class SheetBuilder:
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg")
ootb_title = tool.Blender.get_data_dir_path(Path("assets") / "view-title.svg")
shutil.copy(ootb_title, title_path)
title_tree = ET.parse(title_path)
@@ -492,7 +494,9 @@ class SheetBuilder:
return group
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
ootb_titleblock_path = tool.Blender.get_data_dir_path(
Path("templates") / "titleblocks" / (titleblock_name + ".svg")
)
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
sheet_dir = os.path.dirname(sheet_path)
@@ -38,6 +38,7 @@ from bonsai.bim.module.drawing.data import DecoratorData
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from typing import Optional, Self
from pathlib import Path
class External(svgwrite.container.Group):
@@ -98,7 +99,7 @@ class SvgWriter:
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / resource_basename)
print(
f"WARNING. Couldn't find {resource} for the drawing by the path: {resource_path}. Default BBIM resource will be copied from {ootb_resource}"
)
@@ -93,7 +93,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
args = ifcpatch.ArgumentsDict(
recipe=props.ifc_patch_recipes,
arguments=arguments,
log=os.path.join(context.scene.BIMProperties.data_dir, "process.log"),
log=tool.Blender.get_data_dir_path("process.log").__str__(),
)
if props.should_load_from_memory and tool.Ifc.get():
+2 -8
View File
@@ -69,11 +69,8 @@ class ProjectData:
if not ifc_file:
return []
current_schema = tool.Ifc.get().schema_identifier
files = (Path(bpy.context.scene.BIMProperties.data_dir) / "libraries").iterdir()
results = [("0", "Custom Library", "")]
for f in files:
if not f.suffix.lower().startswith(".ifc"):
continue
for f in tool.Blender.get_data_dir_paths("libraries", "*.ifc*"):
if cls.get_file_schema(f) != current_schema:
continue
results.append((f.name, f.stem, "Library"))
@@ -85,10 +82,7 @@ class ProjectData:
for export_schema in cls.data["export_schema"]:
template_files[export_schema[0]].append(("0", "Blank Project", ""))
files = (Path(bpy.context.scene.BIMProperties.data_dir) / "templates" / "projects").iterdir()
for f in files:
if not f.suffix.lower().startswith(".ifc"):
continue
for f in tool.Blender.get_data_dir_paths(Path("templates") / "projects", "*.ifc*"):
current_schema = cls.get_file_schema(f)
if current_schema not in template_files:
continue
@@ -856,8 +856,8 @@ class LoadProjectElements(bpy.types.Operator):
bonsai.bim.schema.reload(self.file.schema_identifier)
start = time.time()
logger = logging.getLogger("ImportIFC")
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
logging.basicConfig(
filename=path_log,
@@ -1374,8 +1374,8 @@ class ExportIFC(bpy.types.Operator):
def _execute(self, context):
start = time.time()
logger = logging.getLogger("ExportIFC")
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
logging.basicConfig(
filename=path_log,
+3 -3
View File
@@ -20,6 +20,7 @@ import os
import bpy
import ifcopenshell.util.element
import ifcopenshell.util.placement
import bonsai.tool as tool
from bonsai.bim.module.project.data import ProjectData
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, ObjProperty
@@ -60,9 +61,8 @@ def get_library_file(self: "BIMProjectProperties", context: bpy.types.Context) -
def update_library_file(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
if self.library_file != "0":
bpy.ops.bim.select_library_file(
filepath=os.path.join(bpy.context.scene.BIMProperties.data_dir, "libraries", self.library_file)
)
filepath = next(p for p in tool.Blender.get_data_dir_paths("libraries", "*.ifc") if p.name == self.library_file)
bpy.ops.bim.select_library_file(filepath=filepath)
def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
@@ -109,19 +109,15 @@ class PsetTemplatesData:
return [(i, i, "") for i in enum_items]
@classmethod
def pset_template_files(cls):
results = []
pset_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "pset")
files = os.listdir(pset_dir)
for f in files:
results.append(
(os.path.join(pset_dir, f), os.path.splitext(os.path.basename(f))[0], "Global Pset Template")
)
def pset_template_files(cls) -> list[tuple[str, str, str]]:
results: list[tuple[str, str, str]] = []
for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"):
results.append((f.__str__(), f.stem, "Global Pset Template"))
pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)
if os.path.isdir(pset_dir):
for path in pathlib.Path(pset_dir).glob("*.ifc"):
results.append((str(path), os.path.splitext(os.path.basename(str(path)))[0], "Project Pset Template"))
results.append((str(path), path.stem, "Project Pset Template"))
return sorted(results, key=lambda x: x[1])
@@ -42,7 +42,9 @@ class AddPsetTemplateFile(bpy.types.Operator):
def execute(self, context):
template = ifcopenshell.file()
filepath = os.path.join(context.scene.BIMProperties.data_dir, "pset", self.new_template_filename + ".ifc")
filepath = next(
p for p in tool.Blender.get_data_dir_paths("pset", "*.ifc") if p.stem == self.new_template_filename
).__str__()
pset_template = ifcopenshell.api.run("pset_template.add_pset_template", template)
ifcopenshell.api.run("pset_template.add_prop_template", template, pset_template=pset_template)
+2 -2
View File
@@ -812,8 +812,8 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator):
start = time.time()
logger = logging.getLogger("ImportIFC")
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
logging.basicConfig(
filename=path_log,
+3 -1
View File
@@ -439,7 +439,9 @@ class BIMProperties(PropertyGroup):
default=os.path.join(cwd, "schema") + os.path.sep, name="Schema Directory", update=update_schema_dir
)
data_dir: StringProperty(
default=os.path.join(cwd, "data") + os.path.sep, name="Data Directory", update=update_data_dir
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
+29 -1
View File
@@ -33,7 +33,7 @@ import importlib
from mathutils import Vector
from pathlib import Path
from bonsai.bim.ifc import IFC_CONNECTED_TYPE
from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar
from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, Generator
from typing_extensions import assert_never
@@ -1470,3 +1470,31 @@ class Blender(bonsai.core.tool.Blender):
return "dm" if rgb_sum > threshold else "lm"
except Exception:
return "dm" # Default to dark mode if an error occurs
@classmethod
def get_default_data_dir(cls) -> Path:
return Path(__file__).parent.parent / "bim" / "data"
@classmethod
def get_custom_data_dir(cls) -> Path:
return Path(bpy.context.scene.BIMProperties.data_dir)
@classmethod
def get_data_dir_path(cls, relative_path: Union[str, Path]) -> Path:
custom_path = cls.get_custom_data_dir() / relative_path
if custom_path.exists():
return custom_path
return cls.get_default_data_dir() / relative_path
@classmethod
def get_data_dir_paths(cls, relative_dir_path: Union[str, Path], glob_pattern: str) -> Generator[Path, None, None]:
custom_path = cls.get_custom_data_dir() / relative_dir_path
if custom_path.is_dir():
for filepath in custom_path.glob(glob_pattern):
yield filepath
default_data_dir = cls.get_default_data_dir()
if default_data_dir == custom_path:
return
for filepath in (default_data_dir / relative_dir_path).glob(glob_pattern):
yield filepath
+3 -2
View File
@@ -9,6 +9,7 @@ import ifcopenshell.util.cost
import ifcopenshell.util.unit
import bonsai.bim.helper
import json
from pathlib import Path
from typing import Optional, Any, Generator, Union, Literal
@@ -111,7 +112,7 @@ class Cost(bonsai.core.tool.Cost):
device = aud.Device()
# chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/
sound = aud.Sound(os.path.join(bpy.context.scene.BIMProperties.data_dir, "chaching.mp3"))
sound = aud.Sound(tool.Blender.get_data_dir_path(filename="chaching.mp3").__str__())
handle = device.play(sound)
sound_buffered = aud.Sound.buffer(sound)
handle_buffered = device.play(sound_buffered)
@@ -627,7 +628,7 @@ class Cost(bonsai.core.tool.Cost):
if filepath:
path = filepath
else:
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", "cost_schedules")
path = tool.Blender.get_data_dir_path(Path("build") / "cost_schedules").__str__()
if not os.path.exists(path):
os.makedirs(path)
+2 -4
View File
@@ -281,7 +281,6 @@ class Drawing(bonsai.core.tool.Drawing):
import bonsai.bim.module.drawing.sheeter as sheeter
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
uri = cls.get_document_uri(document, "LAYOUT")
sheet_builder.create(uri, titleblock)
return uri
@@ -291,7 +290,6 @@ class Drawing(bonsai.core.tool.Drawing):
import bonsai.bim.module.drawing.sheeter as sheeter
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
drawing_references = {}
drawing_names = []
for reference in cls.get_document_references(sheet):
@@ -1180,8 +1178,8 @@ class Drawing(bonsai.core.tool.Drawing):
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
if os.path.exists(ootb_resource):
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / resource_basename)
if ootb_resource.is_file():
shutil.copy(ootb_resource, resource_path)
@classmethod
+2 -2
View File
@@ -40,8 +40,8 @@ class Project(bonsai.core.tool.Project):
@classmethod
def append_all_types_from_template(cls, template: str) -> None:
# TODO refactor
filepath = os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "projects", template)
bpy.ops.bim.select_library_file(filepath=filepath)
filepath = tool.Blender.get_data_dir_path(Path("templates") / "projects" / template)
bpy.ops.bim.select_library_file(filepath=filepath.__str__())
if IfcStore.library_file.schema != tool.Ifc.get().schema:
return
for element in IfcStore.library_file.by_type("IfcTypeProduct"):
+5 -7
View File
@@ -112,8 +112,8 @@ class Web(bonsai.core.tool.Web):
global ws_process
webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui")
ws_path = os.path.join(webui_path, "sioserver.py")
webui_path = tool.Blender.get_data_dir_path("webui")
ws_path = (webui_path / "sioserver.py").__str__()
py_version = sys.version_info
@@ -136,7 +136,7 @@ class Web(bonsai.core.tool.Web):
ws_process = subprocess.Popen(
[sys.executable, ws_path, "--p", str(port), "--host", "127.0.0.1"],
cwd=webui_path,
cwd=webui_path.__str__(),
env=env,
)
@@ -208,8 +208,7 @@ class Web(bonsai.core.tool.Web):
cls.disconnect_websocket_server()
# sleep(0.5)
webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui")
pid_file = os.path.join(webui_path, "running_pid.json")
pid_file = tool.Blender.get_data_dir_path("webui") / "running_pid.json"
with open(pid_file, "r") as f:
pids = json.load(f)
@@ -246,8 +245,7 @@ class Web(bonsai.core.tool.Web):
while True:
if time.time() - start > max_time:
return False
webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui")
pid_file = os.path.join(webui_path, "running_pid.json")
pid_file = tool.Blender.get_data_dir_path("webui") / "running_pid.json"
try:
with open(pid_file, "r") as f:
data = json.load(f)