From 64aeef71034528e95b53759e96dc011891fbe95e Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 21 Jun 2025 07:59:51 -0500 Subject: [PATCH 01/32] Automatically detect bonsai_path --- .../scripts/installation/dev_environment.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/bonsai/scripts/installation/dev_environment.py b/src/bonsai/scripts/installation/dev_environment.py index 7dd32633a2..886cf3548d 100644 --- a/src/bonsai/scripts/installation/dev_environment.py +++ b/src/bonsai/scripts/installation/dev_environment.py @@ -38,14 +38,23 @@ REPO_PATH = r"" # Usually don't need to change, just ensure Blender version matches. BLENDER_PATH = Path.home() / r"AppData/Roaming/Blender Foundation/Blender/4.4" -# BONSAI_PATH: Path to 'bonsai' extension folder inside BLENDER_PATH. -# Need to ensure extensions repo folder in the path below ('raw_githubusercontent_com') matches yours. -# -# Typical scenarios: -# - Bonsai is installed from Bonsai Unstalble Repo - use 'raw_githubusercontent_com' (as it is by default) -# - Bonsai is installed via offline installation - use 'user_default' -# - Bonsai is installed from Blender's official extensions platform - use 'blender_org' -BONSAI_PATH = BLENDER_PATH / r"extensions/raw_githubusercontent_com/bonsai" +# Determine BONSAI_PATH from existing options +def find_bonsai_path(blender_path): + candidates = [ + blender_path / r"extensions/raw_githubusercontent_com/bonsai", + blender_path / r"extensions/user_default/bonsai", + blender_path / r"extensions/blender_org/bonsai", + ] + for path in candidates: + if path.exists(): + print(f"Found Bonsai at: {path}") + return path + raise FileNotFoundError("Could not find Bonsai path in expected locations.") + +BONSAI_PATH = find_bonsai_path(BLENDER_PATH) + + + # --------------------------- From f3c86d7fcc47fa0af2f9b540a6a4cab334467c8e Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 21 Jun 2025 08:08:50 -0500 Subject: [PATCH 02/32] added a few to ifc_classes_suggestions.json --- bb.bat | 114 ++++++++++++++++++ .../util/schema/ifc_classes_suggestions.json | 45 +++++-- 2 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 bb.bat diff --git a/bb.bat b/bb.bat new file mode 100644 index 0000000000..67e19f9720 --- /dev/null +++ b/bb.bat @@ -0,0 +1,114 @@ +@echo off + +setlocal enabledelayedexpansion + +:: Ensure script runs from its own directory +cd /d "%~dp0" + +:: Use pushd/popd in case we need to return later +pushd . + + +rem ADAPT setting REPO_PATH to use custom path for IfcOpenShell repository. +rem Otherwise by default it is assumed script is executed from IfcOpenShell directory. +SET REPO_PATH=D:\Dropbox\GitHub\IfcOpenShell +SET BLENDER_PATH=%HOMEDRIVE%\Users\%USERNAME%\AppData\Roaming\Blender Foundation\Blender\4.2 +SET PACKAGE_PATH=%BLENDER_PATH%\extensions\.local\lib\python3.11\site-packages +SET BONSAI_PATH=%BLENDER_PATH%\extensions\user_default\bonsai +rem Note: if you originally installed Bonsai via Blender's extension the path will be... +rem BONSAI_PATH=%BLENDER_PATH%\extensions\user_default\bonsai + +echo SETUP BONSAI ADD-ON LIVE DEVELOPMENT ENVIRONMENT +echo Update REPO_PATH, BLENDER_PATH, PACKAGE_PATH, BONSAI_PATH in the script above. +echo This script needs to be run as administrator (to create symbolic links) +echo Make sure you have followed these steps before proceeding :) +echo. +echo Currently set paths: +if not defined REPO_PATH ( + echo REPO_PATH is not set, assuming we're already in IfcOpenShell directory. + set REPO_PATH=%cd% +) +rem Print summary of paths +echo. +echo Please review if the following is right: +echo PWD: %CD% +echo REPO PATH (.....\IfcOpenshell): %REPO_PATH% +echo BLENDER PATH: %BLENDER_PATH% +echo PACKAGE PATH (.....\extensions\.local\lib\python3.11\site-packages): %PACKAGE_PATH% +echo BONSAI PATH (.....\extensions\raw_githubusercontent_com\bonsai): %BONSAI_PATH% +echo. +rem Prompt user to continue +pause + + +echo Changing to the Git repository directory... +cd %REPO_PATH% + +:: Handle symlinks (they could be disabled by default on Windows). +git config --local core.symlinks true + +:: Delete and checkout is the only way to ensure files are added as symlinks. +del /Q src\bonsai\bonsai\bim\data\templates\projects\*.ifc +git checkout -- src/bonsai/bonsai/bim/data/templates/projects/*.ifc + +rem Add files that need to be ignored in push to GitHub (.gitignore) +set "FILE=.gitignore" +set "STRING=*.so" + +rem Check if the file exists and contains the string +if not exist "%FILE%" ( + echo %STRING% >> "%FILE%" + echo "%FILE%" does not exist + echo Added '%STRING%' to %FILE% +) else ( + findstr /L "%STRING%" "%FILE%" >nul + if errorlevel 1 ( + echo %STRING% >> "%FILE%" + echo %STRING% not found in %FILE% + echo Added %STRING% to %FILE% + ) else ( + echo '%STRING%' already exists in %FILE% + ) +) + +echo Copy over compiled IfcOpenShell files... +copy "%PACKAGE_PATH%\ifcopenshell\*_wrapper*" "%CD%\src\ifcopenshell-python\ifcopenshell\" + +echo Remove extension and link to Git... +del "%BONSAI_PATH%\__init__.py" +rd /S /Q "%PACKAGE_PATH%\bonsai" +rd /S /Q "%PACKAGE_PATH%\ifcopenshell" +del "%PACKAGE_PATH%\ifccsv.py" +del "%PACKAGE_PATH%\ifcdiff.py" +del "%PACKAGE_PATH%\bsdd.py" +rd /S /Q "%PACKAGE_PATH%\bcf" +rd /S /Q "%PACKAGE_PATH%\ifc4d" +rd /S /Q "%PACKAGE_PATH%\ifc5d" +rd /S /Q "%PACKAGE_PATH%\ifccityjson" +rd /S /Q "%PACKAGE_PATH%\ifcclash" +rd /S /Q "%PACKAGE_PATH%\ifcpatch" +rd /S /Q "%PACKAGE_PATH%\ifctester" +rd /S /Q "%PACKAGE_PATH%\ifcfm" + +mklink "%BONSAI_PATH%\__init__.py" "%CD%\src\bonsai\bonsai\__init__.py" +mklink /D "%PACKAGE_PATH%\bonsai" "%CD%\src\bonsai\bonsai" +mklink /D "%PACKAGE_PATH%\ifcopenshell" "%CD%\src\ifcopenshell-python\ifcopenshell" +mklink "%PACKAGE_PATH%\ifccsv.py" "%CD%\src\ifccsv\ifccsv.py" +mklink "%PACKAGE_PATH%\ifcdiff.py" "%CD%\src\ifcdiff\ifcdiff.py" +mklink "%PACKAGE_PATH%\bsdd.py" "%CD%\src\bsdd\bsdd.py" +mklink /D "%PACKAGE_PATH%\bcf" "%CD%\src\bcf\bcf" +mklink /D "%PACKAGE_PATH%\ifc4d" "%CD%\src\ifc4d\ifc4d" +mklink /D "%PACKAGE_PATH%\ifc5d" "%CD%\src\ifc5d\ifc5d" +mklink /D "%PACKAGE_PATH%\ifccityjson" "%CD%\src\ifccityjson\ifccityjson" +mklink /D "%PACKAGE_PATH%\ifcclash" "%CD%\src\ifcclash\ifcclash" +mklink /D "%PACKAGE_PATH%\ifcpatch" "%CD%\src\ifcpatch\ifcpatch" +mklink /D "%PACKAGE_PATH%\ifctester" "%CD%\src\ifctester\ifctester" +mklink /D "%PACKAGE_PATH%\ifcfm" "%CD%\src\ifcfm\ifcfm" + +echo Manually downloading some third party dependencies... +curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js -o "%PACKAGE_PATH%\bonsai\bim\data\gantt\jsgantt.js" +curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css -o "%PACKAGE_PATH%\bonsai\bim\data\gantt\jsgantt.css" +curl -L https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl -o "%PACKAGE_PATH%\bonsai\bim\data\brick\Brick.ttl" + +popd +pause \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json b/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json index 1afe8852e4..ea520e028b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json +++ b/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json @@ -31,16 +31,6 @@ "predefined_type": "TOPPING" } ], - "IfcDistributionElement": [ - { - "name": "Furnace" - } - ], - "IfcDistributionElementType": [ - { - "name": "Furnace" - } - ], "IfcElectricDistributionBoard": [ { "name": "Circuit Breaker Panel", @@ -140,6 +130,41 @@ "IfcUnitaryEquipment": [ { "name": "Fan Coil Unit" + }, + { + "name": "Roof Top Unit (RTU)", + "predefined_type": "ROOFTOPUNIT" + }, + { + "name": "Furnace" + }, + { + "name": "Central air conditioner (split or package)", + "predefined_type": "AIRCONDITIONINGUNIT" + }, + { + "name": "Mini-split system", + "predefined_type": "SPLITSYSTEM" + } + ], + "IfcUnitaryEquipmentType": [ + { + "name": "Fan Coil Unit" + }, + { + "name": "Roof Top Unit (RTU)", + "predefined_type": "ROOFTOPUNIT" + }, + { + "name": "Furnace" + }, + { + "name": "Central air conditioner (split or package)", + "predefined_type": "AIRCONDITIONINGUNIT" + }, + { + "name": "Mini-split system", + "predefined_type": "SPLITSYSTEM" } ], "IfcWall": [ From 9149bb0fdcb14fac1474cbb9a4be113ef34ad05f Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 21 Jun 2025 08:11:01 -0500 Subject: [PATCH 03/32] whoops, accidently pushed. --- bb.bat | 114 --------------------------------------------------------- 1 file changed, 114 deletions(-) delete mode 100644 bb.bat diff --git a/bb.bat b/bb.bat deleted file mode 100644 index 67e19f9720..0000000000 --- a/bb.bat +++ /dev/null @@ -1,114 +0,0 @@ -@echo off - -setlocal enabledelayedexpansion - -:: Ensure script runs from its own directory -cd /d "%~dp0" - -:: Use pushd/popd in case we need to return later -pushd . - - -rem ADAPT setting REPO_PATH to use custom path for IfcOpenShell repository. -rem Otherwise by default it is assumed script is executed from IfcOpenShell directory. -SET REPO_PATH=D:\Dropbox\GitHub\IfcOpenShell -SET BLENDER_PATH=%HOMEDRIVE%\Users\%USERNAME%\AppData\Roaming\Blender Foundation\Blender\4.2 -SET PACKAGE_PATH=%BLENDER_PATH%\extensions\.local\lib\python3.11\site-packages -SET BONSAI_PATH=%BLENDER_PATH%\extensions\user_default\bonsai -rem Note: if you originally installed Bonsai via Blender's extension the path will be... -rem BONSAI_PATH=%BLENDER_PATH%\extensions\user_default\bonsai - -echo SETUP BONSAI ADD-ON LIVE DEVELOPMENT ENVIRONMENT -echo Update REPO_PATH, BLENDER_PATH, PACKAGE_PATH, BONSAI_PATH in the script above. -echo This script needs to be run as administrator (to create symbolic links) -echo Make sure you have followed these steps before proceeding :) -echo. -echo Currently set paths: -if not defined REPO_PATH ( - echo REPO_PATH is not set, assuming we're already in IfcOpenShell directory. - set REPO_PATH=%cd% -) -rem Print summary of paths -echo. -echo Please review if the following is right: -echo PWD: %CD% -echo REPO PATH (.....\IfcOpenshell): %REPO_PATH% -echo BLENDER PATH: %BLENDER_PATH% -echo PACKAGE PATH (.....\extensions\.local\lib\python3.11\site-packages): %PACKAGE_PATH% -echo BONSAI PATH (.....\extensions\raw_githubusercontent_com\bonsai): %BONSAI_PATH% -echo. -rem Prompt user to continue -pause - - -echo Changing to the Git repository directory... -cd %REPO_PATH% - -:: Handle symlinks (they could be disabled by default on Windows). -git config --local core.symlinks true - -:: Delete and checkout is the only way to ensure files are added as symlinks. -del /Q src\bonsai\bonsai\bim\data\templates\projects\*.ifc -git checkout -- src/bonsai/bonsai/bim/data/templates/projects/*.ifc - -rem Add files that need to be ignored in push to GitHub (.gitignore) -set "FILE=.gitignore" -set "STRING=*.so" - -rem Check if the file exists and contains the string -if not exist "%FILE%" ( - echo %STRING% >> "%FILE%" - echo "%FILE%" does not exist - echo Added '%STRING%' to %FILE% -) else ( - findstr /L "%STRING%" "%FILE%" >nul - if errorlevel 1 ( - echo %STRING% >> "%FILE%" - echo %STRING% not found in %FILE% - echo Added %STRING% to %FILE% - ) else ( - echo '%STRING%' already exists in %FILE% - ) -) - -echo Copy over compiled IfcOpenShell files... -copy "%PACKAGE_PATH%\ifcopenshell\*_wrapper*" "%CD%\src\ifcopenshell-python\ifcopenshell\" - -echo Remove extension and link to Git... -del "%BONSAI_PATH%\__init__.py" -rd /S /Q "%PACKAGE_PATH%\bonsai" -rd /S /Q "%PACKAGE_PATH%\ifcopenshell" -del "%PACKAGE_PATH%\ifccsv.py" -del "%PACKAGE_PATH%\ifcdiff.py" -del "%PACKAGE_PATH%\bsdd.py" -rd /S /Q "%PACKAGE_PATH%\bcf" -rd /S /Q "%PACKAGE_PATH%\ifc4d" -rd /S /Q "%PACKAGE_PATH%\ifc5d" -rd /S /Q "%PACKAGE_PATH%\ifccityjson" -rd /S /Q "%PACKAGE_PATH%\ifcclash" -rd /S /Q "%PACKAGE_PATH%\ifcpatch" -rd /S /Q "%PACKAGE_PATH%\ifctester" -rd /S /Q "%PACKAGE_PATH%\ifcfm" - -mklink "%BONSAI_PATH%\__init__.py" "%CD%\src\bonsai\bonsai\__init__.py" -mklink /D "%PACKAGE_PATH%\bonsai" "%CD%\src\bonsai\bonsai" -mklink /D "%PACKAGE_PATH%\ifcopenshell" "%CD%\src\ifcopenshell-python\ifcopenshell" -mklink "%PACKAGE_PATH%\ifccsv.py" "%CD%\src\ifccsv\ifccsv.py" -mklink "%PACKAGE_PATH%\ifcdiff.py" "%CD%\src\ifcdiff\ifcdiff.py" -mklink "%PACKAGE_PATH%\bsdd.py" "%CD%\src\bsdd\bsdd.py" -mklink /D "%PACKAGE_PATH%\bcf" "%CD%\src\bcf\bcf" -mklink /D "%PACKAGE_PATH%\ifc4d" "%CD%\src\ifc4d\ifc4d" -mklink /D "%PACKAGE_PATH%\ifc5d" "%CD%\src\ifc5d\ifc5d" -mklink /D "%PACKAGE_PATH%\ifccityjson" "%CD%\src\ifccityjson\ifccityjson" -mklink /D "%PACKAGE_PATH%\ifcclash" "%CD%\src\ifcclash\ifcclash" -mklink /D "%PACKAGE_PATH%\ifcpatch" "%CD%\src\ifcpatch\ifcpatch" -mklink /D "%PACKAGE_PATH%\ifctester" "%CD%\src\ifctester\ifctester" -mklink /D "%PACKAGE_PATH%\ifcfm" "%CD%\src\ifcfm\ifcfm" - -echo Manually downloading some third party dependencies... -curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js -o "%PACKAGE_PATH%\bonsai\bim\data\gantt\jsgantt.js" -curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css -o "%PACKAGE_PATH%\bonsai\bim\data\gantt\jsgantt.css" -curl -L https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl -o "%PACKAGE_PATH%\bonsai\bim\data\brick\Brick.ttl" - -popd -pause \ No newline at end of file From d875baefde59b1a12a582f970af6b29954c93812 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 21 Jun 2025 10:14:09 -0500 Subject: [PATCH 04/32] #5709 - Partly addresses RCP camera flipping around. (still hacky though) --- src/bonsai/bonsai/bim/module/geometry/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 8c5f8ef9ec..61db6923c9 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -20,6 +20,7 @@ import bpy from . import ui, prop, operator from bpy.app.handlers import persistent import ifcopenshell.util.element +import math classes = ( operator.AddCurvelikeItem, @@ -108,6 +109,7 @@ def block_scale(scene: bpy.types.Scene) -> None: camera = tool.Ifc.get_entity(obj) if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW": obj.scale = (-1, -1, -1) + obj.rotation_euler = (0.0, 0.0, math.radians(180)) else: if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) From 98c479f1bd55df210f8cca0773faf890bd7e29ab Mon Sep 17 00:00:00 2001 From: falken10vdl <33285113+falken10vdl@users.noreply.github.com> Date: Sun, 22 Jun 2025 14:09:57 +0200 Subject: [PATCH 05/32] Create instance context menu (#6830) --- src/bonsai/bonsai/bim/helper.py | 2 +- .../bonsai/bim/module/geometry/__init__.py | 1 + .../bonsai/bim/module/geometry/operator.py | 36 +++++++++++++++++++ src/bonsai/bonsai/bim/module/geometry/ui.py | 6 ++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 31be04f874..b4e3ba4f79 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -314,7 +314,7 @@ def prop_with_search( row = layout.row(align=True) row.prop(data, prop_name, **kwargs) try: - if len(get_enum_items(data, prop_name, original_operator_path=original_operator_path)) > 10: + if len(get_enum_items(data, prop_name, original_operator_path=original_operator_path)) > 0: # Magick courtesy of https://blender.stackexchange.com/a/203443/86891 row.context_pointer_set(name="data", data=data) op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM") diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 61db6923c9..bba979af30 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -77,6 +77,7 @@ classes = ( operator.UpdateItemAttributes, operator.UpdateParametricRepresentation, operator.UpdateRepresentation, + operator.CreateInstance, prop.RepresentationItem, prop.RepresentationItemObject, prop.ShapeAspect, diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index f59467def6..0ed5c4cdd6 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -46,6 +46,7 @@ import bonsai.core.root import bonsai.core.drawing import bonsai.tool as tool import bonsai.bim.handler +from bonsai.bim.module.model.data import AuthoringData from mathutils import Vector, Matrix, Quaternion from time import time from bonsai.bim.ifc import IfcStore @@ -3432,3 +3433,38 @@ class UnassignRepresentationLayer(bpy.types.Operator, tool.Ifc.Operator): layer = ifc_file.by_id(self.layer_id) ifcopenshell.api.layer.unassign_layer(ifc_file, [representation], layer) return {"FINISHED"} + + +class CreateInstance(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.create_instance" + bl_label = "IFC Create Instance" + bl_description = "Create an instance of the type associated with the selected object" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + if not context.selected_objects or len(context.selected_objects) > 1: + self.report({"ERROR"}, "Select exactly one object to create an instance of its type") + return {"CANCELLED"} + + active_obj = context.active_object + element = tool.Ifc.get_entity(active_obj) + if not element: + self.report({"ERROR"}, "Selected object is not an IFC element") + return {"CANCELLED"} + + relating_type = ifcopenshell.util.element.get_type(element) + if not relating_type: + self.report({"ERROR"}, "Selected object has no associated type") + return {"CANCELLED"} + + try: + props = tool.Model.get_model_props() + props.ifc_class = relating_type.is_a() + props.relating_type_id = str(relating_type.id()) + except: + self.report({"ERROR"}, "You must be using the Multiobject Tool or the relevant editing tool") + return {"CANCELLED"} + + bpy.ops.bim.hotkey(hotkey="S_A") + + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py index 8b7e7be43a..f814cd21d3 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -66,6 +66,12 @@ def object_menu(self, context): self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN") self.layout.menu("BIM_MT_separate", icon="PLUGIN") + # only show the create instance operator if the current tool is the BIM tool + # if context.space_data and hasattr(context.space_data, "show_object_viewport_mesh"): + # current_tool = context.workspace.tools.from_space_view3d_mode(context.mode, create=False) + # if current_tool and current_tool.idname == "bim.bim_tool": + self.layout.operator("bim.create_instance", icon="PLUGIN") + def edit_mesh_menu(self, context): self.layout.separator() From 3a06afae254091b3e45a836887125359588fb377 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 13:52:56 +0500 Subject: [PATCH 06/32] black . --- src/bonsai/scripts/installation/dev_environment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/scripts/installation/dev_environment.py b/src/bonsai/scripts/installation/dev_environment.py index 886cf3548d..06df7700c2 100644 --- a/src/bonsai/scripts/installation/dev_environment.py +++ b/src/bonsai/scripts/installation/dev_environment.py @@ -38,6 +38,7 @@ REPO_PATH = r"" # Usually don't need to change, just ensure Blender version matches. BLENDER_PATH = Path.home() / r"AppData/Roaming/Blender Foundation/Blender/4.4" + # Determine BONSAI_PATH from existing options def find_bonsai_path(blender_path): candidates = [ @@ -51,11 +52,10 @@ def find_bonsai_path(blender_path): return path raise FileNotFoundError("Could not find Bonsai path in expected locations.") + BONSAI_PATH = find_bonsai_path(BLENDER_PATH) - - # --------------------------- # Never changed by user. From 30c33518f09d532bf2b2c06db2bfabdcf6f312af Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 14:30:22 +0500 Subject: [PATCH 07/32] typing --- .../bonsai/bim/module/light/decorator.py | 12 +- .../bonsai/bim/module/light/operator.py | 49 +++---- src/bonsai/bonsai/bim/module/light/prop.py | 120 ++++++++++++++---- src/bonsai/bonsai/bim/module/light/ui.py | 18 ++- src/bonsai/bonsai/tool/blender.py | 19 ++- 5 files changed, 157 insertions(+), 61 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/decorator.py b/src/bonsai/bonsai/bim/module/light/decorator.py index 127f636919..9bdeb961e1 100644 --- a/src/bonsai/bonsai/bim/module/light/decorator.py +++ b/src/bonsai/bonsai/bim/module/light/decorator.py @@ -59,10 +59,10 @@ class SolarDecorator: shader.uniform_float("color", color) batch.draw(shader) - def draw_text(self, context): + def draw_text(self, context: bpy.types.Context) -> None: self.addon_prefs = tool.Blender.get_addon_preferences() - props = bpy.context.scene.BIMSolarProperties + props = tool.Blender.get_solar_props() origin = Matrix.Translation(props.sun_path_origin) location = origin @ (props.sun_position * 1.05) @@ -76,7 +76,8 @@ class SolarDecorator: grid_north_p = origin @ angle @ (Vector((0, 0.8, 0)) * props.sun_path_size) self.draw_text_at_position(context, "True North", grid_north_p) - def draw_text_at_position(self, context, text, position): + def draw_text_at_position(self, context: bpy.types.Context, text: str, position: Vector) -> None: + assert context.region and context.region_data coords_2d = location_3d_to_region_2d(context.region, context.region_data, position) if not coords_2d: return @@ -87,7 +88,8 @@ class SolarDecorator: blf.position(self.font_id, co[0], co[1], 0) blf.draw(self.font_id, line) - def draw_geometry(self, context): + def draw_geometry(self, context: bpy.types.Context) -> None: + assert context.region self.addon_prefs = tool.Blender.get_addon_preferences() decorator_color_special = self.addon_prefs.decorator_color_special decorator_color_error = self.addon_prefs.decorator_color_error @@ -131,7 +133,7 @@ class SolarDecorator: self.shader = gpu.types.GPUShader(vertex_shader, fragment_shader) - props = bpy.context.scene.BIMSolarProperties + props = tool.Blender.get_solar_props() origin = Matrix.Translation(props.sun_path_origin) diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py index 9d02146036..6eb3e2a09c 100644 --- a/src/bonsai/bonsai/bim/module/light/operator.py +++ b/src/bonsai/bonsai/bim/module/light/operator.py @@ -58,7 +58,7 @@ class ExportOBJ(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() if not props.should_load_from_memory and not props.ifc_file: cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.") return False @@ -66,10 +66,11 @@ class ExportOBJ(bpy.types.Operator): def execute(self, context): # Get the output directory - should_load_from_memory = context.scene.radiance_exporter_properties.should_load_from_memory - output_dir = context.scene.radiance_exporter_properties.output_dir + props = tool.Blender.get_radiance_exporter_props() + should_load_from_memory = props.should_load_from_memory + output_dir = props.output_dir - context.scene.radiance_exporter_properties.is_exporting = True + props.is_exporting = True # Conversion from IFC to OBJ # Settings for obj @@ -86,7 +87,7 @@ class ExportOBJ(bpy.types.Operator): ifc_file = tool.Ifc.get() else: - ifc_file_path = context.scene.radiance_exporter_properties.ifc_file + ifc_file_path = props.ifc_file ifc_file = ifcopenshell.open(ifc_file_path) obj_file_path = os.path.join(output_dir, "model.obj") @@ -121,7 +122,7 @@ class ExportOBJ(bpy.types.Operator): break serialiser.finalize() - context.scene.radiance_exporter_properties.is_exporting = False + props.is_exporting = False self.report({"INFO"}, "Exported OBJ file to: {}".format(obj_file_path)) @@ -141,9 +142,10 @@ class RadianceRender(bpy.types.Operator): return {"CANCELLED"} print("Starting Radiance rendering process...") - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y + assert context.scene context.scene.render.resolution_x = resolution_x context.scene.render.resolution_y = resolution_y @@ -172,8 +174,9 @@ class RadianceRender(bpy.types.Operator): obj_file_path = os.path.join(output_dir, "model.obj") - sun_props = context.scene.BIMSolarProperties - sun_pos_props = context.scene.sun_pos_properties + sun_props = tool.Blender.get_solar_props() + sun_pos_props = tool.Blender.get_sun_props() + assert sun_pos_props sky_file_path = os.path.join(output_dir, "sky.rad") # latitude = sun_props.latitude # longitude = sun_props.longitude @@ -331,7 +334,7 @@ ground_glow source ground # f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n") # f.write("ground_glow source ground\n0\n0\n4 0 0 -1 180\n") - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() data = props.get_mappings_dict() @@ -458,7 +461,7 @@ ground_glow source ground return {"FINISHED"} def get_active_camera(self, context): - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() if props.use_active_camera: return context.scene.camera else: @@ -475,8 +478,7 @@ ground_glow source ground return (position.x, position.y, position.z), (direction.x, direction.y, direction.z) def getResolution(self, context): - scene = context.scene - props = scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() resolution_x = props.radiance_resolution_x resolution_y = props.radiance_resolution_y return resolution_x, resolution_y @@ -505,7 +507,7 @@ class ImportTrueNorth(bpy.types.Operator): return SolarData.data["true_north"] is not None def execute(self, context): - props = context.scene.BIMSolarProperties + props = tool.Blender.get_solar_props() for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): if not context.TrueNorth: continue @@ -521,7 +523,7 @@ class ImportLatLong(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMSolarProperties + props = tool.Blender.get_solar_props() site = tool.Ifc.get().by_id(int(props.sites)) if site.RefLatitude and site.RefLongitude: props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude) @@ -536,8 +538,9 @@ class MoveSunPathTo3DCursor(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMSolarProperties - props.sun_path_origin = bpy.context.scene.cursor.location + props = tool.Blender.get_solar_props() + assert context.scene + props.sun_path_origin = context.scene.cursor.location tool.Blender.update_viewport() return {"FINISHED"} @@ -557,7 +560,7 @@ class ViewFromSun(bpy.types.Operator): camera.data.ortho_scale = 100 # The default of 6m is too small context.scene.collection.objects.link(camera) tool.Blender.activate_camera(camera) - props = context.scene.BIMSolarProperties + props = tool.Blender.get_solar_props() props.hour = props.hour # Just to refresh camera position return {"FINISHED"} @@ -568,7 +571,7 @@ class RefreshIFCMaterials(bpy.types.Operator): bl_description = "Refresh the list of IFC materials for mapping" def execute(self, context): - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() ifc_file: ifcopenshell.file ifc_file = tool.Ifc.get() if props.should_load_from_memory else ifcopenshell.open(props.ifc_file) @@ -616,7 +619,7 @@ class UnmapMaterial(bpy.types.Operator): material_index: bpy.props.IntProperty() def execute(self, context): - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() material = props.materials[self.material_index] props.unmap_material(material.name) return {"FINISHED"} @@ -633,7 +636,7 @@ class RADIANCE_OT_select_camera(bpy.types.Operator): return context.object is not None and context.object.type == "CAMERA" def execute(self, context): - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() props.selected_camera = context.object props.use_active_camera = False return {"FINISHED"} @@ -648,7 +651,7 @@ class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper): filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) def execute(self, context): - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() mappings = {} for material in props.materials: @@ -674,7 +677,7 @@ class RADIANCE_OT_import_material_mappings(bpy.types.Operator, ImportHelper): filename_ext = ".json" def execute(self, context): - props = context.scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() props.import_mappings(self.filepath) self.report({"INFO"}, f"Material mappings imported from {self.filepath}") return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index c7b243e367..3e659ea811 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -22,6 +22,7 @@ import tzfpy import json import datetime import bonsai.tool as tool +from typing import TYPE_CHECKING, Literal, Union from math import radians, pi from mathutils import Euler, Vector, Matrix, Quaternion from bpy.props import ( @@ -42,66 +43,78 @@ from bonsai.bim.module.light.decorator import SolarDecorator sun_position = tool.Blender.get_sun_position_addon() with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f: - spectraldb = json.load(f) + spectraldb: dict[str, dict[str, str]] = json.load(f) -def get_sites(self, context): +def get_sites(self: "BIMSolarProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS: if not SolarData.is_loaded: SolarData.load() return SolarData.data["sites"] -def update_latlong(self, context): +def update_latlong(self: "BIMSolarProperties", context: bpy.types.Context) -> None: update_sun_path(self) -def update_hourminute(self, context): +def update_hourminute(self: "BIMSolarProperties", context: bpy.types.Context) -> None: update_sun_path(self) -def update_date(self, context): +def update_date(self: "BIMSolarProperties", context: bpy.types.Context) -> None: update_sun_path(self) -def update_true_north(self, context): +def update_true_north(self: "BIMSolarProperties", context: bpy.types.Context) -> None: update_sun_path(self) -def update_sun_path_size(self, context): +def update_sun_path_size(self: "BIMSolarProperties", context: bpy.types.Context) -> None: update_sun_path(self) -def update_shadow_mode(self, context): +def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -> None: + assert context.scene if self.shadow_mode == "SHADING": update_sun_path(self) context.scene.render.engine = "BLENDER_WORKBENCH" + assert context.scene.display + assert context.scene.display.shading context.scene.display.shading.light = "FLAT" context.scene.display.shading.show_shadows = True context.scene.display.shading.show_object_outline = True context.scene.display.shadow_focus = 1.0 + assert context.scene.view_settings context.scene.view_settings.view_transform = "Standard" # Preserve shading colours space = tool.Blender.get_view3d_space() + assert space space.shading.type = "RENDERED" elif self.shadow_mode == "RENDERING": - if context.scene.sun_pos_properties.sun_object is None: + sun_props = tool.Blender.get_sun_props() + assert sun_props + if sun_props.sun_object is None: bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1)) bpy.ops.object.move_to_collection(collection_index=0) - context.scene.sun_pos_properties.sun_object = bpy.context.active_object + sun_props.sun_object = bpy.context.active_object update_sun_path(self) context.scene.render.engine = "BLENDER_EEVEE_NEXT" + assert context.scene.display + assert context.scene.display.shading context.scene.display.shading.light = "FLAT" context.scene.display.shading.show_shadows = True context.scene.display.shading.show_object_outline = True context.scene.display.shadow_focus = 1.0 + assert context.scene.view_settings context.scene.view_settings.view_transform = "Standard" # Preserve shading colours space = tool.Blender.get_view3d_space() + assert space space.shading.type = "RENDERED" else: space = tool.Blender.get_view3d_space() + assert space space.shading.type = "SOLID" -def update_display_sun_path(self, context): +def update_display_sun_path(self: "BIMSolarProperties", context: bpy.types.Context) -> None: if self.display_sun_path: update_sun_path(self) SolarDecorator.install(bpy.context) @@ -109,20 +122,25 @@ def update_display_sun_path(self, context): SolarDecorator.uninstall() -def update_resolution(self, context): +def update_resolution(self: "RadianceExporterProperties", context: bpy.types.Context) -> None: + assert context.scene context.scene.render.resolution_x = self.radiance_resolution_x context.scene.render.resolution_y = self.radiance_resolution_y -def update_sun_path(self): +def update_sun_path(self: "BIMSolarProperties") -> None: if not SolarData.is_loaded: SolarData.load() if (sun_position := SolarData.data["sun_position"]) is None: return - props = bpy.context.scene.BIMSolarProperties - sun_props = bpy.context.scene.sun_pos_properties + if TYPE_CHECKING: + import sun_position + + props = tool.Blender.get_solar_props() + sun_props = tool.Blender.get_sun_props() + assert sun_props sun_props.sun_distance = self.sun_path_size sun_props.latitude = self.latitude @@ -166,6 +184,8 @@ def update_sun_path(self): props.elevation = elevation props.UTC_zone = zone + assert bpy.context.scene + assert bpy.context.scene.display if sun_vector.z < 0: bpy.context.scene.display.light_direction = mat @ Vector((0, 0, 1)) else: @@ -173,6 +193,7 @@ def update_sun_path(self): SolarData.data["sun"] = sun_vector if obj := bpy.data.objects.get("SunPathCamera"): + assert isinstance(obj.data, bpy.types.Camera) obj.matrix_world.translation = sun_vector z180_quaternion = Quaternion((0, 0, 1), radians(180)) obj.rotation_mode = "QUATERNION" @@ -181,25 +202,31 @@ def update_sun_path(self): class RadianceMaterial(PropertyGroup): - name: StringProperty(name="Name") style_id: StringProperty(name="Style ID") category: StringProperty(name="Category") subcategory: StringProperty(name="Subcategory") is_mapped: BoolProperty(name="Is Mapped", default=False) color: FloatVectorProperty(name="Color", subtype="COLOR", default=(1.0, 1.0, 1.0), min=0.0, max=1.0, size=3) + if TYPE_CHECKING: + style_id: str + category: str + subcategory: str + is_mapped: bool + color: tuple[float, float, float] + class RadianceExporterProperties(PropertyGroup): - def update_output_dir(self, context): + def update_output_dir(self, context) -> None: if self.output_dir: self.output_dir = bpy.path.abspath(self.output_dir) - def update_ifc_file(self, context): + def update_ifc_file(self, context) -> None: if self.ifc_file: self.ifc_file = bpy.path.abspath(self.ifc_file) - def add_material_mapping(self, style_id, style_name): + def add_material_mapping(self, style_id: str, style_name: str) -> RadianceMaterial: item = self.materials.add() item.name = style_name item.style_id = style_id @@ -208,7 +235,7 @@ class RadianceExporterProperties(PropertyGroup): item.color = (1.0, 1.0, 1.0) # Default white return item - def import_mappings(self, filepath): + def import_mappings(self, filepath: str) -> None: with open(filepath, "r") as f: mappings = json.load(f) @@ -225,10 +252,10 @@ class RadianceExporterProperties(PropertyGroup): new_material.subcategory = mapping["subcategory"] new_material.is_mapped = True - def get_material_mapping(self, style_name): + def get_material_mapping(self, style_name: str) -> Union[RadianceMaterial, None]: return next((item for item in self.materials if item.name == style_name), None) - def set_material_mapping(self, style_id, style_name, category, subcategory): + def set_material_mapping(self, style_id: str, style_name: str, category: str, subcategory: str) -> None: item = self.get_material_mapping(style_name) if item: item.category = category @@ -238,14 +265,14 @@ class RadianceExporterProperties(PropertyGroup): self.materials[-1].category = category self.materials[-1].subcategory = subcategory - def get_mappings_dict(self): + def get_mappings_dict(self) -> dict[str, tuple[str, str]]: return { item.style_id: (item.category, item.subcategory) for item in self.materials if item.category and item.subcategory } - def unmap_material(self, style_name): + def unmap_material(self, style_name: str) -> None: item = self.get_material_mapping(style_name) if item: item.category = "" @@ -273,7 +300,7 @@ class RadianceExporterProperties(PropertyGroup): ("Glass", "Glass", ""), ] - def update_material_mapping(self, context): + def update_material_mapping(self, context: bpy.types.Context) -> None: if self.active_material_index >= 0 and self.active_material_index < len(self.materials): active_material = self.materials[self.active_material_index] active_material.category = self.category @@ -285,7 +312,7 @@ class RadianceExporterProperties(PropertyGroup): items=categories, name="Category", description="Material category", update=update_material_mapping ) - def get_subcategories(self, context): + def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS: global SUBCATEGORIES_ENUM_ITEMS if self.category in spectraldb: SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()] @@ -390,6 +417,27 @@ class RadianceExporterProperties(PropertyGroup): poll=lambda self, object: object.type == "CAMERA", ) + if TYPE_CHECKING: + is_exporting: bool + category: str + subcategory: str + materials: bpy.types.bpy_prop_collection_idprop[RadianceMaterial] + active_material_index: int + should_load_from_memory: bool + radiance_resolution_x: int + radiance_resolution_y: int + output_dir: str + ifc_file: str + radiance_quality: Literal["LOW", "MEDIUM", "HIGH"] + radiance_detail: Literal["LOW", "MEDIUM", "HIGH"] + radiance_variability: Literal["LOW", "MEDIUM", "HIGH"] + output_file_name: str + output_file_format: Literal["HDR"] + use_hdr: bool + choose_hdr_image: Literal["Noon"] + use_active_camera: bool + selected_camera: Union[bpy.types.Object, None] + class BIMSolarProperties(PropertyGroup): sites: EnumProperty(items=get_sites, name="Sites") @@ -436,3 +484,23 @@ class BIMSolarProperties(PropertyGroup): description="Displays analemmas and sun position", update=update_display_sun_path, ) + + if TYPE_CHECKING: + sites: str + latitude: float + longitude: float + timezone: str + true_north: float + year: int + month: int + day: int + hour: int + minute: int + sun_position: Vector + sun_path_origin: Vector + sun_path_size: float + azimuth: float + elevation: float + UTC_zone: float + shadow_mode: Literal["NONE", "SHADING", "RENDERING"] + display_sun_path: bool diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index 8cbb637e06..d3068823da 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -18,6 +18,7 @@ import bpy import bonsai.tool as tool +from typing import TYPE_CHECKING from bonsai.bim.module.light.data import SolarData @@ -33,10 +34,9 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): bl_parent_id = "BIM_PT_tab_lighting" def draw(self, context): + assert self.layout layout = self.layout - scene = context.scene - - props = scene.radiance_exporter_properties + props = tool.Blender.get_radiance_exporter_props() if tool.Ifc.get(): row = self.layout.row() @@ -145,11 +145,15 @@ class BIM_PT_solar(bpy.types.Panel): if not SolarData.is_loaded: SolarData.load() + assert self.layout if (sun_position := SolarData.data["sun_position"]) is None: self.layout.label(text="Enable 'Sun Position' Add-on To Continue", icon="ERROR") return - props = context.scene.BIMSolarProperties + if TYPE_CHECKING: + import sun_position + + props = tool.Blender.get_solar_props() if SolarData.data["sites"]: row = self.layout.row(align=True) @@ -159,7 +163,7 @@ class BIM_PT_solar(bpy.types.Panel): row = self.layout.row(align=True) row.label(text="No Sites With Lat/Longs Found", icon="ERROR") - sun_props = context.scene.sun_pos_properties + sun_props = tool.Blender.get_sun_props() row = self.layout.row() row.prop(sun_props, "coordinates", icon="URL") @@ -243,7 +247,9 @@ class BIM_PT_solar(bpy.types.Panel): row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity") elif props.shadow_mode == "RENDERING": row = self.layout.row() - row.prop(context.scene.sun_pos_properties.sun_object.data, "energy", text="Sun Intensity") + sun_props = tool.Blender.get_sun_props() + assert sun_props + row.prop(sun_props.sun_object.data, "energy", text="Sun Intensity") row = self.layout.row(align=True) row.operator("bim.view_from_sun", icon="LIGHT_HEMI") diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index ca45881b55..9ab3789510 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -54,13 +54,15 @@ from typing import ( from collections.abc import Iterable, Callable, Generator, Sequence, Sized if TYPE_CHECKING: + from sun_position.properties import SunPosProperties import bpy.stub_internal.rna_enums as rna_enums from bonsai.bim.prop import BIMProperties, BIMObjectProperties from bonsai.bim.module.attribute.prop import BIMAttributeProperties - from bonsai.bim.module.csv.prop import CsvProperties from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties + from bonsai.bim.module.csv.prop import CsvProperties from bonsai.bim.module.diff.prop import DiffProperties from bonsai.bim.module.group.prop import BIMGroupProperties + from bonsai.bim.module.light.prop import BIMSolarProperties, RadianceExporterProperties T = TypeVar("T") @@ -1495,6 +1497,11 @@ class Blender(bonsai.core.tool.Blender): return sun_position + @classmethod + def get_sun_props(cls) -> Union[SunPosProperties, None]: + assert (scene := bpy.context.scene) + return getattr(scene, "sun_pos_properties", None) + @classmethod def scale_font_size(cls, size): default_dpi = 72 @@ -1694,6 +1701,16 @@ class Blender(bonsai.core.tool.Blender): def get_object_attribute_props(cls, obj: bpy.types.Object) -> BIMAttributeProperties: return obj.BIMAttributeProperties + @classmethod + def get_solar_props(cls) -> BIMSolarProperties: + assert (scene := bpy.context.scene) + return scene.BIMSolarProperties + + @classmethod + def get_radiance_exporter_props(cls) -> RadianceExporterProperties: + assert (scene := bpy.context.scene) + return scene.radiance_exporter_properties + @classmethod def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int: if isinstance(obj, bpy.types.Object): From 58449368fb8be7bdb31ea49b0f29963ac36cac8d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 14:32:13 +0500 Subject: [PATCH 08/32] radiance_exporter_properties -> BIMRadianceExporeterProperies For consistency with other props --- src/bonsai/bonsai/bim/module/light/__init__.py | 4 ++-- src/bonsai/bonsai/tool/blender.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/__init__.py b/src/bonsai/bonsai/bim/module/light/__init__.py index ff44498927..8530b145d9 100644 --- a/src/bonsai/bonsai/bim/module/light/__init__.py +++ b/src/bonsai/bonsai/bim/module/light/__init__.py @@ -54,7 +54,7 @@ classes = ( def register(): - bpy.types.Scene.radiance_exporter_properties = bpy.props.PointerProperty(type=prop.RadianceExporterProperties) + bpy.types.Scene.BIMRadianceExporeterProperies = bpy.props.PointerProperty(type=prop.RadianceExporterProperties) bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties) pyradiance_path = Path(get_pyradiance_path()) bin_path = pyradiance_path / "bin" @@ -65,5 +65,5 @@ def register(): def unregister(): - del bpy.types.Scene.radiance_exporter_properties + del bpy.types.Scene.BIMRadianceExporeterProperies del bpy.types.Scene.BIMSolarProperties diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 9ab3789510..51989b0a21 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1709,7 +1709,7 @@ class Blender(bonsai.core.tool.Blender): @classmethod def get_radiance_exporter_props(cls) -> RadianceExporterProperties: assert (scene := bpy.context.scene) - return scene.radiance_exporter_properties + return scene.BIMRadianceExporeterProperies @classmethod def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int: From 28bee52dffdae278bb1303aa4e4baba123cdcadf Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 14:46:51 +0500 Subject: [PATCH 09/32] Blender 4.5 - deprecate GPUShader constructor Now it should be replaced with `gpu.shader.create_from_info` --- src/bonsai/bonsai/bim/module/light/decorator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/decorator.py b/src/bonsai/bonsai/bim/module/light/decorator.py index 9bdeb961e1..12b4438e09 100644 --- a/src/bonsai/bonsai/bim/module/light/decorator.py +++ b/src/bonsai/bonsai/bim/module/light/decorator.py @@ -104,12 +104,7 @@ class SolarDecorator: self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) self.line_shader.uniform_float("lineWidth", 2.0) - # general shader - self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - vertex_shader = """ - uniform mat4 ModelViewProjectionMatrix; - in vec3 pos; void main() { gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0); @@ -118,8 +113,6 @@ class SolarDecorator: """ fragment_shader = """ - uniform vec4 color; - out vec4 fragColor; void main() { float dist = length(gl_PointCoord - vec2(0.5)); @@ -131,7 +124,14 @@ class SolarDecorator: } """ - self.shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + shader_info = gpu.types.GPUShaderCreateInfo() + shader_info.push_constant("MAT4", "ModelViewProjectionMatrix") + shader_info.vertex_in(0, "VEC3", "pos") + shader_info.vertex_source(vertex_shader) + shader_info.fragment_out(0, "VEC4", "fragColor") + shader_info.push_constant("VEC4", "color") + shader_info.fragment_source(fragment_shader) + self.shader = gpu.shader.create_from_info(shader_info) props = tool.Blender.get_solar_props() From a76af3c2ba2239ed5a82a2988dcd801dca3aad7a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 17:18:30 +0500 Subject: [PATCH 10/32] validate IfcApplication.UR2 - check that both version and app name are not None (263d238f9) Otherwise it will be caught by regular validation checks --- src/ifcopenshell-python/ifcopenshell/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py index 38708d8018..6fd62f538d 100644 --- a/src/ifcopenshell-python/ifcopenshell/validate.py +++ b/src/ifcopenshell-python/ifcopenshell/validate.py @@ -694,7 +694,7 @@ def validate_ifc_applications(f: ifcopenshell.file, logger: Union[Logger, json_l app_name: tuple[str, str] = (inst.ApplicationFullName, inst.Version) app_id: str = inst.ApplicationIdentifier - if app_name is not None: + if all(x is not None for x in app_name): if app_name not in used_names: used_names[app_name] = inst else: From 1a80e3f1a1dd7f76112ff5ff795adb23f2fca275 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 17:29:27 +0500 Subject: [PATCH 11/32] bim.enum_property_search - note on activate_init --- src/bonsai/bonsai/bim/operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 889a1cffd6..9b55b0c0f0 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -977,6 +977,8 @@ class BIM_OT_enum_property_search(bpy.types.Operator): def draw(self, context): # Mandatory to access context.data in update : self.layout.context_pointer_set(name="data", data=self.data) + # NOTE: activate_init don't work with prop_search, so cannot activate field for typing, + # though it would fit perfectly. self.layout.prop_search(self, "dummy_name", self, "collection_names") def execute(self, context): From f684a6058c322e8aadca45a86767c2620af098bc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 18:13:03 +0500 Subject: [PATCH 12/32] Light UI - quickly select current time/pick location from Google Maps Location - https://files.catbox.moe/ew9tgd.png ("Now" and "Pick" buttons) --- .../bonsai/bim/module/light/__init__.py | 2 + .../bonsai/bim/module/light/operator.py | 50 ++++++++++++++++++- src/bonsai/bonsai/bim/module/light/prop.py | 34 ++++++++++--- src/bonsai/bonsai/bim/module/light/ui.py | 10 +++- 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/__init__.py b/src/bonsai/bonsai/bim/module/light/__init__.py index 8530b145d9..5c3c25ae58 100644 --- a/src/bonsai/bonsai/bim/module/light/__init__.py +++ b/src/bonsai/bonsai/bim/module/light/__init__.py @@ -38,6 +38,8 @@ classes = ( operator.MoveSunPathTo3DCursor, operator.RadianceRender, operator.ViewFromSun, + operator.LightPickCoordinates, + operator.LightSetTimeToNow, operator.RefreshIFCMaterials, operator.UnmapMaterial, operator.RADIANCE_OT_select_camera, diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py index 6eb3e2a09c..2dc0834ea2 100644 --- a/src/bonsai/bonsai/bim/module/light/operator.py +++ b/src/bonsai/bonsai/bim/module/light/operator.py @@ -28,8 +28,7 @@ from datetime import datetime import bpy import bonsai.tool as tool from pathlib import Path -from typing import Union, Optional -from collections.abc import Sequence +from typing import Union, TYPE_CHECKING import json import math import time @@ -38,6 +37,7 @@ import ifcopenshell.util.geolocation import webbrowser import ifcopenshell.geom import multiprocessing +import requests from mathutils import Vector from bonsai.bim.module.light.data import SolarData from bpy_extras.io_utils import ExportHelper @@ -565,6 +565,52 @@ class ViewFromSun(bpy.types.Operator): return {"FINISHED"} +class LightPickCoordinates(bpy.types.Operator): + bl_idname = "bim.light_pick_coordinates" + bl_label = "Pick Coordinates" + bl_description = ( + "Open web browser with Google Maps to pick coordinates (Right Mouse Click in maps to copy selected location).\n\n" + "ALT+Click to insert current location based on the current IP-address (using ip-api.com)." + ) + bl_options = {"REGISTER", "UNDO"} + + use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + use_current_location: bool + + def invoke(self, context, event): + if event.alt: + self.use_current_location = True + return self.execute(context) + + def execute(self, context): + props = tool.Blender.get_solar_props() + if not self.use_current_location: + zoom = 13.5 + url = f"https://www.google.com/maps/@{props.latitude},{props.longitude},{zoom}z" + webbrowser.open(url) + return {"FINISHED"} + + response = requests.get("http://ip-api.com/json/") + data = response.json() + props.latitude = data["lat"] + props.longitude = data["lon"] + return {"FINISHED"} + + +class LightSetTimeToNow(bpy.types.Operator): + bl_idname = "bim.light_set_time_to_now" + bl_label = "Now" + bl_description = "Set time to current local time." + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = tool.Blender.get_solar_props() + props.set_from_datetime(datetime.now()) + return {"FINISHED"} + + class RefreshIFCMaterials(bpy.types.Operator): bl_idname = "bim.refresh_ifc_materials" bl_label = "Refresh IFC Materials" diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index 3e659ea811..91a7d6b522 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -41,6 +41,7 @@ from bonsai.bim.module.light.data import SolarData from bonsai.bim.module.light.decorator import SolarDecorator sun_position = tool.Blender.get_sun_position_addon() +now = datetime.datetime.now() with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f: spectraldb: dict[str, dict[str, str]] = json.load(f) @@ -441,15 +442,27 @@ class RadianceExporterProperties(PropertyGroup): class BIMSolarProperties(PropertyGroup): sites: EnumProperty(items=get_sites, name="Sites") - latitude: FloatProperty(name="Latitude", min=-90, max=90, update=update_latlong) - longitude: FloatProperty(name="Longitude", min=-180, max=180, update=update_latlong) + latitude: FloatProperty( + name="Latitude", + min=-90, + max=90, + update=update_latlong, + default=-33.865143, + ) + longitude: FloatProperty( + name="Longitude", + min=-180, + max=180, + update=update_latlong, + default=151.209900, + ) timezone: StringProperty(name="Timezone", default="Etc/GMT") true_north: FloatProperty(name="True North", min=-180, max=180, update=update_true_north) - year: IntProperty(name="Year", min=1, max=9999, default=2025, update=update_date) - month: IntProperty(name="Month", min=1, max=12, default=1, update=update_date) - day: IntProperty(name="Date", min=1, max=31, default=1, update=update_date) - hour: IntProperty(name="Hour", min=0, max=23, default=12, update=update_hourminute) - minute: IntProperty(name="Minute", min=0, max=59, update=update_hourminute) + year: IntProperty(name="Year", min=1, max=9999, default=now.year, update=update_date) + month: IntProperty(name="Month", min=1, max=12, default=now.month, update=update_date) + day: IntProperty(name="Date", min=1, max=31, default=now.day, update=update_date) + hour: IntProperty(name="Hour", min=0, max=23, default=now.hour, update=update_hourminute) + minute: IntProperty(name="Minute", min=0, max=59, default=now.minute, update=update_hourminute) sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0)) sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(0, 0, 0)) sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path_size) @@ -504,3 +517,10 @@ class BIMSolarProperties(PropertyGroup): UTC_zone: float shadow_mode: Literal["NONE", "SHADING", "RENDERING"] display_sun_path: bool + + def set_from_datetime(self, dt: datetime.datetime) -> None: + self.year = dt.year + self.month = dt.month + self.day = dt.day + self.hour = dt.hour + self.minute = dt.minute diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index d3068823da..fb62ad5bab 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -165,7 +165,11 @@ class BIM_PT_solar(bpy.types.Panel): sun_props = tool.Blender.get_sun_props() - row = self.layout.row() + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.operator("bim.light_pick_coordinates", icon="URL", text="Pick") + + row = self.layout.row(align=True) row.prop(sun_props, "coordinates", icon="URL") row = self.layout.row(align=True) row.prop(props, "latitude") @@ -176,6 +180,10 @@ class BIM_PT_solar(bpy.types.Panel): if SolarData.data["true_north"] is not None: row.operator("bim.import_true_north", icon="IMPORT", text="") + row = self.layout.row() + row.alignment = "RIGHT" + row.operator("bim.light_set_time_to_now", icon="TIME", text="Now") + row = self.layout.row() row.prop(props, "year") row = self.layout.row(align=True) From c1e6e7d7363d169deda0e4cc056e8f7b547cd2f3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 18:43:59 +0500 Subject: [PATCH 13/32] light module true_north prop - use Blender angle subtype --- src/bonsai/bonsai/bim/module/light/decorator.py | 10 +++++----- src/bonsai/bonsai/bim/module/light/operator.py | 3 ++- src/bonsai/bonsai/bim/module/light/prop.py | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/decorator.py b/src/bonsai/bonsai/bim/module/light/decorator.py index 12b4438e09..77cd20ddbd 100644 --- a/src/bonsai/bonsai/bim/module/light/decorator.py +++ b/src/bonsai/bonsai/bim/module/light/decorator.py @@ -22,7 +22,7 @@ import gpu import bmesh import bonsai.tool as tool from bpy.types import SpaceView3D -from math import radians +from math import radians, degrees from mathutils import Vector, Matrix from gpu_extras.batch import batch_for_shader from bpy_extras.view3d_utils import location_3d_to_region_2d @@ -72,7 +72,7 @@ class SolarDecorator: self.draw_text_at_position(context, f"{props.hour:02}:{props.minute:02}", location) self.tn_angle = props.true_north - angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z") + angle = Matrix.Rotation(props.true_north, 4, "Z") grid_north_p = origin @ angle @ (Vector((0, 0.8, 0)) * props.sun_path_size) self.draw_text_at_position(context, "True North", grid_north_p) @@ -155,13 +155,13 @@ class SolarDecorator: # True north self.tn_angle = props.true_north points = [Vector((0, 0, 0)), Vector((0, 0.75, 0))] - angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z") + angle = Matrix.Rotation(self.tn_angle, 4, "Z") points = [origin @ angle @ (v * props.sun_path_size) for v in points] self.draw_batch("POINTS", points, decorator_color_special) verts = [Vector((0, 0, 0)), Vector((0, 0.75, 0))] edges = [[0, 1]] - angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z") + angle = Matrix.Rotation(self.tn_angle, 4, "Z") verts = [origin @ angle @ (v * props.sun_path_size) for v in verts] self.draw_batch("LINES", verts, decorator_color_special, edges) @@ -174,7 +174,7 @@ class SolarDecorator: arc_start = Vector((0, 0.25, 0)) * props.sun_path_size arc_end = angle @ arc_start - angle_half = Matrix.Rotation(radians(self.tn_angle / 2), 4, "Z") + angle_half = Matrix.Rotation(self.tn_angle / 2, 4, "Z") arc_mid = angle_half @ arc_start arc_segments = tool.Cad.create_arc_segments( pts=[origin @ v for v in [arc_start, arc_mid, arc_end]], num_verts=12, make_edges=True diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py index 2dc0834ea2..40a68df33b 100644 --- a/src/bonsai/bonsai/bim/module/light/operator.py +++ b/src/bonsai/bonsai/bim/module/light/operator.py @@ -38,6 +38,7 @@ import webbrowser import ifcopenshell.geom import multiprocessing import requests +from math import radians from mathutils import Vector from bonsai.bim.module.light.data import SolarData from bpy_extras.io_utils import ExportHelper @@ -512,7 +513,7 @@ class ImportTrueNorth(bpy.types.Operator): if not context.TrueNorth: continue value = context.TrueNorth.DirectionRatios - props.true_north = ifcopenshell.util.geolocation.yaxis2angle(*value[:2]) + props.true_north = radians(ifcopenshell.util.geolocation.yaxis2angle(*value[:2])) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index 91a7d6b522..ef674582c6 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -151,7 +151,7 @@ def update_sun_path(self: "BIMSolarProperties") -> None: sun_props.day = self.day sun_props.time = self.hour + (self.minute / 60) # Preserve IFC sign convention - sun_props.north_offset = radians(self.true_north * -1) + sun_props.north_offset = self.true_north * -1 props.timezone = tzfpy.get_tz(props.longitude, props.latitude) timezone = pytz.timezone(props.timezone) @@ -457,7 +457,7 @@ class BIMSolarProperties(PropertyGroup): default=151.209900, ) timezone: StringProperty(name="Timezone", default="Etc/GMT") - true_north: FloatProperty(name="True North", min=-180, max=180, update=update_true_north) + true_north: FloatProperty(name="True North", min=-pi, max=pi, subtype="ANGLE", update=update_true_north) year: IntProperty(name="Year", min=1, max=9999, default=now.year, update=update_date) month: IntProperty(name="Month", min=1, max=12, default=now.month, update=update_date) day: IntProperty(name="Date", min=1, max=31, default=now.day, update=update_date) From 8f7b965a58bcbc48ac9b75b641dfd6b6245577af Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 18:41:15 +0500 Subject: [PATCH 14/32] Light UI - add our own 'coordinates' property To ensure when it's changed - latitude and longitude update will follow. Previously we were just displaying sun position addon's `coordinates' prop. --- src/bonsai/bonsai/bim/module/light/prop.py | 58 +++++++++++++--------- src/bonsai/bonsai/bim/module/light/ui.py | 2 +- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index ef674582c6..54e5f6f73c 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -53,23 +53,26 @@ def get_sites(self: "BIMSolarProperties", context: bpy.types.Context) -> tool.Bl return SolarData.data["sites"] +def update_coordinates(self: "BIMSolarProperties", context: bpy.types.Context) -> None: + # We define our own `coordinates` property just to ensure changing it would update + # all other props. + # But we still need to set original prop value and retrieve it to ensure coordinate + # was parsed correctly. + sun_props = tool.Blender.get_sun_props() + assert sun_props + sun_props.coordinates = self.coordinates + self["coordinates"] = sun_props.coordinates + self["longitude"] = sun_props.longitude + self["latitude"] = sun_props.latitude + update_sun_path(self) + + def update_latlong(self: "BIMSolarProperties", context: bpy.types.Context) -> None: - update_sun_path(self) - - -def update_hourminute(self: "BIMSolarProperties", context: bpy.types.Context) -> None: - update_sun_path(self) - - -def update_date(self: "BIMSolarProperties", context: bpy.types.Context) -> None: - update_sun_path(self) - - -def update_true_north(self: "BIMSolarProperties", context: bpy.types.Context) -> None: - update_sun_path(self) - - -def update_sun_path_size(self: "BIMSolarProperties", context: bpy.types.Context) -> None: + sun_props = tool.Blender.get_sun_props() + assert sun_props + sun_props.longitude = sun_props.longitude + sun_props.latitude = sun_props.latitude + self["coordinates"] = sun_props.coordinates update_sun_path(self) @@ -129,7 +132,7 @@ def update_resolution(self: "RadianceExporterProperties", context: bpy.types.Con context.scene.render.resolution_y = self.radiance_resolution_y -def update_sun_path(self: "BIMSolarProperties") -> None: +def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context, None] = None) -> None: if not SolarData.is_loaded: SolarData.load() @@ -442,6 +445,12 @@ class RadianceExporterProperties(PropertyGroup): class BIMSolarProperties(PropertyGroup): sites: EnumProperty(items=get_sites, name="Sites") + coordinates: StringProperty( + name="Coordinates", + description="Latitude and longitude on Earth. Coordinates can be directly entered from an online map", + update=update_coordinates, + default="33°51′54.51″S 151°12′35.64″E", + ) latitude: FloatProperty( name="Latitude", min=-90, @@ -457,15 +466,15 @@ class BIMSolarProperties(PropertyGroup): default=151.209900, ) timezone: StringProperty(name="Timezone", default="Etc/GMT") - true_north: FloatProperty(name="True North", min=-pi, max=pi, subtype="ANGLE", update=update_true_north) - year: IntProperty(name="Year", min=1, max=9999, default=now.year, update=update_date) - month: IntProperty(name="Month", min=1, max=12, default=now.month, update=update_date) - day: IntProperty(name="Date", min=1, max=31, default=now.day, update=update_date) - hour: IntProperty(name="Hour", min=0, max=23, default=now.hour, update=update_hourminute) - minute: IntProperty(name="Minute", min=0, max=59, default=now.minute, update=update_hourminute) + true_north: FloatProperty(name="True North", min=-pi, max=pi, subtype="ANGLE", update=update_sun_path) + year: IntProperty(name="Year", min=1, max=9999, default=now.year, update=update_sun_path) + month: IntProperty(name="Month", min=1, max=12, default=now.month, update=update_sun_path) + day: IntProperty(name="Date", min=1, max=31, default=now.day, update=update_sun_path) + hour: IntProperty(name="Hour", min=0, max=23, default=now.hour, update=update_sun_path) + minute: IntProperty(name="Minute", min=0, max=59, default=now.minute, update=update_sun_path) sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0)) sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(0, 0, 0)) - sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path_size) + sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path) azimuth: FloatProperty(name="Azimuth") elevation: FloatProperty(name="Elevation") UTC_zone: FloatProperty(name="UTC Zone") @@ -500,6 +509,7 @@ class BIMSolarProperties(PropertyGroup): if TYPE_CHECKING: sites: str + coordinates: str latitude: float longitude: float timezone: str diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index fb62ad5bab..c5739335c3 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -170,7 +170,7 @@ class BIM_PT_solar(bpy.types.Panel): row.operator("bim.light_pick_coordinates", icon="URL", text="Pick") row = self.layout.row(align=True) - row.prop(sun_props, "coordinates", icon="URL") + row.prop(props, "coordinates", icon="URL") row = self.layout.row(align=True) row.prop(props, "latitude") row.prop(props, "longitude") From 623997b40d2a1e4e037aca2758eebd7ddfa8b06c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 18:54:06 +0500 Subject: [PATCH 15/32] Revert unintended prop_with_search change (98c479f1b) --- src/bonsai/bonsai/bim/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index b4e3ba4f79..31be04f874 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -314,7 +314,7 @@ def prop_with_search( row = layout.row(align=True) row.prop(data, prop_name, **kwargs) try: - if len(get_enum_items(data, prop_name, original_operator_path=original_operator_path)) > 0: + if len(get_enum_items(data, prop_name, original_operator_path=original_operator_path)) > 10: # Magick courtesy of https://blender.stackexchange.com/a/203443/86891 row.context_pointer_set(name="data", data=data) op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM") From 4574351817be8889f5b6e438dccaea2a4529cbcd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Jun 2025 19:05:12 +0500 Subject: [PATCH 16/32] Light UI - detect Sun Position addon activation/deactivation more reliably --- src/bonsai/bonsai/bim/module/light/ui.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index c5739335c3..35cf36d306 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -146,10 +146,17 @@ class BIM_PT_solar(bpy.types.Panel): SolarData.load() assert self.layout - if (sun_position := SolarData.data["sun_position"]) is None: + + # Props are more reliable as they'll come and go regardless of Data update. + if (sun_props := tool.Blender.get_sun_props()) is None: self.layout.label(text="Enable 'Sun Position' Add-on To Continue", icon="ERROR") return + sun_position = SolarData.data["sun_position"] + if sun_position is None: + # There are props, so there must be an addon. + SolarData.data["sun_position"] = SolarData.sun_position() + if TYPE_CHECKING: import sun_position @@ -163,8 +170,6 @@ class BIM_PT_solar(bpy.types.Panel): row = self.layout.row(align=True) row.label(text="No Sites With Lat/Longs Found", icon="ERROR") - sun_props = tool.Blender.get_sun_props() - row = self.layout.row(align=True) row.alignment = "RIGHT" row.operator("bim.light_pick_coordinates", icon="URL", text="Pick") From 50d3cab1ec5849986921f63c14405e9296496ab6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 12:49:29 +0500 Subject: [PATCH 17/32] Fix regression with thumbnails update #6839 (be1836f) --- src/bonsai/bonsai/bim/module/model/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 31efc65e9f..3ce3319acf 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -610,7 +610,7 @@ class LoadTypeThumbnails(bpy.types.Operator): while queue: # if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation element = queue.pop() - tool.Model.mark_thumbnail_for_update(element) + tool.Model.update_thumbnail_for_element(element) return {"FINISHED"} From fcaeb920ec4004aef04d348753a320ec5f302002 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 12:57:34 +0500 Subject: [PATCH 18/32] typing --- src/bonsai/bonsai/bim/module/drawing/data.py | 17 +++++++----- src/bonsai/bonsai/bim/module/model/ui.py | 3 +++ .../bonsai/bim/module/model/workspace.py | 17 +++++++----- src/bonsai/bonsai/bim/module/unit/prop.py | 1 - src/bonsai/bonsai/bim/module/unit/ui.py | 17 +++++++++++- src/bonsai/bonsai/core/unit.py | 26 +++++++++---------- src/bonsai/bonsai/tool/unit.py | 2 +- .../ifcopenshell/util/unit.py | 1 + 8 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index cfe1492247..65633748ca 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -285,7 +285,7 @@ class DecoratorData: return thickness @classmethod - def get_section_markers_display_data(cls, obj): + def get_section_markers_display_data(cls, obj: bpy.types.Object) -> Union[dict[str, Any], None]: """used by IfcAnnotations with ObjectType = "SECTION" """ result = cls.data.get(obj.name, None) if result is not None: @@ -327,10 +327,11 @@ class DecoratorData: return display_data @classmethod - def get_text_data(cls, obj: bpy.types.Object) -> dict: + def get_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]: """used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n returns font size in mm for current ifc text object""" element = tool.Ifc.get_entity(obj) + assert element props = tool.Drawing.get_text_props(obj) # getting font size pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {} @@ -371,16 +372,18 @@ class DecoratorData: return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at} @classmethod - def get_dimension_data(cls, obj): + def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]: """used by Ifc Annotations with ObjectType: DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS """ element = tool.Ifc.get_entity(obj) + assert element dimension_style = "arrow" fill_bg = False classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes") if classes: + assert type(classes) is str classes_split = classes.lower().split() if "oblique" in classes_split: dimension_style = "oblique" @@ -406,15 +409,17 @@ class DecoratorData: } @classmethod - def get_fall_data(cls, obj): + def get_fall_data(cls, obj: bpy.types.Object) -> dict[str, Union[str, None]]: object_type = None if element := tool.Ifc.get_entity(obj): object_type = ifcopenshell.util.element.get_predefined_type(element) return {"object_type": object_type} @classmethod - def get_symbol_data(cls, obj): - return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj)) + def get_symbol_data(cls, obj: bpy.types.Object) -> Union[str, None]: + element = tool.Ifc.get_entity(obj) + assert element + return tool.Drawing.get_annotation_symbol(element) @classmethod def object_decorators(cls, handler): diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index f8eb50a38e..000a9a46f4 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -36,6 +36,7 @@ from bonsai.bim.module.model.stair import regenerate_stair_mesh from bonsai.bim.module.model.railing import update_railing_modifier_bmesh from bonsai.bim.module.model.roof import update_roof_modifier_bmesh from collections.abc import Iterable +from typing import Any class BIM_MT_type_manager_menu(bpy.types.Menu): @@ -95,6 +96,7 @@ class LaunchTypeManager(bpy.types.Operator): return context.window_manager.invoke_props_dialog(self, width=550, title="Type Manager", confirm_text="Close") def draw(self, context): + assert self.layout props = tool.Model.get_model_props() row = self.layout.row(align=True) text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type'] or 'Types'}" @@ -135,6 +137,7 @@ class LaunchTypeManager(bpy.types.Operator): flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True) + relating_type: dict[str, Any] for relating_type in AuthoringData.data["paginated_relating_types"]: outer_col = flow.column() box = outer_col.box() diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 90a9849e30..618d0db95b 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -29,7 +29,7 @@ from bpy.types import WorkSpaceTool, Menu from bonsai.bim.module.model.data import AuthoringData, ItemData from bonsai.bim.module.system.data import PortData from bonsai.bim.module.model.prop import get_ifc_class -from typing import Optional, Union +from typing import Optional, Union, Any from functools import partial @@ -506,15 +506,17 @@ class CreateObjectUI: cls.draw_type_manager_launcher(context) @classmethod - def draw_container_info(cls, context): + def draw_container_info(cls, context: bpy.types.Context) -> None: text = AuthoringData.data["default_container"] + assert context.region if context.region.type == "UI": text = f"Container: {text}" cls.layout.row(align=True).label(text=text, icon="OUTLINER_COLLECTION") @classmethod - def draw_type_manager_launcher(cls, context): + def draw_type_manager_launcher(cls, context: bpy.types.Context) -> None: + assert context.region ui_context = context.region.type props = tool.Model.get_model_props() row = cls.layout.row(align=True) @@ -568,7 +570,8 @@ class CreateObjectUI: op.ifc_element_type = AuthoringData.data["ifc_element_type"] @classmethod - def draw_add_object(cls, context): + def draw_add_object(cls, context: bpy.types.Context) -> None: + assert context.region ui_context = str(context.region.type) row = cls.layout.row(align=True) if AuthoringData.data["relating_type_id"]: @@ -580,7 +583,8 @@ class CreateObjectUI: row.label(text="No Construction Type", icon="FILE_3D") @classmethod - def draw_add_object_parameters(cls, context): + def draw_add_object_parameters(cls, context: bpy.types.Context) -> None: + assert context.region ui_context = str(context.region.type) row = cls.layout.row(align=True) if not AuthoringData.data["relating_type_id"]: @@ -633,7 +637,8 @@ class CreateObjectUI: row.prop(data=cls.props, property="rl_mode", text="RL Mode" if ui_context != "TOOL_HEADER" else "RL") @classmethod - def draw_thumbnail(cls, context): + def draw_thumbnail(cls, context: bpy.types.Context) -> None: + assert context.region ui_context = context.region.type row = cls.layout.row(align=True) if not AuthoringData.data["ifc_element_type"]: diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index f0f04e16ae..a335532294 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -52,7 +52,6 @@ def get_named_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) class Unit(PropertyGroup): - name: StringProperty(name="Name") unit_type: StringProperty(name="Unit Type") is_assigned: BoolProperty(name="Is Assigned") ifc_class: StringProperty(name="IFC Class") diff --git a/src/bonsai/bonsai/bim/module/unit/ui.py b/src/bonsai/bonsai/bim/module/unit/ui.py index f7b55a96e3..27f4f78a7e 100644 --- a/src/bonsai/bonsai/bim/module/unit/ui.py +++ b/src/bonsai/bonsai/bim/module/unit/ui.py @@ -16,11 +16,17 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bonsai.bim.helper import bonsai.tool as tool +import bpy from bpy.types import Panel, UIList from bonsai.bim.helper import prop_with_search from bonsai.bim.module.unit.data import UnitsData +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.unit.prop import BIMUnitProperties, Unit class BIM_PT_units(Panel): @@ -102,7 +108,16 @@ class BIM_PT_units(Panel): class BIM_UL_units(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMUnitProperties, + item: Unit, + icon_: int, + active_data, + active_propname, + ) -> None: props = tool.Unit.get_unit_props() if item: icon = "MOD_MESHDEFORM" diff --git a/src/bonsai/bonsai/core/unit.py b/src/bonsai/bonsai/core/unit.py index e9f002b95c..a075c9c5e5 100644 --- a/src/bonsai/bonsai/core/unit.py +++ b/src/bonsai/bonsai/core/unit.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: import bonsai.tool as tool -def assign_scene_units(ifc: tool.Ifc, unit: tool.Unit) -> None: +def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None: if unit.is_scene_unit_metric(): prefix = unit.get_scene_unit_si_prefix("LENGTHUNIT") lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=prefix) @@ -44,66 +44,66 @@ def assign_scene_units(ifc: tool.Ifc, unit: tool.Unit) -> None: ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit, planeangleunit]) -def assign_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None: +def assign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: ifc.run("unit.assign_unit", units=[unit]) unit_tool.import_units() -def unassign_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None: +def unassign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: ifc.run("unit.unassign_unit", units=[unit]) unit_tool.import_units() -def load_units(unit: tool.Unit) -> None: +def load_units(unit: type[tool.Unit]) -> None: unit.import_units() unit.enable_editing_units() -def disable_unit_editing_ui(unit: tool.Unit) -> None: +def disable_unit_editing_ui(unit: type[tool.Unit]) -> None: unit.disable_editing_units() -def remove_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None: +def remove_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: ifc.run("unit.remove_unit", unit=unit) unit_tool.import_units() -def add_monetary_unit(ifc: tool.Ifc, unit: tool.Unit) -> ifcopenshell.entity_instance: +def add_monetary_unit(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> ifcopenshell.entity_instance: result = ifc.run("unit.add_monetary_unit") unit.import_units() return result -def add_si_unit(ifc: tool.Ifc, unit: tool.Unit, unit_type: str) -> ifcopenshell.entity_instance: +def add_si_unit(ifc: type[tool.Ifc], unit: type[tool.Unit], unit_type: str) -> ifcopenshell.entity_instance: result = ifc.run("unit.add_si_unit", unit_type=unit_type) unit.import_units() return result def add_context_dependent_unit( - ifc: tool.Ifc, unit: tool.Unit, unit_type: str, name: str + ifc: type[tool.Ifc], unit: type[tool.Unit], unit_type: str, name: str ) -> ifcopenshell.entity_instance: result = ifc.run("unit.add_context_dependent_unit", unit_type=unit_type, name=name) unit.import_units() return result -def add_conversion_based_unit(ifc: tool.Ifc, unit: tool.Unit, name: str) -> ifcopenshell.entity_instance: +def add_conversion_based_unit(ifc: type[tool.Ifc], unit: type[tool.Unit], name: str) -> ifcopenshell.entity_instance: result = ifc.run("unit.add_conversion_based_unit", name=name) unit.import_units() return result -def enable_editing_unit(unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None: +def enable_editing_unit(unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: unit_tool.set_active_unit(unit) unit_tool.import_unit_attributes(unit) -def disable_editing_unit(unit: tool.Unit) -> None: +def disable_editing_unit(unit: type[tool.Unit]) -> None: unit.clear_active_unit() -def edit_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None: +def edit_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: attributes = unit_tool.export_unit_attributes() if unit_tool.is_unit_class(unit, "IfcMonetaryUnit"): ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes) diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index f296a29500..be5bc0fe5a 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -125,7 +125,7 @@ class Unit(bonsai.core.tool.Unit): props = tool.Unit.get_unit_props() props.units.clear() - units = [] + units: list[ifcopenshell.entity_instance] = [] for unit_class in ["IfcDerivedUnit", "IfcMonetaryUnit", "IfcNamedUnit"]: units += tool.Ifc.get().by_type(unit_class) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index b7e91c798b..6e78d73546 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -695,6 +695,7 @@ def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUN if not (projects := ifc_file.by_type("IfcProject")) or not (units := projects[0].UnitsInContext): return 1 unit_scale = 1 + unit: ifcopenshell.entity_instance for unit in units.Units: if not hasattr(unit, "UnitType") or unit.UnitType != unit_type: continue From c333535ae5211a379d4fb60159ed2157df43dc15 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 13:06:37 +0500 Subject: [PATCH 19/32] Blender 4.5 - prevent infinite loading of previews Related - https://projects.blender.org/blender/blender/issues/140903 --- src/bonsai/bonsai/tool/model.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index b88d9d166a..535bd16b59 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1063,12 +1063,15 @@ class Model(bonsai.core.tool.Model): return # Already processed assert isinstance(obj, bpy.types.Object) - obj.asset_generate_preview() - while not obj.preview: - pass + # Since Blender 4.5 have to use `preview_ensure` instead of `asset_generate_preview`, see #6839. + obj.preview_ensure() - # if object has .data we can use default blender .asset_generate_preview() - if not obj.data: + if obj.data: + # If object has .data we can use default Blender preview. + # No need to preview to update, Blender will do it in background, + # `preview.icon_id` doesn't change after `asset_generate_preview()`. + obj.asset_generate_preview() + else: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) size = 128 img = Image.new("RGBA", (size, size)) From b3778facdacabb5fd3d3d2a50aca1f29a4903b7d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 13:15:50 +0500 Subject: [PATCH 20/32] Fix core drawing tests after be65774 --- src/bonsai/bonsai/core/tool.py | 2 +- src/bonsai/test/core/test_drawing.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index c88b16cf9e..0edc1559ca 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -305,7 +305,7 @@ class Drawing: def copy_representation(cls, source, dest): pass def create_annotation_context(cls, target_view, object_type=None): pass def create_annotation_object(cls, drawing, object_type): pass - def create_camera(cls, name, matrix, location_hint): pass + def create_camera(cls, name, matrix, location_hint, target_view): pass def create_svg_schedule(cls, schedule): pass def create_svg_sheet(cls, document, titleblock): pass def delete_collection(cls, collection): pass diff --git a/src/bonsai/test/core/test_drawing.py b/src/bonsai/test/core/test_drawing.py index d0f5902572..ee35fe1a5d 100644 --- a/src/bonsai/test/core/test_drawing.py +++ b/src/bonsai/test/core/test_drawing.py @@ -332,11 +332,11 @@ class TestDisableEditingDrawings: class TestAddDrawing: - def test_run(self, ifc, collector, drawing): + def test_run(self, ifc: Prophecy, collector: Prophecy, drawing: Prophecy): drawing.generate_drawing_name("target_view", "location_hint").should_be_called().will_return("drawing_name") drawing.ensure_unique_drawing_name("drawing_name").should_be_called().will_return("name") drawing.generate_drawing_matrix("target_view", "location_hint").should_be_called().will_return("matrix") - drawing.create_camera("name", "matrix", "location_hint").should_be_called().will_return("obj") + drawing.create_camera("name", "matrix", "location_hint", "target_view").should_be_called().will_return("obj") drawing.get_body_context().should_be_called().will_return("context") drawing.run_root_assign_class( obj="obj", From df69ca27da57fdeeeb9d4435dd74a66790773c0f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 13:35:08 +0500 Subject: [PATCH 21/32] Remove duplicated active_dictionary prop (22f10e5) --- src/bonsai/bonsai/bim/module/bsdd/prop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index 1895750387..e1e3f781f2 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -111,7 +111,7 @@ class BSDDPset(PropertyGroup): class BIMBSDDProperties(PropertyGroup): - active_dictionary: StringProperty(name="Active Dictionary") + # TODO: `active_dictionary` is not used anywhere? active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary") active_uri: StringProperty(name="Active URI") dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary) From 4a69df1f6bf750dda47e23579a60cf021ad49a87 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 13:37:05 +0500 Subject: [PATCH 22/32] Classification source - automatically update on changes in bsdd ui --- src/bonsai/bonsai/bim/module/bsdd/prop.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index e1e3f781f2..ea3260909d 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -20,6 +20,7 @@ import bpy import bonsai.tool as tool from bpy.types import PropertyGroup from bonsai.bim.module.bsdd.data import BSDDData +from bonsai.bim.module.classification.data import ClassificationsData from bonsai.bim.prop import Attribute, StrProperty from bpy.props import ( PointerProperty, @@ -42,6 +43,21 @@ def get_active_dictionary(self, context): def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None: BSDDData.data["active_dictionary"] = BSDDData.active_dictionary() + if ClassificationsData.is_loaded: + props = tool.Classification.get_classification_props() + # Preserve original enum value. + classification_source = props.classification_source + ClassificationsData.data["classification_source"] = ClassificationsData.classification_source() + + # Try to restore enum value. + if "classification_source" not in props: + # It's already on the default value, nothing to restore. + return + try: + props.classification_source = classification_source + except TypeError: + # Item is no longer active and not present in enum, fallback to the default. + del props["classification_source"] def update_is_selected(self: "BSDDProperty", context: bpy.types.Context) -> None: From 097e03d6f5facdafac720993d0263add098b63a9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 13:53:18 +0500 Subject: [PATCH 23/32] Fix TestGenerateSheetIdentification (b20997d) --- src/bonsai/test/tool/test_drawing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 35d771aef3..1624945792 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -492,7 +492,7 @@ class TestGenerateSheetIdentification(NewFile): ifc = ifcopenshell.file() tool.Ifc.set(ifc) assert subject.generate_sheet_identification() == "A01" - document = ifc.createIfcDocumentInformation() + document = ifc.createIfcDocumentInformation(Scope="SHEET") assert subject.generate_sheet_identification() == "A02" From 705a924f41588a6f1dcc34e51c982e5d2c31d2a0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 13:53:39 +0500 Subject: [PATCH 24/32] Fix context's TestImportAttributes after e4a50b3 --- src/bonsai/test/tool/test_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/test/tool/test_context.py b/src/bonsai/test/tool/test_context.py index 7c3c5837d8..7151952d79 100644 --- a/src/bonsai/test/tool/test_context.py +++ b/src/bonsai/test/tool/test_context.py @@ -68,7 +68,7 @@ class TestImportAttributes(test.bim.bootstrap.NewFile): assert props.context_attributes["TargetScale"].float_value == 0.5 assert props.context_attributes["TargetView"].enum_value == "NOTDEFINED" assert props.context_attributes["UserDefinedTargetView"].string_value == "UserDefinedTargetView" - assert not props.context_attributes["Precision"] + assert "Precision" not in props.context_attributes def test_importing_twice(self): ifc = ifcopenshell.file() From cd73f80894c4faf921dfe9c9728e5f5b401f7259 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 13:59:05 +0500 Subject: [PATCH 25/32] Fix tool test_drawing after 992ad8e --- src/bonsai/test/tool/test_drawing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 1624945792..cc1b73aa0e 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -832,7 +832,7 @@ class TestUpdateTextValue(NewFile): bpy.ops.bim.edit_text() annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes") assert "title" in annotation_classes - assert DecoratorData.get_ifc_text_data(obj)["FontSize"] == 7.0 + assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0 def test_add_second_literal(self, setup=True): if setup: @@ -869,7 +869,7 @@ class TestUpdateTextValue(NewFile): annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes") assert props.font_size == "7.0" assert "title" in annotation_classes - assert DecoratorData.get_ifc_text_data(obj)["FontSize"] == 7.0 + assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0 # test second literal is present assert props.literals[1].attributes["Literal"].string_value == "test_value" From 82d020b20de69bec8b7c13a299a87d40619b1006 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 17:18:58 +0500 Subject: [PATCH 26/32] bim light tests - fix after f684a6058 Since defaults were updated --- src/bonsai/test/bim/feature/light.feature | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bonsai/test/bim/feature/light.feature b/src/bonsai/test/bim/feature/light.feature index 04559e4729..f92ff281a6 100644 --- a/src/bonsai/test/bim/feature/light.feature +++ b/src/bonsai/test/bim/feature/light.feature @@ -19,6 +19,8 @@ Scenario: Changing the date And I look at the "Solar Access / Shadow" panel When I set the "Year" property to "2024" And I set the "Date" property to "3" + And I set the "Latitude" property to "0" + And I set the "Longitude" property to "0" Then I see "Sunrise: 06:00:31" Scenario: Changing the time @@ -26,6 +28,8 @@ Scenario: Changing the time And I look at the "Solar Access / Shadow" panel When I set the "Hour" property to "13" And I set the "Minute" property to "30" + And I set the "Latitude" property to "0" + And I set the "Longitude" property to "0" Then I see "Local Time: 13:30:00" Scenario: Automatic timezone detection based on lat / long From 845e03461443d6070d8fc574916f0d136b3d6aad Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 18:17:08 +0500 Subject: [PATCH 27/32] test_feature - fix non-unique function name --- src/bonsai/test/bim/test_feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 41e53882a0..aae89f9c76 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -1561,7 +1561,7 @@ def the_file_name_should_contain_value(name, value): @then(parsers.parse('the file "{name}" should not contain "{value}"')) -def the_file_name_should_contain_value(name, value): +def the_file_name_should_not_contain_value(name, value): name = replace_variables(name) with open(name, "r") as f: content = f.read() From 47b077b55528328daf41e8f73dd3a0198c3361b3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 18:31:13 +0500 Subject: [PATCH 28/32] Fix sheets related tests --- src/bonsai/bonsai/bim/module/drawing/sheeter.py | 2 +- src/bonsai/test/bim/feature/drawing.feature | 10 +++++----- src/bonsai/test/tool/test_drawing.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/sheeter.py b/src/bonsai/bonsai/bim/module/drawing/sheeter.py index 47bf3181d4..f73b71b64f 100644 --- a/src/bonsai/bonsai/bim/module/drawing/sheeter.py +++ b/src/bonsai/bonsai/bim/module/drawing/sheeter.py @@ -66,7 +66,7 @@ class SheetBuilder: view = ET.SubElement(root, "g") view.attrib["data-type"] = "titleblock" titleblock = ET.SubElement(view, "image") - titleblock.attrib["xlink:href"] = os.path.relpath(titleblock_path, sheet_dir) + titleblock.attrib["xlink:href"] = Path(os.path.relpath(titleblock_path, sheet_dir)).as_posix() titleblock.attrib["x"] = "0" titleblock.attrib["y"] = "0" titleblock.attrib["width"] = str(view_width) diff --git a/src/bonsai/test/bim/feature/drawing.feature b/src/bonsai/test/bim/feature/drawing.feature index a5bcf7e75d..ea7126cb54 100644 --- a/src/bonsai/test/bim/feature/drawing.feature +++ b/src/bonsai/test/bim/feature/drawing.feature @@ -247,8 +247,8 @@ Scenario: Add sheet And I look at the "Sheets" panel And I click "IMPORT" When I click "ADD" - Then the file "{ifc_dir}/layouts/A00 - UNTITLED.svg" should contain "titleblocks/A1.svg" - And the file "{ifc_dir}/layouts/A00 - UNTITLED.svg" should not contain "GRID NORTH" + Then the file "{ifc_dir}/layouts/A01 - UNTITLED.svg" should contain "titleblocks/A1.svg" + And the file "{ifc_dir}/layouts/A01 - UNTITLED.svg" should not contain "GRID NORTH" Scenario: Create sheet Given an empty IFC project @@ -258,8 +258,8 @@ Scenario: Create sheet And I click "ADD" And I select the "UNTITLED" item in the "BIM_UL_sheets" list When I click "OUTPUT" - Then the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should not contain "titleblocks/A1.svg" - And the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should contain "GRID NORTH" + Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should not contain "titleblocks/A1.svg" + And the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "GRID NORTH" Scenario: Add drawing to sheet Given an empty IFC project @@ -310,4 +310,4 @@ Scenario: Create sheet - with a drawing added to it And I press "bim.expand_sheet(sheet={sheet})" And I click "IMAGE_PLANE" When I click "OUTPUT" - Then the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should contain "IfcWall" + Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall" diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index cc1b73aa0e..f006e5ce2c 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -724,7 +724,7 @@ class TestDrawingMaintainingSheetPosition(NewFile): props = tool.Drawing.get_document_props() bpy.ops.bim.create_project() ifc = tool.Ifc.get() - sheet_path = Path.cwd() / "layouts" / "A00 - UNTITLED.svg" + sheet_path = Path.cwd() / "layouts" / "A01 - UNTITLED.svg" bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4)) obj = bpy.data.objects["Cube"] From 4f77c41bd59f1089fb17cd07f409e52ff21f2db2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 18:42:41 +0500 Subject: [PATCH 29/32] calculate_unit_scale - small optimization --- src/ifcopenshell-python/ifcopenshell/util/unit.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 6e78d73546..8d2b2d8306 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -697,11 +697,12 @@ def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUN unit_scale = 1 unit: ifcopenshell.entity_instance for unit in units.Units: - if not hasattr(unit, "UnitType") or unit.UnitType != unit_type: + if getattr(unit, "UnitType", ...) != unit_type: continue while unit.is_a("IfcConversionBasedUnit"): - unit_scale *= unit.ConversionFactor.ValueComponent.wrappedValue - unit = unit.ConversionFactor.UnitComponent + conversion_factor = unit.ConversionFactor + unit_scale *= conversion_factor.ValueComponent.wrappedValue + unit = conversion_factor.UnitComponent if unit.is_a("IfcSIUnit"): unit_scale *= get_prefix_multiplier(unit.Prefix) return unit_scale From fe81dc7f2145811172edc95883181956c790001b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 18:59:53 +0500 Subject: [PATCH 30/32] Units UI - match icons in enum with icons in ui list To build a visual association Example (previously enum had no icons) https://files.catbox.moe/0ks3jj.png https://files.catbox.moe/y1yrv5.png --- src/bonsai/bonsai/bim/module/unit/data.py | 22 ++++++++++------------ src/bonsai/bonsai/bim/module/unit/ui.py | 7 +------ src/bonsai/bonsai/tool/unit.py | 8 ++++++++ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/unit/data.py b/src/bonsai/bonsai/bim/module/unit/data.py index bae735245b..22977988bf 100644 --- a/src/bonsai/bonsai/bim/module/unit/data.py +++ b/src/bonsai/bonsai/bim/module/unit/data.py @@ -61,23 +61,21 @@ class UnitsData: return ifcopenshell.util.unit.get_full_unit_name(unit) @classmethod - def unit_classes(cls): + def unit_classes(cls) -> tool.Blender.BLENDER_ENUM_ITEMS: assert (entity := tool.Ifc.schema().declaration_by_name("IfcNamedUnit").as_entity()) declarations = ifcopenshell.util.schema.get_subtypes(entity) version = tool.Ifc.get_schema() + classes = sorted([d.name() for d in declarations]) + ["IfcDerivedUnit", "IfcMonetaryUnit"] results = [ - (c, c, get_entity_doc(version, c).get("description", "")) for c in sorted([d.name() for d in declarations]) + ( + c, + c, + get_entity_doc(version, c).get("description", ""), + tool.Unit.get_icon_for_unit_class(c), + i, + ) + for i, c in enumerate(classes) ] - results.extend( - [ - ("IfcDerivedUnit", "IfcDerivedUnit", get_entity_doc(version, "IfcDerivedUnit").get("description", "")), - ( - "IfcMonetaryUnit", - "IfcMonetaryUnit", - get_entity_doc(version, "IfcMonetaryUnit").get("description", ""), - ), - ] - ) return results @classmethod diff --git a/src/bonsai/bonsai/bim/module/unit/ui.py b/src/bonsai/bonsai/bim/module/unit/ui.py index 27f4f78a7e..d132a142d9 100644 --- a/src/bonsai/bonsai/bim/module/unit/ui.py +++ b/src/bonsai/bonsai/bim/module/unit/ui.py @@ -120,12 +120,7 @@ class BIM_UL_units(UIList): ) -> None: props = tool.Unit.get_unit_props() if item: - icon = "MOD_MESHDEFORM" - if item.ifc_class == "IfcSIUnit": - icon = "SNAP_GRID" - elif item.ifc_class == "IfcMonetaryUnit": - icon = "COPY_ID" - + icon = tool.Unit.get_icon_for_unit_class(item.ifc_class) row = layout.row(align=True) row.label(text=item.unit_type or "No Type", icon=icon) row.label(text=item.name or "Unnamed") diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index be5bc0fe5a..311a288eca 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -206,3 +206,11 @@ class Unit(bonsai.core.tool.Unit): precision = 1e-5 decimal_places = 5 return str(round(precision * round(value / precision), decimal_places)) + + @classmethod + def get_icon_for_unit_class(cls, ifc_class: str) -> str: + if ifc_class == "IfcSIUnit": + return "SNAP_GRID" + elif ifc_class == "IfcMonetaryUnit": + return "COPY_ID" + return "MOD_MESHDEFORM" From b4c4efcf5bf4d2e437e1a20673ebd1e0604b6be6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 19:06:47 +0500 Subject: [PATCH 31/32] unit.unassign_unit - smal doc-string fix --- src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py index 9651bfa87e..443d9d0b1c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py @@ -22,7 +22,7 @@ from typing import Optional def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.entity_instance]] = None) -> None: """Unassigns units as default units for the project - :param units: A list of units to assign as project defaults. + :param units: A list of units to unassign as project defaults. :return: None Example: From caccfd2516a8ec663b1a34bc5a1c82ea3d5d8ee1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Jun 2025 19:08:11 +0500 Subject: [PATCH 32/32] unit operators - add some descriptions --- src/bonsai/bonsai/bim/module/unit/operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/unit/operator.py b/src/bonsai/bonsai/bim/module/unit/operator.py index 0385a0ab4a..19620a89dc 100644 --- a/src/bonsai/bonsai/bim/module/unit/operator.py +++ b/src/bonsai/bonsai/bim/module/unit/operator.py @@ -35,6 +35,7 @@ class AssignSceneUnits(bpy.types.Operator, tool.Ifc.Operator): class AssignUnit(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_unit" bl_label = "Assign Unit" + bl_description = "Assign provided unit as the default project unit for it's unit type." bl_options = {"REGISTER", "UNDO"} unit: bpy.props.IntProperty() @@ -45,6 +46,7 @@ class AssignUnit(bpy.types.Operator, tool.Ifc.Operator): class UnassignUnit(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unassign_unit" bl_label = "Unassign Unit" + bl_description = "Unassign the specified unit as the project default." bl_options = {"REGISTER", "UNDO"} unit: bpy.props.IntProperty()