This commit is contained in:
Andrej730
2025-06-23 14:30:22 +05:00
parent 3a06afae25
commit 30c33518f0
5 changed files with 157 additions and 61 deletions
@@ -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)
+26 -23
View File
@@ -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"}
+94 -26
View File
@@ -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
+12 -6
View File
@@ -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")
+18 -1
View File
@@ -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):