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
+154 -66
View File
@@ -11,31 +11,84 @@ def get_addon_prefs():
# region Manage nodes
def get_compositor_tree(scene=None):
'''Return the compositing node group of passed scene (active scene if not passed)
None if the scene has no compositing node group
'''
scene = scene or bpy.context.scene
return scene.compositing_node_group
def get_group_output_node(tree, create=False):
'''Return the active group output node of the tree (compositor output since Blender 5.0)
create: create the node, and an "Image" output socket in tree interface, if missing
'''
out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT' and n.is_active_output), None)
if out is None:
out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT'), None)
if not create:
return out
if not any(i.item_type == 'SOCKET' and i.in_out == 'OUTPUT' for i in tree.interface.items_tree):
tree.interface.new_socket(name='Image', in_out='OUTPUT', socket_type='NodeSocketColor')
if out is None:
out = tree.nodes.new('NodeGroupOutput')
return out
def real_loc(n):
if not n.parent:
return n.location
return n.location + real_loc(n.parent)
return n.location_absolute
def get_media_type(file_format):
'''Return the media type corresponding to passed file format'''
if file_format == 'OPEN_EXR_MULTILAYER':
return 'MULTI_LAYER_IMAGE'
if file_format == 'FFMPEG':
return 'VIDEO'
return 'IMAGE'
def set_format_settings(fmt, settings):
'''Apply a dict of attributes to an image format settings (e.g: file output node format)
media_type and file_format are set first, available values of other attributes depend on them
'''
settings = dict(settings)
file_format = settings.pop('file_format', None)
media_type = settings.pop('media_type', None)
if file_format and not media_type:
media_type = get_media_type(file_format)
if media_type:
fmt.media_type = media_type
if file_format:
fmt.file_format = file_format
for k, v in settings.items():
setattr(fmt, k, v)
def set_scene_output_from_active_fileout_item():
scn = bpy.context.scene
rd = scn.render
ntree = scn.node_tree
ntree = get_compositor_tree(scn)
fo = ntree.nodes.active
if fo.type != 'OUTPUT_FILE':
return
sl = fo.file_slots[fo.active_input_index]
full_path = os.path.join(fo.base_path, sl.path)
item = fo.file_output_items[fo.active_item_index]
if fo.format.media_type == 'MULTI_LAYER_IMAGE':
## All items are layers of the same file
full_path = os.path.join(fo.directory, fo.file_name)
fmt = fo.format
else:
full_path = os.path.join(fo.directory, fo.file_name + item.name)
fmt = item.format if item.override_node_format else fo.format
rd.filepath = full_path
fmt = fo.format if sl.use_node_format else sl.format
## set those attr first to avoid error settings other attributes in next loop
rd.image_settings.media_type = fmt.media_type
rd.image_settings.file_format = fmt.file_format
rd.image_settings.color_mode = fmt.color_mode
rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else 8 # Force set since Sometimes it's weirdly set to "" (not in enum choice)
rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else '8' # Force set since Sometimes it's weirdly set to "" (not in enum choice)
excluded = ['file_format', 'color_mode', 'color_depth',
excluded = ['media_type', 'file_format', 'color_mode', 'color_depth',
'view_settings', 'views_format']
''' ## all attrs
@@ -68,21 +121,61 @@ def set_file_output_format(fo):
'color_mode': 'RGBA'
}
for k, v in env_file_format.items():
setattr(fo.format, k, v)
set_format_settings(fo.format, env_file_format)
def set_file_output_path(fo, path, suffix=''):
'''Set directory and file name of a file output node from a single path
Multilayer: last element is the file name, ex: "//render/name/name_"
Other formats: whole path is the directory, ex: "//render/name/"
suffix: optional subfolder added at the end of the directory, ex: "tech/"
'''
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
directory, file_name = re.match(r'(.*[\\/])?(.*)', path).groups()
directory = directory or ''
else:
directory, file_name = path, ''
if suffix:
directory = directory.rstrip('\\/') + '/' + suffix if directory else suffix
fo.directory = directory
fo.file_name = file_name
## Source socket type to file output item socket type
FILE_OUTPUT_ITEM_TYPES = {
'VALUE': 'FLOAT',
'INT': 'INT',
'BOOLEAN': 'BOOLEAN',
'VECTOR': 'VECTOR',
'RGBA': 'RGBA',
}
def add_file_output_item(fo, name, socket=None):
'''Add an item to file output node
socket: source socket, if passed item type (and vector dimensions) match the socket
return created item and its input socket
'''
socket_type = FILE_OUTPUT_ITEM_TYPES.get(socket.type, 'RGBA') if socket is not None else 'RGBA'
item = fo.file_output_items.new(socket_type, name)
if socket_type == 'VECTOR' and hasattr(socket, 'default_value'):
## ex: render layer "Vector" pass is 4D
item.vector_socket_dimensions = len(socket.default_value)
## items inputs are in the same order (followed by a virtual socket)
return item, fo.inputs[len(fo.file_output_items) - 1]
def clear_disconnected(fo):
'''Remove unlinked inputs from file output node'''
for inp in reversed(fo.inputs):
if not inp.is_linked:
print(f'Deleting unlinked fileout slot: {inp.name}')
fo.inputs.remove(inp)
'''Remove unlinked items from file output node'''
items = fo.file_output_items
for i in reversed(range(len(items))):
if not fo.inputs[i].is_linked:
print(f'Deleting unlinked fileout item: {items[i].name}')
items.remove(items[i])
def create_node(type, tree=None, **kargs):
'''Get a type, a tree to add in, and optionnaly multiple attribute to set
return created node
'''
tree = tree or bpy.context.scene.node_tree
tree = tree or get_compositor_tree()
node = tree.nodes.new(type)
for k,v in kargs.items():
@@ -129,79 +222,68 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
"""
if not outputs:
return None
nodes = scene.node_tree.nodes
links = scene.node_tree.links
tree = get_compositor_tree(scene)
nodes = tree.nodes
links = tree.links
fo = file_out
if not fo:
fo = nodes.get(out_name)
if not fo:
fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree,
location=(real_loc(node)[0] + location_offset[0],
real_loc(node)[1] + location_offset[1]), width=600)
fo.inputs.remove(fo.inputs[0]) # Remove default image input
fo = create_node('CompositorNodeOutputFile', tree=tree, width=600)
fo.file_output_items.clear() # Ensure there is no default item
if file_format:
for k, v in file_format.items():
setattr(fo.format, k, v)
set_format_settings(fo.format, file_format)
else:
set_file_output_format(fo)
if color_depth:
fo.format.color_depth = color_depth
fo.name = out_name
if node.parent:
fo.parent = node.parent
fo.location_absolute = (real_loc(node)[0] + location_offset[0],
real_loc(node)[1] + location_offset[1])
if base_path_template is not None:
fo.base_path = build_path_from_template(base_path_template,
node_name=node.name.strip(),
node_label=node.label.strip(),
scene_name=node.scene.name if node.type == 'R_LAYERS' else None,
viewlayer_name=node.layer if node.type == 'R_LAYERS' else None,
)
if suffix:
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
## insert suffix right after last slash as folder
path_list = re.split('(\\/)', fo.base_path)
path_list.insert(-1, suffix)
fo.base_path = ''.join(path_list)
else:
## Ensure slash separator then add suffix
fo.base_path = fo.base_path.rstrip('\\/') + '/' + suffix
else:
path = build_path_from_template(base_path_template,
node_name=node.name.strip(),
node_label=node.label.strip(),
scene_name=node.scene.name if node.type == 'R_LAYERS' else None,
viewlayer_name=node.layer if node.type == 'R_LAYERS' else None,
)
elif base_path:
path = base_path
elif fo.format.file_format == 'OPEN_EXR_MULTILAYER':
# default behavior without template
if base_path:
fo.base_path = base_path
else:
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
fo.base_path = f'//render/{out_base}/{suffix}{out_base}_'
else:
fo.base_path = f'//render/{out_base}/{suffix}'
path = f'//render/{out_base}/{out_base}_'
else:
path = f'//render/{out_base}/'
## suffix is added as a subfolder of the directory
set_file_output_path(fo, path, suffix=suffix)
for o in outputs:
if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
continue
if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)):
slot_name = bpy.path.clean_name(custom_name)
else:
slot_name = bpy.path.clean_name(o.name)
fs = fo.file_slots.new('tmp')
ls = fo.layer_slots.new('tmp')
## Multilayer: item name is the layer name in the file
## Other formats: item name is the subpath added to directory + file name
if fo.format.file_format == 'OPEN_EXR_MULTILAYER' or use_slot_template:
## use_slot_template always False (currently never use slot template)
item_name = slot_name
else:
item_name = f'{slot_name}/{slot_name}_'
ls = fo.layer_slots[-1]
ls.name = slot_name
fs = fo.file_slots[-1]
fs.path = f'{slot_name}/{slot_name}_'
if use_slot_template:
## Always False (currently never use slot template)
fs.path = slot_name
out_input = fo.inputs[-1]
_item, out_input = add_file_output_item(fo, item_name, socket=o)
links.new(o, out_input)
clear_disconnected(fo)
@@ -451,6 +533,12 @@ def set_properties_editor_tab(tab, skip_if_exists=True):
return 1
def get_object_icon(obj):
'''Return outliner icon name of passed object'''
if obj.type == 'LIGHT_PROBE':
return 'OUTLINER_OB_LIGHTPROBE'
return f'OUTLINER_OB_{obj.type}'
def show_and_active_object(obj, make_active=True, select=True, unhide=True):
'''
Show the object
@@ -685,7 +773,7 @@ def store_visibility_states(collection=None):
def store_collection_states(collection, context=None):
'''Pass a colelction and store visibility state'''
'''Pass a collection and store visibility state'''
context = context or bpy.context
scene = context.scene