Update for Blender 5.2

- use new api compositor access
- Fix file output nodes connexion/renumber/rename/slots handlibng
- consider the new output types
- fixes node position with new API
- fix errors with handler

LLM assisted update
This commit is contained in:
Pullusb
2026-09-29 18:57:26 +02:00
parent 02aa67fc26
commit d650ec8c98
12 changed files with 295 additions and 179 deletions
+1 -1
View File
@@ -244,7 +244,7 @@ class RT_OT_conform_collection_hierarchy(Operator):
for k, v in type_dict.items():
if item_type == "objects":
icon = f'OUTLINER_OB_{k.type}'
icon = fn.get_object_icon(k)
name = k.name
else:
icon = 'OUTLINER_COLLECTION'
+65 -62
View File
@@ -11,24 +11,16 @@ from .. import fn
# region renumber outputs
def delete_numbering(fo): # padding=3
'''Delete prefix numbering on all slots on passed file output'''
'''Delete prefix numbering on all items on passed file output'''
if fo.type != 'OUTPUT_FILE': return
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
slots = fo.layer_slots
field_attr = 'name'
else:
slots = fo.file_slots
field_attr = 'path'
for fs in slots:
elems = getattr(fs, field_attr).split('/')
for fs in fo.file_output_items:
elems = fs.name.split('/')
for i, e in enumerate(elems):
elems[i] = re.sub(r'^\d{3}_', '', e)
new = '/'.join(elems)
setattr(fs, field_attr, new)
fs.name = '/'.join(elems)
## Add - Insert prefix incrementation in fileoutput
@@ -43,9 +35,7 @@ def get_numbered_output(out, slot_name):
def add_fileslot_number(fs, number):
field_attr = 'name' if hasattr(fs, 'name') else 'path'
elems = getattr(fs, field_attr).split('/')
elems = fs.name.split('/')
for i, e in enumerate(elems):
if re.match(r'^\d{3}_', e):
elems[i] = re.sub(r'^(\d{3})', lambda x: str(number).zfill(3), e)
@@ -53,37 +43,34 @@ def add_fileslot_number(fs, number):
elems[i] = f'{str(number).zfill(3)}_{e}'
new = '/'.join(elems)
setattr(fs, field_attr, new)
fs.name = new
return new
def renumber(fo, offset=10):
'''Force renumber all the slots with a 3'''
'''Force renumber all the items with a 3'''
if fo.type != 'OUTPUT_FILE': return
ct = 10 # start at 10
slots = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots
for fs in slots:
for fs in fo.file_output_items:
add_fileslot_number(fs, ct)
ct += offset
def get_num(string) -> int:
'''get a tring or a file_slot object
'''get a string or a file output item object
return leading number or None
'''
if not isinstance(string, str):
if hasattr(string, 'path'):
string = string.path
else:
string = string.name
string = string.name
num = re.search(r'^(\d{3})_', string)
if num:
return int(num.group(1))
def reverse_fileout_inputs(fo):
count = len(fo.inputs)
items = fo.file_output_items
count = len(items)
for i in range(count):
fo.inputs.move(count-1, i)
items.move(count-1, i)
def renumber_keep_existing(fo, offset=10, invert=True):
'''Renumber by keeping existing numbers and inserting new one whenever possible
@@ -96,7 +83,7 @@ def renumber_keep_existing(fo, offset=10, invert=True):
if invert:
reverse_fileout_inputs(fo)
fsl = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots
fsl = fo.file_output_items
last_idx = len(fsl) - 1
prev = None
@@ -140,7 +127,7 @@ def renumber_keep_existing(fo, offset=10, invert=True):
if next_num is None:
add_fileslot_number(fs, 0)
elif next_num == 0:
print(f'Cannot insert value before 0 to {fsl.path}')
print(f'Cannot insert value before 0 to {fs.name}')
continue
else:
add_fileslot_number(fs, int(next_num / 2))
@@ -161,7 +148,7 @@ def renumber_keep_existing(fo, offset=10, invert=True):
# iterate rename
gap_inc = int((next_num - prev_num) / divider)
if gap_inc < 1: # same values !
print(f'cannot insert a median value at {fs.path} between {prev_num} and {next_num}')
print(f'cannot insert a median value at {fs.name} between {prev_num} and {next_num}')
continue
ct = prev_num
@@ -187,7 +174,7 @@ class RT_OT_number_outputs(Operator):
@classmethod
def poll(cls, context):
return True
return fn.get_compositor_tree(context.scene) is not None
mode : StringProperty(default='SELECTED', options={'SKIP_SAVE'})
clear : BoolProperty(default=False, options={'SKIP_SAVE'})
@@ -199,10 +186,8 @@ class RT_OT_number_outputs(Operator):
return self.execute(context)
def execute(self, context):
scn = context.scene
ct = 0
nodes = scn.node_tree.nodes
nodes = fn.get_compositor_tree(context.scene).nodes
for fo in nodes:
if fo.type != 'OUTPUT_FILE':
continue
@@ -234,9 +219,13 @@ class RT_OT_mute_toggle_output_nodes(Operator):
mute : BoolProperty(default=True, options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return fn.get_compositor_tree(context.scene) is not None
def execute(self, context):
ct = 0
for n in context.scene.node_tree.nodes:
for n in fn.get_compositor_tree(context.scene).nodes:
if n.type != 'OUTPUT_FILE':
continue
n.mute = self.mute
@@ -255,8 +244,12 @@ class RT_OT_set_output_node_format(Operator):
mute : BoolProperty(default=True, options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return fn.get_compositor_tree(context.scene) is not None
def execute(self, context):
nodes = context.scene.node_tree.nodes
nodes = fn.get_compositor_tree(context.scene).nodes
if not nodes.active or nodes.active.type != 'OUTPUT_FILE':
self.report({"ERROR"}, f'Active node should be an output file to use as reference for output format')
return {"CANCELLED"}
@@ -273,8 +266,14 @@ class RT_OT_set_output_node_format(Operator):
if n.type != 'OUTPUT_FILE' or n == ref or not n.select:
continue
## media type and file format first, available values of other attributes depend on them
n.format.media_type = ref.format.media_type
n.format.file_format = ref.format.file_format
for attr in dir(ref.format):
if attr.startswith('__') or attr in {'rna_type','bl_rna', 'view_settings', 'display_settings','stereo_3d_format'}: # views_format
if attr.startswith('__') or attr in {'rna_type','bl_rna', 'media_type', 'file_format',
'view_settings', 'display_settings', 'linear_colorspace_settings',
'has_linear_colorspace', 'stereo_3d_format'}: # views_format
continue
try:
setattr(n.format, attr, getattr(ref.format, attr))
@@ -295,36 +294,38 @@ class RT_OT_set_output_node_format(Operator):
class RT_OT_set_active_file_output_slot_to_composite(bpy.types.Operator):
bl_idname = "rt.set_active_file_output_slot_to_composite"
bl_label = "Set Active File Output Slot To Composite"
bl_description = "Use active slot of active file output node to set scene output settings (swap connection)"
bl_label = "Set Active File Output Item To Composite"
bl_description = "Use active item of active file output node to set scene output settings\
\nand swap connection to compositor Group Output node"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.scene.use_nodes\
and context.scene.node_tree\
and context.scene.node_tree.nodes.active\
and context.scene.node_tree.nodes.active.type == 'OUTPUT_FILE'
tree = fn.get_compositor_tree(context.scene)
return bool(tree\
and tree.nodes.active\
and tree.nodes.active.type == 'OUTPUT_FILE')
relink_composite : bpy.props.BoolProperty(
name='Relink Composite',
default=True,
description='In case file slot is linked, swap link to Composite file',
description='In case file output item is linked, swap link to compositor Group Output',
options={'SKIP_SAVE'},
)
def invoke(self, context, event):
self.fo = context.scene.node_tree.nodes.active
if not len(self.fo.file_slots):
self.report({'ERROR'}, 'no slots in active file output')
tree = fn.get_compositor_tree(context.scene)
self.fo = tree.nodes.active
if not len(self.fo.file_output_items):
self.report({'ERROR'}, 'no items in active file output')
return {'CANCELLED'}
# check if active slot has a source
if not self.fo.inputs[self.fo.active_input_index].is_linked:
# check if active item has a source
if not self.fo.inputs[self.fo.active_item_index].is_linked:
return self.execute(context)
# check if composite linked
out = context.scene.node_tree.nodes.get('Composite')
out = fn.get_group_output_node(tree)
if not out or not out.inputs[0].is_linked:
self.compo_out_from_link = ''
return self.execute(context)
@@ -337,34 +338,36 @@ class RT_OT_set_active_file_output_slot_to_composite(bpy.types.Operator):
def draw(self, context):
layout = self.layout
col = layout.column()
col.label(text=f'Composite node connected to: {self.compo_out_from_link}')
col.label(text=f'Would you like to replace by file output slot source ?')
col.label(text=f'Group Output node connected to: {self.compo_out_from_link}')
col.label(text=f'Would you like to replace by file output item source ?')
layout.prop(self, 'relink_composite')
def execute(self, context):
self.fo = fn.get_compositor_tree(context.scene).nodes.active
if not len(self.fo.file_output_items):
self.report({'ERROR'}, 'no items in active file output')
return {'CANCELLED'}
# if comp
fn.set_scene_output_from_active_fileout_item()
idx = self.fo.active_input_index
sl = self.fo.file_slots[idx]
idx = self.fo.active_item_index
item = self.fo.file_output_items[idx]
sk = self.fo.inputs[idx]
if not sk.is_linked:
self.report({'INFO'}, f'Outut changed to match {sl.path} (slot was not linked)')
self.report({'INFO'}, f'Outut changed to match {item.name} (item was not linked)')
return {'FINISHED'}
## If linked replace links to Composite node
## If linked replace links to Group Output node
if not self.relink_composite:
return {'FINISHED'}
ntree = context.scene.node_tree
links = context.scene.node_tree.links
nodes = context.scene.node_tree.nodes
ntree = fn.get_compositor_tree(context.scene)
links = ntree.links
out = nodes.get('Composite')
if not out:
out = fn.create_node('COMPOSITE', tree=ntree)
out = fn.get_group_output_node(ntree, create=True)
fo_loc = fn.real_loc(self.fo)
out.location = (fo_loc.x, fo_loc.y + 160)
out.location_absolute = (fo_loc.x, fo_loc.y + 160)
# if out.inputs[0].is_linked:
# self.report({'WARNING'}, f'Outut changed to match {sl.path} (Composite node already linked)')
+22 -29
View File
@@ -10,7 +10,7 @@ from bpy.props import (StringProperty,
class RT_OT_outputs_search_and_replace(bpy.types.Operator):
bl_idname = "rt.outputs_search_and_replace"
bl_label = "Search And Replace Outputs Paths"
bl_description = "Search/Replace texts in output path and slots"
bl_description = "Search/Replace texts in output paths (directory and file name) and items"
bl_options = {'REGISTER', 'UNDO'}
## Search and replace options
@@ -22,9 +22,9 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
name="Target Fields",
description="Fields to search and replace in outputs",
items=(
('path', "Base Paths", "search and replace in output node paths"),
('slots', "File Slots", "search and replace in output node file slots (also Layer slots for multilayers outputs)"),
('all', "All", "search and replace in both paths and slots"),
('path', "Paths", "search and replace in output node paths (directory and file name, separately)"),
('slots', "Items", "search and replace in output node items (subpaths, or layer names for multilayers outputs)"),
('all', "All", "search and replace in both paths and items"),
),
default='all'
)
@@ -46,7 +46,8 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.node_tree and context.scene.node_tree.nodes
tree = fn.get_compositor_tree(context.scene)
return bool(tree and tree.nodes)
def rename(self, source):
if not self.find:
@@ -133,12 +134,12 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
# return
## Checked in poll
node_tree = context.scene.node_tree
node_tree = fn.get_compositor_tree(context.scene)
file_outputs = [f for f in node_tree.nodes if f.type == 'OUTPUT_FILE']
if self.selected_node_only:
file_outputs = [f for f in file_outputs if f.select]
if not file_outputs:
self.report({'ERROR'}, 'No file output nodes found')
return {'CANCELLED'}
@@ -148,29 +149,21 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
for file_out in file_outputs:
## Get the target prop
if self.target in ('path', 'all'):
current_base_path = file_out.base_path
new_base_path = self.rename(current_base_path)
if new_base_path is not None and current_base_path != new_base_path:
print(f"\n{file_out.name}: base path: {current_base_path} to {new_base_path}")
file_out.base_path = new_base_path
count += 1
for attr in ('directory', 'file_name'):
current_path = getattr(file_out, attr)
new_path = self.rename(current_path)
if new_path is not None and current_path != new_path:
print(f"\n{file_out.name}: {attr}: {current_path} to {new_path}")
setattr(file_out, attr, new_path)
count += 1
if self.target in ('slots', 'all'):
for slot in file_out.file_slots:
current_slot_path = slot.path
new_slot_path = self.rename(current_slot_path)
if new_slot_path is not None and current_slot_path != new_slot_path:
print(f"{file_out.name}: file slot: {current_slot_path} to {new_slot_path}")
slot.path = new_slot_path
count += 1
## Layers slots
for layer in file_out.layer_slots:
current_layer_path = layer.name
new_layer_path = self.rename(current_layer_path)
if new_layer_path is not None and current_layer_path != new_layer_path:
print(f"{file_out.name}: layer slot: {current_layer_path} to {new_layer_path}")
layer.name = new_layer_path
for item in file_out.file_output_items:
current_item_name = item.name
new_item_name = self.rename(current_item_name)
if new_item_name is not None and current_item_name != new_item_name:
print(f"{file_out.name}: item: {current_item_name} to {new_item_name}")
item.name = new_item_name
count += 1
if count:
+23 -5
View File
@@ -324,9 +324,16 @@ class RT_OT_create_output_layers(bpy.types.Operator):
self.socket_collection = context.window_manager.rt_socket_collection
self.socket_collection.clear()
tree = fn.get_compositor_tree(context.scene)
if tree is None:
self.report({'ERROR'}, 'Scene has no compositing node group')
return {'CANCELLED'}
if event.ctrl:
# Direct connect, do not use any options
self.base_path = ''
self.final_base_path_template = None
return self.execute(context)
## Store variable usable in templates
@@ -344,7 +351,7 @@ class RT_OT_create_output_layers(bpy.types.Operator):
tech_passes_names = [tp.strip().lower() for tp in tech_passes_names.split(',') if tp.strip()]
selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE']
selected = [n for n in tree.nodes if n.select and n.type != 'OUTPUT_FILE']
if not selected:
self.report({'ERROR'}, 'No render layer nodes selected')
return {'CANCELLED'}
@@ -411,9 +418,21 @@ class RT_OT_create_output_layers(bpy.types.Operator):
if ref.type == 'R_LAYERS':
self.scene_name = ref.scene.name
self.view_layer_name = ref.layer
self.final_base_path_template = ''
## Set here too, since draw only update it when settings are expanded
self.final_base_path_template = self.get_base_path_template(context)
return context.window_manager.invoke_props_dialog(self, width=530)
def get_base_path_template(self, context):
'''Return base path template from scene settings, None if templates are disabled'''
if not self.use_base_path_templates:
return None
final_format = self.file_format
if final_format == 'NONE':
final_format = os.environ.get('FILE_FORMAT', 'OPEN_EXR_MULTILAYER')
if final_format == 'OPEN_EXR_MULTILAYER':
return context.scene.render_toolbox.default_multilayer_base_path
return context.scene.render_toolbox.default_base_path
def draw(self, context):
layout = self.layout
box = layout.box()
@@ -530,7 +549,7 @@ Possible variables:
display_node_name = item.node_name
## A bit dirty: For render layer node, show render layer-node name.
if display_node_name.startswith('Render Layer'):
rln = context.scene.node_tree.nodes.get(item.node_name)
rln = fn.get_compositor_tree(context.scene).nodes.get(item.node_name)
display_node_name = f'{rln.scene.name}/{rln.layer} ({display_node_name})'
col.label(text=display_node_name, icon='NODE_SEL')
@@ -623,8 +642,7 @@ Possible variables:
'color_depth' : '32',
}
scn = context.scene
nodes = scn.node_tree.nodes
nodes = fn.get_compositor_tree(context.scene).nodes
selected = [n for n in nodes if n.select]
outfile = next((n for n in selected if n.type == 'OUTPUT_FILE'), None)
# Exclude output file from
+4 -2
View File
@@ -7,6 +7,8 @@ from bpy.props import (BoolProperty,
CollectionProperty,
StringProperty)
from .. import fn
def list_simplify_affected_objects(context):
"""Return a list of object affected by simplify
return : [[obj, [{type: "SUBSURF", ..}, {..}], icon], ..]
@@ -103,7 +105,7 @@ def list_simplify_affected_objects(context):
# If object has affected data, add it to the list
if affected_data:
ob_list.append([obj, affected_data, "OUTLINER_OB_" + obj.type])
ob_list.append([obj, affected_data, fn.get_object_icon(obj)])
return ob_list
@@ -288,7 +290,7 @@ class RT_OT_list_object_affected_by_simplify(Operator):
# Create subrow for hair info
subrow = row.row()
subrow.label(text=f"Hair: {mod.name}", icon='HAIR')
subrow.label(text=f"Hair: {mod.name}", icon='CURVES_DATA')
subrow.label(text=f"Node Group: {data['node_group']}")
elif data['type'] == 'SUBSURFACE':
+7 -1
View File
@@ -5,6 +5,8 @@ from bpy.props import (StringProperty,
BoolProperty,
)
from .. import fn
class RT_OT_enable_all_viewlayers(Operator):
bl_idname = "rt.enable_all_viewlayers"
bl_label = "Enable All Viewlayers"
@@ -27,10 +29,14 @@ class RT_OT_activate_only_selected_layers(Operator):
bl_description = "Activate only selected node view layer , excluding all others"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return fn.get_compositor_tree(context.scene) is not None
def execute(self, context):
scn = context.scene
nodes = scn.node_tree.nodes
nodes = fn.get_compositor_tree(scn).nodes
rlayers_nodes = [n for n in nodes if n.select and n.type == 'R_LAYERS']
vls = [scn.view_layers.get(n.layer) for n in rlayers_nodes if scn.view_layers.get(n.layer)]
+1 -1
View File
@@ -425,7 +425,7 @@ class RT_OT_list_modifier_visibility(Operator):
for m in o.modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
self.ob_list.append([o, mods, fn.get_object_icon(o)])
mods.append(m)
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
return context.window_manager.invoke_props_dialog(self, width=350)