diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 73cb7adcc2..1924acec46 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -169,6 +169,7 @@ classes = [ # Services and systems ui.BIM_PT_tab_services, ui.BIM_PT_tab_zones, + ui.BIM_PT_tab_solar_analysis, # Structural analysis ui.BIM_PT_tab_structural, # Construction scheduling diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py index 2301b91cd0..ce59bb231a 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/data.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -119,16 +119,11 @@ class GeoreferenceData: def true_north(cls): ifc = tool.Ifc.get() true_north = {} - - if ifc.schema == "IFC2X3": - return - for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): if not context.TrueNorth: continue true_north = context.TrueNorth.DirectionRatios break - return true_north @classmethod diff --git a/src/blenderbim/blenderbim/bim/module/light/__init__.py b/src/blenderbim/blenderbim/bim/module/light/__init__.py index f1a3162234..4434460f3b 100644 --- a/src/blenderbim/blenderbim/bim/module/light/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/light/__init__.py @@ -22,13 +22,19 @@ from . import ui, prop, operator classes = ( operator.ExportOBJ, + operator.ImportLatLong, + operator.ImportTrueNorth, operator.RadianceRender, + prop.BIMSolarProperties, prop.RadianceExporterProperties, ui.BIM_PT_radiance_exporter, + ui.BIM_PT_solar, ) def register(): bpy.types.Scene.radiance_exporter_properties = bpy.props.PointerProperty(type=prop.RadianceExporterProperties) + bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties) def unregister(): - del bpy.types.Scene.radiance_exporter_properties \ No newline at end of file + del bpy.types.Scene.radiance_exporter_properties + del bpy.types.Scene.BIMSolarProperties diff --git a/src/blenderbim/blenderbim/bim/module/light/operator.py b/src/blenderbim/blenderbim/bim/module/light/operator.py index e46c7320c9..b555925928 100644 --- a/src/blenderbim/blenderbim/bim/module/light/operator.py +++ b/src/blenderbim/blenderbim/bim/module/light/operator.py @@ -17,17 +17,17 @@ # along with BlenderBIM Add-on. If not, see . import os - -import pyradiance as pr import bpy - -from pathlib import Path -from typing import Union, Optional, Sequence import json +import shutil +import multiprocessing +import pyradiance as pr import ifcopenshell import ifcopenshell.geom -import multiprocessing -import shutil +import blenderbim.tool as tool +from pathlib import Path +from typing import Union +from blenderbim.bim.module.light.data import SolarData @@ -229,4 +229,43 @@ def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwarg with open(output_file, 'wb') as f: f.write(output_bytes) - return output_file \ No newline at end of file + return output_file + + +class ImportTrueNorth(bpy.types.Operator): + bl_idname = "bim.import_true_north" + bl_label = "Import True North" + bl_description = "Imports the True North from your IFC geometric context" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if not tool.Ifc.get(): + return False + if not SolarData.is_loaded: + SolarData.load() + return SolarData.data["true_north"] is not None + + def execute(self, context): + props = context.scene.BIMSolarProperties + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.TrueNorth: + continue + value = context.TrueNorth.DirectionRatios + props.true_north = ifcopenshell.util.geolocation.yaxis2angle(*value[:2]) + return {"FINISHED"} + + +class ImportLatLong(bpy.types.Operator): + bl_idname = "bim.import_lat_long" + bl_label = "Import Latitude / Longitude" + bl_description = "Imports the latitude / longitude from an IfcSite" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMSolarProperties + site = tool.Ifc.get().by_id(int(props.sites)) + if site.RefLatitude and site.RefLongitude: + props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude) + props.longitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLongitude) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/light/prop.py b/src/blenderbim/blenderbim/bim/module/light/prop.py index 9850a1b0db..64b582b583 100644 --- a/src/blenderbim/blenderbim/bim/module/light/prop.py +++ b/src/blenderbim/blenderbim/bim/module/light/prop.py @@ -17,8 +17,53 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from bpy.props import IntProperty, StringProperty, EnumProperty +import pytz +import tzfpy +import datetime +from math import radians +from bpy.props import IntProperty, StringProperty, EnumProperty, FloatProperty from bpy.types import PropertyGroup +from blenderbim.bim.module.light.data import SolarData + + +def get_sites(self, context): + if not SolarData.is_loaded: + SolarData.load() + return SolarData.data["sites"] + + +def update_latlong(self, context): + sun_props = context.scene.sun_pos_properties + sun_props.latitude = self.latitude + sun_props.longitude = self.longitude + update_sun_path() + + +def update_hourminute(self, context): + sun_props = context.scene.sun_pos_properties + sun_props.time = self.hour + (self.minute / 60) + update_sun_path() + + +def update_date(self, context): + update_sun_path() + + +def update_true_north(self, context): + sun_props = context.scene.sun_pos_properties + # Preserve IFC sign convention + sun_props.north_offset = radians(self.true_north * -1) + + +def update_sun_path(): + props = bpy.context.scene.BIMSolarProperties + sun_props = bpy.context.scene.sun_pos_properties + timezone = pytz.timezone(tzfpy.get_tz(props.longitude, props.latitude)) + dt = datetime.datetime(datetime.datetime.now(datetime.UTC).year, int(props.month), props.date, props.hour, props.minute) + local_time = timezone.localize(dt, is_dst=None) + sun_props.use_daylight_savings = bool(local_time.dst()) + sun_props.UTC_zone = local_time.utcoffset().total_seconds() / 3600 + class RadianceExporterProperties(PropertyGroup): @@ -26,25 +71,14 @@ class RadianceExporterProperties(PropertyGroup): if self.json_file_path: self.json_file_path = bpy.path.abspath(self.json_file_path) - - - radiance_resolution_x: IntProperty( - name="X", - description="Horizontal resolution of the output image", - default=1920, - min=1 + name="X", description="Horizontal resolution of the output image", default=1920, min=1 ) radiance_resolution_y: IntProperty( - name="Y", - description="Vertical resolution of the output image", - default=1080, - min=1 + name="Y", description="Vertical resolution of the output image", default=1080, min=1 ) ifc_file_name: StringProperty( - name="IFC File Name", - description="Name of the IFC file to use (without .ifc extension)", - default="" + name="IFC File Name", description="Name of the IFC file to use (without .ifc extension)", default="" ) json_file_path: StringProperty( @@ -52,36 +86,61 @@ class RadianceExporterProperties(PropertyGroup): description="Path to the JSON file", default="", subtype="FILE_PATH", - update=lambda self, context: self.update_json_file_path(context) + update=lambda self, context: self.update_json_file_path(context), ) - + radiance_quality: EnumProperty( name="Quality", description="Radiance rendering quality", - items=[ - ('LOW', "Low", "Low quality"), - ('MEDIUM', "Medium", "Medium quality"), - ('HIGH', "High", "High quality") - ], - default='MEDIUM' + items=[("LOW", "Low", "Low quality"), ("MEDIUM", "Medium", "Medium quality"), ("HIGH", "High", "High quality")], + default="MEDIUM", ) radiance_detail: EnumProperty( name="Detail", description="Radiance rendering detail", - items=[ - ('LOW', "Low", "Low detail"), - ('MEDIUM', "Medium", "Medium detail"), - ('HIGH', "High", "High detail") - ], - default='MEDIUM' + items=[("LOW", "Low", "Low detail"), ("MEDIUM", "Medium", "Medium detail"), ("HIGH", "High", "High detail")], + default="MEDIUM", ) radiance_variability: EnumProperty( name="Variability", description="Radiance rendering variability", items=[ - ('LOW', "Low", "Low variability"), - ('MEDIUM', "Medium", "Medium variability"), - ('HIGH', "High", "High variability") + ("LOW", "Low", "Low variability"), + ("MEDIUM", "Medium", "Medium variability"), + ("HIGH", "High", "High variability"), ], - default='MEDIUM' - ) \ No newline at end of file + default="MEDIUM", + ) + + +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) + true_north: FloatProperty(name="True North", min=-180, max=180, update=update_true_north) + month: EnumProperty( + name="Month", + items=[ + (str(i + 1), m, "") + for i, m in enumerate( + ( + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ) + ) + ], + update=update_date + ) + date: IntProperty(name="Date", min=1, max=31, default=1, update=update_date) + hour: IntProperty(name="Hour", min=0, max=23, update=update_hourminute) + minute: IntProperty(name="Minute", min=0, max=59, update=update_hourminute) diff --git a/src/blenderbim/blenderbim/bim/module/light/ui.py b/src/blenderbim/blenderbim/bim/module/light/ui.py index ab7e0c42cf..2c9bf0fcda 100644 --- a/src/blenderbim/blenderbim/bim/module/light/ui.py +++ b/src/blenderbim/blenderbim/bim/module/light/ui.py @@ -17,6 +17,9 @@ # along with BlenderBIM Add-on. If not, see . import bpy +from blenderbim.bim.module.light.data import SolarData +from sun_position.sun_calc import format_time, format_hms, sun + class BIM_PT_radiance_exporter(bpy.types.Panel): """Creates a Panel in the render properties window""" @@ -61,4 +64,75 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row.operator("export_scene.radiance", text="Export to OBJ") row = layout.row() - row.operator("render_scene.radiance", text="Radiance Render") \ No newline at end of file + row.operator("render_scene.radiance", text="Radiance Render") + + +class BIM_PT_solar(bpy.types.Panel): + """Creates a Panel in the render properties window""" + bl_label = "Solar Access / Shadow" + bl_idname = "BIM_PT_solar" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "scene" + bl_parent_id = "BIM_PT_tab_solar_analysis" + + def draw(self, context): + if not SolarData.is_loaded: + SolarData.load() + + props = context.scene.BIMSolarProperties + + if SolarData.data["sites"]: + row = self.layout.row(align=True) + row.prop(props, "sites") + row.operator("bim.import_lat_long", icon="IMPORT", text="") + else: + row = self.layout.row(align=True) + row.label(text="No Sites With Lat/Longs Found", icon="ERROR") + + sun_props = context.scene.sun_pos_properties + + row = self.layout.row() + row.prop(sun_props, "coordinates", icon='URL') + row = self.layout.row(align=True) + row.prop(props, "latitude") + row.prop(props, "longitude") + + row = self.layout.row(align=True) + row.prop(props, "true_north") + row.prop(sun_props, "show_north", icon="HIDE_ON" if sun_props.show_north else "HIDE_OFF", text="") + if SolarData.data["true_north"] is not None: + row.operator("bim.import_true_north", icon="IMPORT", text="") + + row = self.layout.row(align=True) + row.prop(props, "month", text="") + row.prop(props, "date") + + row = self.layout.row(align=True) + row.prop(props, "hour") + row.prop(props, "minute") + + row = self.layout.row(align=True) + box = row.box() + + local_time = format_time(sun_props.time, sun_props.use_daylight_savings) + utc_time = format_time(sun_props.time, sun_props.use_daylight_savings, sun_props.UTC_zone) + + row2 = box.row() + row2.label(text=f"Local Time: {local_time}") + row2 = box.row() + row2.label(text=f"UTC Time: {utc_time}") + + box = row.box() + + sunrise = format_hms(sun.sunrise) + sunset = format_hms(sun.sunset) + + row2 = box.row() + row2.label(text=f"Sunrise: {sunrise}") + row2 = box.row() + row2.label(text=f"Sunset: {sunset}") + + row = self.layout.row(align=True) + row.prop(sun_props, "sun_distance", text="Sun Path Size") + row.prop(sun_props, "show_analemmas", icon="HIDE_ON" if sun_props.show_analemmas else "HIDE_OFF", text="") diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 154677687f..851de0440c 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -373,7 +373,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row.prop(context.scene.DocProperties, "magic_font_scale") row = self.layout.row() row.prop(context.scene.DocProperties, "imperial_precision") - # Scene panel groups @@ -709,6 +708,20 @@ class BIM_PT_tab_zones(Panel): pass +class BIM_PT_tab_solar_analysis(Panel): + bl_label = "Solar Analysis" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "SERVICES") and tool.Ifc.get() + + def draw(self, context): + pass + + class BIM_PT_tab_quality_control(Panel): bl_label = "Quality Control" bl_space_type = "PROPERTIES"