# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors # # SPDX-License-Identifier: GPL-3.0-or-later import bpy import re import idprop from . import utils from .categories import rna_overrides def struct_from_rna_path(rna_path): ''' Returns struct object for specified rna path. ''' if not '.' in rna_path: return None elements = rna_path.rsplit('.', 1) if '][' in elements[1]: struct_path = f"{elements[0]}.{elements[1]}" else: struct_path = f"{elements[0]}.bl_rna.properties['{elements[1]}']" try: return eval(struct_path) except: return None def stylize_name(path): ''' Splits words by '_', capitalizes them and separates them with ' '. ''' custom_prop = utils.parse_rna_path_for_custom_property(path) if custom_prop: return f"{eval(custom_prop[0]+'.name')}: {custom_prop[1]}" path_elements = utils.parse_rna_path_to_elements(path) parent = '.'.join(path_elements[:-1]) main = path_elements[-1] try: if main in ['default_value']: return eval(parent).name else: return f"{eval(parent).name}: {' '.join([word.capitalize() for word in main.split('_')])}" except: return ' '.join([word.capitalize() for word in main.split('_')]) class LOR_OT_override_picker(bpy.types.Operator): """Adds an operator on button mouse hover""" bl_idname = "lighting_overrider.override_picker" bl_label = "Add RNA Override" bl_options = {'UNDO'} rna_path: bpy.props.StringProperty(name="Data path to override", default="") override_float: bpy.props.FloatProperty(name="Override", default=0) batch_override: bpy.props.BoolProperty(name="Batch Override", default=False, options={'SKIP_SAVE'}) init_val = None property = None override = None name_string = 'RNA Override' type = 'VALUE' _array_path_re = re.compile(r'^(.*)\[[0-9]+\]$') @classmethod def poll(cls, context): #print(f'poll: {bpy.ops.ui.copy_data_path_button.poll()}') return True def draw(self, context): layout = self.layout property = self.property if property is None: return col = layout.column() col.scale_y = 1.8 col.scale_x = 1.5 if self.type == 'COLOR': col.template_color_picker(context.scene, 'override', value_slider=True) col = layout.column() col.prop(context.scene, 'override') self.override = context.scene['override'] if self.batch_override: row = layout.row() row.alert = True row.label(text=f'Batch Overriding {len(context.selected_objects)} Objects', icon='DOCUMENTS') def invoke(self, context, event): if not bpy.ops.ui.copy_data_path_button.poll(): return {'PASS_THROUGH'} clip = context.window_manager.clipboard bpy.ops.ui.copy_data_path_button(full_path=True) rna_path = context.window_manager.clipboard context.window_manager.clipboard = clip if rna_path.endswith('name'): print("Warning: Don't override datablock names.") return {'CANCELLED'} if not rna_path.startswith('bpy.data.objects'): self.batch_override = False # Strip off array indices (f.e. 'a.b.location[0]' -> 'a.b.location') m = self._array_path_re.match(rna_path) if m: rna_path = m.group(1) self.rna_path = rna_path self.property = struct_from_rna_path(rna_path) if self.property is None: print("Warning: No struct was found for given RNA path.") return {'CANCELLED'} self.name_string = stylize_name(self.rna_path) if 'type' in dir(self.property): # Gather UI data keys = ['description', 'default', 'min', 'max', 'soft_min', 'soft_max', 'step', 'precision', 'subtype'] vars = {} for key in keys: try: vars[key] = eval(f'self.property.{key}') except: print(f'{key} not in property') if self.property.type == 'FLOAT': vars['unit'] = self.property.unit if not self.property.is_array: bpy.types.Scene.override = bpy.props.FloatProperty(name = self.name_string, **vars) else: vars['size'] = self.property.array_length vars['default'] = self.property.default_array[:] if vars['subtype'] == 'COLOR': self.type = 'COLOR' else: self.type = 'VECTOR' bpy.types.Scene.override = bpy.props.FloatVectorProperty(name = self.name_string, **vars) elif self.property.type == 'STRING': bpy.types.Scene.override = bpy.props.StringProperty(name = self.name_string, **vars) self.type = 'STRING' elif self.property.type == 'BOOLEAN': bpy.types.Scene.override = bpy.props.BoolProperty(name = self.name_string, **vars) self.type = 'BOOL' elif self.property.type == 'INT': bpy.types.Scene.override = bpy.props.IntProperty(name = self.name_string, **vars) self.type = 'INTEGER' elif self.property.type == 'ENUM': self.type = 'STRING' items = [(item.identifier, item.name, item.description, item.icon, i) for i, item in enumerate(self.property.enum_items)] vars.pop('subtype', None) bpy.types.Scene.override = bpy.props.EnumProperty(items = items, name = self.name_string, **vars) else: vars = {} custom_prop = utils.parse_rna_path_for_custom_property(self.rna_path) if custom_prop: data_block = eval(custom_prop[0]) property_name = custom_prop[1] vars = data_block.id_properties_ui(property_name).as_dict() if type(self.property) is float: bpy.types.Scene.override = bpy.props.FloatProperty(name = self.name_string, **vars) elif type(self.property) is idprop.types.IDPropertyArray: bpy.types.Scene.override = bpy.props.FloatVectorProperty(name = self.name_string, size=len(self.property), **vars) if vars['subtype'] in ['COLOR', 'COLOR_GAMMA']: self.type = 'COLOR' else: self.type = 'VECTOR' elif type(self.property) is str: bpy.types.Scene.override = bpy.props.StringProperty(name = self.name_string, **vars) self.type = 'STRING' elif type(self.property) is int: bpy.types.Scene.override = bpy.props.IntProperty(name = self.name_string, **vars) self.type = 'INTEGER' elif type(self.property) == bool: bpy.types.Scene.override = bpy.props.BoolProperty(name = self.name_string, **vars) self.type = 'BOOL' # check for custom property context.scene.override = eval(rna_path) self.override = context.scene.override self.init_val = eval(rna_path) wm = context.window_manager state = wm.invoke_props_dialog(self) if state in {'FINISHED', 'CANCELLED'}: del context.scene['override'] return state else: return state def execute(self, context): meta_settings = context.scene.LOR_Settings settings = utils.get_settings(meta_settings) path_elements = utils.parse_rna_path_to_elements(self.rna_path) if context.scene.override==self.init_val: del context.scene['override'] return {'CANCELLED'} utils.mute_animation_on_rna_path(self.rna_path) if type(eval(self.rna_path)) == idprop.types.IDPropertyArray: exec(self.rna_path+f'[:] = context.scene.override') # workaround for Blender not retaining UI data of property (see https://projects.blender.org/blender/blender/pulls/109203) else: exec(self.rna_path+f' = context.scene.override') add_info=[self.name_string, self.rna_path, context.scene.override, self.type] rna_overrides.add_rna_override(context, add_info) # TODO deduplicate with utils if path_elements[2].startswith('objects'): db_path = '.'.join(path_elements[:3]) if 'session_uid' in dir(eval(db_path)): data_block = eval(db_path) subpath = f'.{".".join(path_elements[3:])}' else: # handle custom props db_path, c_prop = utils.parse_rna_path_for_custom_property(self.rna_path) data_block = eval(db_path) subpath = f'["{c_prop}"]' data_block.update_tag() if not self.batch_override: del context.scene['override'] return {'FINISHED'} for ob in context.selected_objects: if ob.library: rna_path = f'bpy.data.objects["{ob.name}", "{ob.library.filepath}"]{subpath}' else: rna_path = f'bpy.data.objects["{ob.name}"]{subpath}' utils.mute_animation_on_rna_path(rna_path) try: eval(rna_path) except: continue if type(eval(rna_path)) == idprop.types.IDPropertyArray: exec(rna_path+f'[:] = context.scene.override') # workaround for Blender not retaining UI data of property (see https://projects.blender.org/blender/blender/pulls/109203) else: exec(rna_path+f' = context.scene.override') name_string = stylize_name(rna_path) add_info = [name_string, rna_path, context.scene.override, self.type] rna_overrides.add_rna_override(context, add_info) utils.kick_evaluation(list(context.selected_objects)) del context.scene['override'] utils.kick_evaluation() return {'FINISHED'} def cancel(self, context): del context.scene['override'] return classes = [ LOR_OT_override_picker, ] def register(): for c in classes: bpy.utils.register_class(c) wm = bpy.context.window_manager if wm.keyconfigs.addon is not None: km = wm.keyconfigs.addon.keymaps.new(name="User Interface") kmi = km.keymap_items.new("lighting_overrider.override_picker","O", "PRESS",shift=False, ctrl=False) kmi.properties.batch_override = False kmi = km.keymap_items.new("lighting_overrider.override_picker","O", "PRESS",shift=False, ctrl=False, alt=True) kmi.properties.batch_override = True def unregister(): for c in classes: bpy.utils.unregister_class(c) if __name__ == "__main__": register()