2025-12-01
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user