From 5e10752bd750c5a3ea0405f0aa63b140a54644b4 Mon Sep 17 00:00:00 2001 From: Chirag Singh Date: Thu, 27 Nov 2025 18:21:45 +0530 Subject: [PATCH] Light: Fixed all issues --- .../bonsai/bim/module/light/__init__.py | 1 - .../bonsai/bim/module/light/operator.py | 243 ++++++++---------- src/bonsai/bonsai/bim/module/light/prop.py | 32 +-- src/bonsai/bonsai/bim/module/light/ui.py | 21 +- 4 files changed, 133 insertions(+), 164 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/__init__.py b/src/bonsai/bonsai/bim/module/light/__init__.py index 0055d8ece9..002419e38e 100644 --- a/src/bonsai/bonsai/bim/module/light/__init__.py +++ b/src/bonsai/bonsai/bim/module/light/__init__.py @@ -39,7 +39,6 @@ classes = ( operator.MoveSunPathTo3DCursor, operator.RadianceRender, operator.FalseColorRadiance, - operator.ConvertHDRToFootCandles, operator.ViewFromSun, operator.LightPickCoordinates, operator.LightSetTimeToNow, diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py index 33694d166a..a5312fdbc5 100644 --- a/src/bonsai/bonsai/bim/module/light/operator.py +++ b/src/bonsai/bonsai/bim/module/light/operator.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import os +import subprocess import pyradiance as pr from datetime import datetime @@ -105,7 +106,27 @@ class ExportOBJ(bpy.types.Operator): elements += ifc_file.by_type("IfcSite") 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) + # Filter elements by visibility in Blender + # Only export elements that are visible in the viewport + visible_elements = [] + for element in elements: + # Get the corresponding Blender object + blender_obj = tool.Ifc.get_object(element) + + # If no Blender object exists, include the element (not yet represented) + if blender_obj is None: + visible_elements.append(element) + continue + + # Check if the object is visible + # visible_get() returns True if the object is visible (not hidden, not on hidden layer, etc.) + if blender_obj.visible_get(): + visible_elements.append(element) + else: + print(f"Skipping hidden element: {element.GlobalId if hasattr(element, 'GlobalId') else element.id()}") + + print(f"Exporting {len(visible_elements)} visible elements out of {len(elements)} total elements") + iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=visible_elements) if iterator.initialize(): while True: shape = iterator.get() @@ -243,8 +264,8 @@ class PrepareRadianceScene(bpy.types.Operator): return {"CANCELLED"} # Build datetime for Radiance gensky - # Note: sun_pos_props and sun_props are synchronized by update_sun_path() - dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute) + # Note: sun_props (BIMSolarProperties) is the authoritative source, synchronized to sun_pos_props + dt = datetime(sun_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute) print(f"Sun position data for Radiance gensky:") print(f" DateTime: {dt}") @@ -258,12 +279,19 @@ class PrepareRadianceScene(bpy.types.Operator): sunny_without_sun = sky_condition == "SUNNY_WITHOUT_SUN" cloudy = sky_condition == "CLOUDY" + # Note: Radiance gensky expects longitude and timezone in "degrees west of Greenwich" + # Standard geographic coordinates have positive east, so we need to negate for eastern hemisphere + # longitude: positive = east of Greenwich (negate for gensky), negative = west (keep as is) + # timezone: UTC offset hours × 15 degrees, but negative for eastern hemisphere (UTC+) + longitude_for_gensky = -sun_props.longitude # Negate because geographic coords are positive east + timezone_for_gensky = -int(sun_props.UTC_zone) * 15 # Negate because UTC+ is east + sky_description = pr.gensky( dt=dt, latitude=sun_props.latitude, - longitude=sun_props.longitude, + longitude=longitude_for_gensky, year=sun_pos_props.year, - timezone=-int(sun_props.UTC_zone), + timezone=timezone_for_gensky, sunny_with_sun=sunny_with_sun, sunny_without_sun=sunny_without_sun, cloudy=cloudy, @@ -433,7 +461,9 @@ ground_glow source ground if ies_light.ies_file_path and ies_light.target_object: obj = ies_light.target_object pos = obj.location - z_rot = ies_light.rotation_z + # Convert rotation from radians to degrees + # (Blender's ANGLE subtype stores values in radians) + z_rot = math.degrees(ies_light.rotation_z) if idx in converted_ies_lights: # Light is enabled and was converted @@ -620,10 +650,6 @@ class FalseColorRadiance(bpy.types.Operator): print(f" Multiplier: {multiplier}") try: - import subprocess - - # Build command line arguments for falsecolor - # Format: falsecolor [options] -ip input.hdr > output.hdr fc_output_name = props.false_color_output_name fc_hdr_path = os.path.join(output_dir, f"{fc_output_name}.hdr") @@ -631,73 +657,82 @@ class FalseColorRadiance(bpy.types.Operator): light_module_dir = os.path.dirname(__file__) falsecolor_exe = os.path.join(light_module_dir, "Radiance", "bin", "falsecolor.exe") radiance_lib = os.path.join(light_module_dir, "Radiance", "lib") - falsecolor_bin = falsecolor_exe if os.path.exists(falsecolor_exe) else "falsecolor" + radiance_bin_dir = os.path.join(light_module_dir, "Radiance", "bin") + print(f"Looking for bundled falsecolor at: {falsecolor_exe}") + + if os.path.exists(falsecolor_exe): + falsecolor_bin = falsecolor_exe + print(f"Using bundled falsecolor: {falsecolor_bin}") + else: + # Try to find falsecolor in system PATH + import shutil + + system_falsecolor = shutil.which("falsecolor.exe") or shutil.which("falsecolor") + if system_falsecolor: + falsecolor_bin = system_falsecolor + radiance_bin_dir = os.path.dirname(system_falsecolor) + radiance_lib = os.path.join(os.path.dirname(radiance_bin_dir), "lib") + print(f"Using system falsecolor: {falsecolor_bin}") + else: + falsecolor_bin = "falsecolor" + print(f"Warning: falsecolor.exe not found in bundle or system PATH, will try system PATH") + + # Build command line arguments for falsecolor cmd = [falsecolor_bin] - - # Add multiplier cmd.extend(["-m", str(multiplier)]) - - # Add scale cmd.extend(["-s", fc_scale]) - - # Add label cmd.extend(["-l", props.false_color_label]) - # Add contour lines if enabled + # Determine contour mode and build command accordingly if props.false_color_contour_lines: - cmd.append("-cl") - - # Add -ip flag (use same image for intensity and overlay) - cmd.append("-ip") - cmd.append(hdr_path) + if props.false_color_contour_mode == "WITH_BG": + # Contour lines with background - use -cl and -ip + print("Generating false color with contour lines (with background)...") + cmd.append("-cl") + cmd.append("-ip") + cmd.append(hdr_path) + else: # WITHOUT_BG + # Contour lines only (with legend but no colored background) + print("Generating false color with contour lines only (with legend, no background)...") + cmd.append("-cl") + cmd.append("-i") + cmd.append(hdr_path) + else: + # No contour lines - standard false color with -ip + print("Generating standard false color image...") + cmd.append("-ip") + cmd.append(hdr_path) print(f"Running falsecolor command: {' '.join(cmd)}") print(f"Using falsecolor binary: {falsecolor_bin}") # Setup environment for Radiance tools env = os.environ.copy() - env["RAYPATH"] = "." + os.pathsep + radiance_lib + + # Add Radiance bin directory to PATH so falsecolor can find its dependencies + if radiance_bin_dir and os.path.exists(radiance_bin_dir): + env["PATH"] = radiance_bin_dir + os.pathsep + env.get("PATH", "") + print(f"Added to PATH: {radiance_bin_dir}") + + # Set RAYPATH for Radiance library files + if os.path.exists(radiance_lib): + env["RAYPATH"] = "." + os.pathsep + radiance_lib + print(f"Set RAYPATH: {radiance_lib}") + else: + print(f"Note: Radiance lib folder not found at {radiance_lib}") # Execute falsecolor and capture output - try: - result = subprocess.run(cmd, capture_output=True, check=True, env=env) - fc_image = result.stdout + # Use cwd=output_dir to ensure falsecolor writes to the correct location + result = subprocess.run(cmd, capture_output=True, check=True, env=env, cwd=output_dir) + fc_image = result.stdout - print(f"Saving false color HDR to: {fc_hdr_path}") - with open(fc_hdr_path, "wb") as f: - f.write(fc_image) + print(f"Saving false color HDR to: {fc_hdr_path}") + with open(fc_hdr_path, "wb") as f: + f.write(fc_image) - print(f"False color HDR generated successfully: {fc_hdr_path}") - self.report({"INFO"}, f"False color image generated: {fc_hdr_path}") - - except FileNotFoundError: - # falsecolor binary not found in PATH or bundled location - # Fallback: use pcond as an alternative visualization - print("WARNING: falsecolor binary not found") - print(f" Checked locations:") - print(f" 1. Bundled: {falsecolor_exe}") - print(f" 2. System PATH: falsecolor") - print("Using alternative method: Tone-mapped HDR for luminance visualization...") - print("Note: This is a fallback visualization without false color overlay") - - fc_image = pr.pcond(hdr=hdr_path, human=True) - - # Still save as HDR for consistency - with open(fc_hdr_path, "wb") as f: - f.write(fc_image) - - print(f"Tone-mapped HDR saved to: {fc_hdr_path}") - self.report( - {"WARNING"}, - "falsecolor binary not found. Generated tone-mapped HDR instead. " - "Ensure Radiance/bin folder is present in the light module directory.", - ) - - except subprocess.CalledProcessError as e: - error_msg = f"falsecolor failed: {e.stderr.decode() if e.stderr else str(e)}" - print(f"ERROR: {error_msg}") - raise RuntimeError(error_msg) + print(f"False color HDR generated successfully: {fc_hdr_path}") + self.report({"INFO"}, f"False color image generated: {fc_hdr_path}") # Generate TIFF version for convenience try: @@ -714,81 +749,27 @@ class FalseColorRadiance(bpy.types.Operator): return {"FINISHED"} - except Exception as e: - error_msg = f"Failed to generate false color image: {str(e)}" - self.report({"ERROR"}, error_msg) + except subprocess.CalledProcessError as e: + error_msg = f"falsecolor failed: {e.stderr.decode() if e.stderr else str(e)}" + print(f"ERROR: {error_msg}") + self.report({"ERROR"}, error_msg) + return {"CANCELLED"} + except FileNotFoundError as fnf_error: + # Check if falsecolor was not found + if "falsecolor" in str(fnf_error).lower() or not os.path.exists(falsecolor_exe): + error_msg = "falsecolor not found! Please install Radiance and add it to your system PATH" + else: + error_msg = f"falsecolor binary not found at: {falsecolor_exe}. Please ensure the Radiance/bin folder is present in the light module directory." print(f"ERROR: {error_msg}") - print(f"Traceback:") - import traceback - - traceback.print_exc() - return {"CANCELLED"} - - -class ConvertHDRToFootCandles(bpy.types.Operator): - """Convert HDR image to foot-candles unit""" - - bl_idname = "render_scene.convert_hdr_to_fc" - bl_label = "Convert HDR to Foot-Candles" - bl_description = "Convert the main render HDR to foot-candles unit for illuminance verification" - - def execute(self, context): - props = tool.Blender.get_radiance_exporter_props() - output_dir = props.output_dir - output_file_name = props.output_file_name - - # Check if the main render HDR exists - hdr_path = os.path.join(output_dir, f"{output_file_name}.hdr") - if not os.path.exists(hdr_path): - error_msg = f"HDR file not found at: {hdr_path}. Please run 'Radiance Render' first." self.report({"ERROR"}, error_msg) - print(f"ERROR: {error_msg}") return {"CANCELLED"} - - print(f"Converting HDR to foot-candles: {hdr_path}") - - # Conversion factor from cd/m² to foot-candles - # 1 foot-candle = 10.764 lux = 10.764 cd/m² - # So to convert: multiply by 1/10.764 ≈ 0.0929 - fc_scale_factor = 0.0929005 - - try: - # Use pyradiance Pcomb to convert - print(f"Applying pcomb with scale factor {fc_scale_factor} and original flag...") - - # Create Pcomb command with original=True (-o flag) and scaler for unit conversion - # Note: fout=False ensures the output is a proper HDR file with headers, not raw data - pcomb_result = pr.Pcomb(fout=False).add(hdr_path, original=True, scaler=fc_scale_factor)() - - # Save the converted HDR - fc_output_name = props.hdr_to_fc_output_name - fc_hdr_path = os.path.join(output_dir, f"{fc_output_name}.hdr") - - print(f"Saving foot-candles HDR to: {fc_hdr_path}") - with open(fc_hdr_path, "wb") as f: - f.write(pcomb_result) - - print(f"HDR converted to foot-candles successfully: {fc_hdr_path}") - self.report({"INFO"}, f"HDR converted to foot-candles: {fc_hdr_path}") - - # Also generate TIFF version for convenient viewing - try: - print("Generating TIFF version of foot-candles image...") - pcond_fc_image = pr.pcond(hdr=fc_hdr_path, human=True) - fc_tiff_path = os.path.join(output_dir, f"{fc_output_name}.tiff") - print(f"Saving foot-candles TIFF to: {fc_tiff_path}") - pr.ra_tiff(inp=pcond_fc_image, out=fc_tiff_path, lzw=True) - print(f"TIFF version generated: {fc_tiff_path}") - self.report({"INFO"}, f"TIFF version also generated: {fc_tiff_path}") - except Exception as e: - print(f"Warning: Failed to generate foot-candles TIFF: {str(e)}") - self.report({"WARNING"}, f"TIFF generation failed: {str(e)}") - - print("Foot-candles conversion completed successfully.") - return {"FINISHED"} - except Exception as e: - error_msg = f"Failed to convert HDR to foot-candles: {str(e)}" + # Check if it's a falsecolor not found error + if "falsecolor" in str(e).lower() or isinstance(e, FileNotFoundError): + error_msg = "falsecolor not found! Please install Radiance and add it to your system PATH" + else: + error_msg = f"Failed to generate false color image: {str(e)}" + self.report({"ERROR"}, error_msg) print(f"ERROR: {error_msg}") print(f"Traceback:") diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index e614ef0cbc..f161c92e50 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -568,11 +568,25 @@ class RadianceExporterProperties(PropertyGroup): ) false_color_contour_lines: BoolProperty( - name="Contour Lines", + name="Enable Contour Lines", description="Add contour lines to the false color image", default=True, ) + false_color_contour_mode: EnumProperty( + name="Contour Mode", + description="Contour line display mode", + items=[ + ( + "WITH_BG", + "Contour Lines with Background", + "Show contour lines overlaid on the colored false color background", + ), + ("WITHOUT_BG", "Contour Lines Only", "Show only contour lines without the false color background"), + ], + default="WITH_BG", + ) + false_color_multiplier: FloatProperty( name="Multiplier", description="Conversion multiplier (179.0 for lux, 16.6295 for foot-candles)", @@ -587,19 +601,6 @@ class RadianceExporterProperties(PropertyGroup): default="false_color", ) - # HDR to foot-candles conversion properties - convert_hdr_to_fc: BoolProperty( - name="Convert to Foot-Candles", - description="Convert the main render HDR to foot-candles unit for illuminance verification", - default=False, - ) - - hdr_to_fc_output_name: StringProperty( - name="FC Output Name", - description="Name of the foot-candles converted HDR file (without extension)", - default="render_fc", - ) - if TYPE_CHECKING: is_exporting: bool category: str @@ -630,10 +631,9 @@ class RadianceExporterProperties(PropertyGroup): false_color_label: Literal["fc", "lux", "cd/m2"] false_color_scale: float false_color_contour_lines: bool + false_color_contour_mode: Literal["WITH_BG", "WITHOUT_BG"] false_color_multiplier: float false_color_output_name: str - convert_hdr_to_fc: bool - hdr_to_fc_output_name: str class BIMSolarProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index 9ede62baa9..b6ec4ef88e 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -241,6 +241,11 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row = box.row() row.prop(props, "false_color_contour_lines") + # Contour mode dropdown (only visible if contour lines enabled) + if props.false_color_contour_lines: + row = box.row() + row.prop(props, "false_color_contour_mode") + row = box.row() row.label(text="Multiplier") row.prop(props, "false_color_multiplier", text="") @@ -252,22 +257,6 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row = box.row() row.operator("render_scene.false_color_radiance", text="Generate False Color Image") - layout.separator() - - # Step 5: Convert to foot-candles - box = layout.box() - box.label(text="Step 5: Convert HDR to Foot-Candles") - row = box.row() - row.prop(props, "convert_hdr_to_fc") - - if props.convert_hdr_to_fc: - row = box.row() - row.label(text="Output Name") - row.prop(props, "hdr_to_fc_output_name", text="") - - row = box.row() - row.operator("render_scene.convert_hdr_to_fc", text="Convert to Foot-Candles") - class BIM_PT_solar(bpy.types.Panel): """Creates a Panel in the render properties window"""