diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 5e078307a9..3f1808562d 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -168,6 +168,7 @@ classes = [ ui.BIM_PT_tab_services, ui.BIM_PT_tab_zones, ui.BIM_PT_tab_solar_analysis, + ui.BIM_PT_tab_lighting, # Structural analysis ui.BIM_PT_tab_structural, # Construction scheduling diff --git a/src/blenderbim/blenderbim/bim/module/light/operator.py b/src/blenderbim/blenderbim/bim/module/light/operator.py index c333564263..533e3751c7 100644 --- a/src/blenderbim/blenderbim/bim/module/light/operator.py +++ b/src/blenderbim/blenderbim/bim/module/light/operator.py @@ -17,155 +17,148 @@ # along with BlenderBIM Add-on. If not, see . import os + +try: + import pyradiance as pr +except ImportError: + print("PyRadiance is not available. Rendering functionality will be disabled.") + pr = None + import bpy -import json -import shutil -import multiprocessing -import pyradiance as pr -import ifcopenshell -import ifcopenshell.geom import blenderbim.tool as tool from pathlib import Path -from typing import Union +from typing import Union, Optional, Sequence +import json +import ifcopenshell +import ifcopenshell.geom +import multiprocessing +from mathutils import Vector from blenderbim.bim.module.light.data import SolarData class ExportOBJ(bpy.types.Operator): """Exports the IFC File to OBJ""" - bl_idname = "export_scene.radiance" bl_label = "Export" bl_description = "Export the IFC to OBJ" def execute(self, context): - # Get the resolution from the user input - resolution_x, resolution_y = self.getResolution(context) - quality = context.scene.radiance_exporter_properties.radiance_quality.upper() - detail = context.scene.radiance_exporter_properties.radiance_detail.upper() - variability = context.scene.radiance_exporter_properties.radiance_variability.upper() - # Calculate the aspect ratio - aspect_ratio = resolution_x / resolution_y + # 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 + json_file = context.scene.radiance_exporter_properties.json_file - # Get the blend file path and create the "Radiance Rendering" directory - self.report({"INFO"}, "Exporting Radiance files...") - blend_file_path = bpy.data.filepath - if not blend_file_path: - self.report({"ERROR"}, "Please save the Blender file before exporting.") - return {"CANCELLED"} + context.scene.radiance_exporter_properties.is_exporting = True - blend_file_dir = os.path.dirname(blend_file_path) - radiance_dir = os.path.join(blend_file_dir, "RadianceRendering") - self.report({"INFO"}, "Radiance directory: {}".format(radiance_dir)) - - if not os.path.exists(radiance_dir): - os.makedirs(radiance_dir) - - # IFC file export and processing - settings = ifcopenshell.geom.settings() + # Conversion from IFC to OBJ # Settings for obj - + settings = ifcopenshell.geom.settings() serializer_settings = ifcopenshell.geom.serializer_settings() - + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS) settings.set("apply-default-materials", True) serializer_settings.set("use-element-guids", True) settings.set("use-world-coords", True) - ifc_file_name = context.scene.radiance_exporter_properties.ifc_file_name - ifc_file_path = os.path.join(blend_file_dir, f"{ifc_file_name}.ifc") - - if not os.path.exists(ifc_file_path): - self.report({"ERROR"}, f"IFC file not found: {ifc_file_path}") - return {"CANCELLED"} - - ifc_file = ifcopenshell.open(ifc_file_path) + if should_load_from_memory: + ifc_file = tool.Ifc.get() + + else: + ifc_file_path = context.scene.radiance_exporter_properties.ifc_file + ifc_file = ifcopenshell.open(ifc_file_path) + for material in ifc_file.by_type("IfcMaterial"): - self.report({"INFO"}, f"Material: {material.Name}, ID: {material.id()}") + self.report({'INFO'}, f"Material: {material.Name}, ID: {material.id()}") - obj_file_path = os.path.join(radiance_dir, "model.obj") - mtl_file_path = os.path.join(radiance_dir, "model.mtl") - - # serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, ifcopenshell.geom.serializer_settings()) + obj_file_path = os.path.join(output_dir, "model.obj") + mtl_file_path = os.path.join(output_dir, "model.mtl") + serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings) serialiser.setFile(ifc_file) serialiser.setUnitNameAndMagnitude("METER", 1.0) serialiser.writeHeader() - iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count()) + if ifc_file.schema in ("IFC2X3", "IFC4"): + elements = ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcProxy") + else: + elements = ifc_file.by_type("IfcElement") + + elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")] + + iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements) if iterator.initialize(): while True: - serialiser.write(iterator.get()) + shape = iterator.get() + materials = shape.geometry.materials + material_ids = shape.geometry.material_ids + for material in materials: + print(material, dir(material)) + print(material_ids) + serialiser.write(shape) if not iterator.next(): break + serialiser.finalize() + context.scene.radiance_exporter_properties.is_exporting = False - return {"FINISHED"} - - def getResolution(self, context): - scene = context.scene - props = scene.radiance_exporter_properties - resolution_x = props.radiance_resolution_x - resolution_y = props.radiance_resolution_y - return resolution_x, resolution_y + return {'FINISHED'} class RadianceRender(bpy.types.Operator): """Radiance Rendering""" - bl_idname = "render_scene.radiance" bl_label = "Render" bl_description = "Renders the scene using Radiance" def execute(self, context): if pr is None: - self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.") - return {"CANCELLED"} - + self.report({'ERROR'}, "PyRadiance is not available. Cannot perform rendering.") + return {'CANCELLED'} + # Get the resolution from the user input resolution_x, resolution_y = self.getResolution(context) quality = context.scene.radiance_exporter_properties.radiance_quality.upper() detail = context.scene.radiance_exporter_properties.radiance_detail.upper() variability = context.scene.radiance_exporter_properties.radiance_variability.upper() - # Get the blend file path and create the "Radiance Rendering" directory - blend_file_path = bpy.data.filepath - if not blend_file_path: - self.report({"ERROR"}, "Please save the Blender file before rendering.") - return {"CANCELLED"} + should_load_from_memory = context.scene.radiance_exporter_properties.should_load_from_memory + output_dir = context.scene.radiance_exporter_properties.output_dir + json_file = context.scene.radiance_exporter_properties.json_file - blend_file_dir = os.path.dirname(blend_file_path) - radiance_dir = os.path.join(blend_file_dir, "RadianceRendering") + obj_file_path = os.path.join(output_dir, "model.obj") + + + camera = self.get_active_camera(context) + if camera is None: + self.report({'ERROR'}, "No active camera found in the scene. Please add a camera and set it as active.") + return {'CANCELLED'} + + # Get camera position and direction + camera_position, camera_direction = self.get_camera_data(camera) # Material processing style = [] - obj_file_path = os.path.join(radiance_dir, "model.obj") + with open(obj_file_path, "r") as obj_file: for line in obj_file: if line.startswith("usemtl"): l = line.strip().split(" ") style.append(l[1]) - # json_file_path = os.path.join(blend_file_dir, "material_mapping.json") - json_file_path = context.scene.radiance_exporter_properties.json_file_path - self.report({"INFO"}, f"Selected JSON file: {json_file_path}") - if json_file_path: - # self.report({'INFO'}, f"Selected JSON file: {json_file_name}") - json_dest_path = os.path.join(blend_file_dir, json_file_path.split("\\")[-1]) - shutil.copy(json_file_path, json_dest_path) - self.report({"INFO"}, f"JSON file saved to: {json_dest_path}") + # Check if json file is empty or not + if json_file: + with open(json_file, 'r') as file: + data = json.load(file) else: - self.report({"WARNING"}, "No JSON file selected") - - with open(json_file_path, "r") as file: - data = json.load(file) - + data = {} + # Create materials.rad file - materials_file = os.path.join(radiance_dir, "materials.rad") + materials_file = os.path.join(output_dir, "materials.rad") with open(materials_file, "w") as file: - file.write("void plastic white\n0\n0\n5 1 1 1 0 0\n") + file.write("void plastic white\n0\n0\n5 0.6 0.6 0.6 0 0\n") file.write("void plastic blue_plastic\n0\n0\n5 0.1 0.2 0.8 0.05 0.1\n") file.write("void plastic red_plastic\n0\n0\n5 0.8 0.1 0.2 0.05 0.1\n") file.write("void metal silver_metal\n0\n0\n5 0.8 0.8 0.8 0.9 0.1\n") @@ -176,48 +169,58 @@ class RadianceRender(bpy.types.Operator): for i in set(style): file.write("inherit alias " + i + " " + data.get(i, "white") + "\n") - self.report({"INFO"}, "Exported Materials Rad file to: {}".format(materials_file)) + self.report({'INFO'}, "Exported Materials Rad file to: {}".format(materials_file)) # Run obj2mesh - rtm_file_path = os.path.join(radiance_dir, "model.rtm") - mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file]) + rtm_file_path = os.path.join(output_dir, "model.rtm") + mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file]) # subprocess.run(["obj2mesh", "-a", materials_file, obj_file_path, rtm_file_path]) - self.report({"INFO"}, "obj2mesh output: {}".format(mesh_file_path)) - scene_file = os.path.join(radiance_dir, "scene.rad") + self.report({'INFO'}, "obj2mesh output: {}".format(mesh_file_path)) + scene_file = os.path.join(output_dir, "scene.rad") with open(scene_file, "w") as file: file.write("void mesh model\n1 " + rtm_file_path + "\n0\n0\n") - self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file)) + self.report({'INFO'}, "Exported Scene file to: {}".format(scene_file)) # Py Radiance Rendering code scene = pr.Scene("ascene") - material_path = os.path.join(radiance_dir, "materials.rad") - scene_path = os.path.join(radiance_dir, "scene.rad") + material_path = os.path.join(output_dir, "materials.rad") + scene_path = os.path.join(output_dir, "scene.rad") scene.add_material(material_path) scene.add_surface(scene_path) - aview = pr.View(position=(1, 1.5, 1), direction=(1, 0, 0)) + aview = pr.View(position=camera_position, direction=camera_direction) scene.add_view(aview) - octpath = os.path.join(blend_file_dir, "ascene.oct") - print("Reached here") - image = pr.render( - scene, - ambbounce=1, - resolution=(resolution_x, resolution_y), - quality=quality, - detail=detail, - variability=variability, - ) - - raw_hdr_path = os.path.join(radiance_dir, "raw.hdr") + image = pr.render(scene, ambbounce=1, resolution=(resolution_x, resolution_y), + quality=quality, detail=detail, variability=variability) + + raw_hdr_path = os.path.join(output_dir, "raw.hdr") with open(raw_hdr_path, "wb") as wtr: wtr.write(image) - self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(raw_hdr_path)) - return {"FINISHED"} + self.report({'INFO'}, "Radiance rendering completed. Output: {}".format(raw_hdr_path)) + return {'FINISHED'} + + def get_active_camera(self, context): + if context.scene.camera: + return context.scene.camera + for obj in context.scene.objects: + if obj.type == 'CAMERA': + return obj + return None + + def get_camera_data(self, camera): + # Get camera position + position = camera.matrix_world.to_translation() + + # Get camera direction + direction = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)) + direction.normalize() + + return (position.x, position.y, position.z), (direction.x, direction.y, direction.z) def getResolution(self, context): scene = context.scene @@ -226,11 +229,10 @@ class RadianceRender(bpy.types.Operator): resolution_y = props.radiance_resolution_y return resolution_x, resolution_y - def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs): output_bytes = pr.obj2mesh(inp, **kwargs) - with open(output_file, "wb") as f: + with open(output_file, 'wb') as f: f.write(output_bytes) return output_file diff --git a/src/blenderbim/blenderbim/bim/module/light/prop.py b/src/blenderbim/blenderbim/bim/module/light/prop.py index aefe832e22..225fbf3621 100644 --- a/src/blenderbim/blenderbim/bim/module/light/prop.py +++ b/src/blenderbim/blenderbim/bim/module/light/prop.py @@ -139,47 +139,90 @@ def update_sun_path(): class RadianceExporterProperties(PropertyGroup): - def update_json_file_path(self, context): - if self.json_file_path: - self.json_file_path = bpy.path.abspath(self.json_file_path) + def update_json_file(self, context): + if self.json_file: + self.json_file = bpy.path.abspath(self.json_file) + + def update_output_dir(self, context): + if self.output_dir: + self.output_dir = bpy.path.abspath(self.output_dir) + + def update_ifc_file(self, context): + if self.ifc_file: + self.ifc_file = bpy.path.abspath(self.ifc_file) + + is_exporting: bpy.props.BoolProperty( + name="Is Exporting", + description="Whether the OBJ export is in progress", + default=False + ) + + should_load_from_memory: BoolProperty( + name="Load from Memory", + default=False, + ) 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="" + output_dir: StringProperty( + name="Output Directory", + description="Directory to output Radiance files", + default="", + subtype="DIR_PATH", + update=lambda self, context: self.update_output_dir(context) ) - - json_file_path: StringProperty( + ifc_file: StringProperty( + name="IFC File", + description="Path to the IFC file", + default="", + subtype="FILE_PATH", + update=lambda self, context: self.update_ifc_file(context) + ) + json_file: StringProperty( name="JSON File", 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(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", ) diff --git a/src/blenderbim/blenderbim/bim/module/light/ui.py b/src/blenderbim/blenderbim/bim/module/light/ui.py index d0d9b89f15..4900270856 100644 --- a/src/blenderbim/blenderbim/bim/module/light/ui.py +++ b/src/blenderbim/blenderbim/bim/module/light/ui.py @@ -32,23 +32,28 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "scene" - bl_parent_id = "BIM_PT_tab_services" + bl_parent_id = "BIM_PT_tab_lighting" def draw(self, context): layout = self.layout scene = context.scene - if sun_position is None: - layout.label(text="Enable 'Sun Position' addon to continue.") - return - props = scene.radiance_exporter_properties + + if tool.Ifc.get(): + row = self.layout.row() + row.prop(props, "should_load_from_memory") + + if not tool.Ifc.get() or not props.should_load_from_memory: + row = self.layout.row(align=True) + row.prop(props, "ifc_file") + # row.operator("bim.select_ifctester_ifc_file", icon="FILE_FOLDER", text="") + + row = layout.row() + row.prop(props, "output_dir") row = layout.row() - row.prop(props, "ifc_file_name") - - row = layout.row() - row.prop(props, "json_file_path") + row.prop(props, "json_file") row = layout.row() row.label(text="Resolution") @@ -68,11 +73,11 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row.prop(props, "radiance_variability") row = layout.row() - row.operator("export_scene.radiance", text="Export to OBJ") + row.operator("export_scene.radiance", text="Export Geometry for Simulation") row = layout.row() row.operator("render_scene.radiance", text="Radiance Render") - + row.enabled = not props.is_exporting class BIM_PT_solar(bpy.types.Panel): """Creates a Panel in the render properties window""" diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 851de0440c..b9ad27592f 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -693,6 +693,19 @@ class BIM_PT_tab_services(Panel): def draw(self, context): pass +class BIM_PT_tab_lighting(Panel): + bl_label = "Lighting" + 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_zones(Panel): bl_label = "Zones"