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"