800 lines
29 KiB
Python
Executable File
800 lines
29 KiB
Python
Executable File
import bpy
|
|
import os
|
|
import re
|
|
import json
|
|
|
|
from .constant import TECH_PASS_KEYWORDS
|
|
from pathlib import Path
|
|
|
|
|
|
# region Manage nodes
|
|
|
|
def real_loc(n):
|
|
if not n.parent:
|
|
return n.location
|
|
return n.location + real_loc(n.parent)
|
|
|
|
def set_scene_output_from_active_fileout_item():
|
|
scn = bpy.context.scene
|
|
rd = scn.render
|
|
ntree = scn.node_tree
|
|
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)
|
|
|
|
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.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)
|
|
|
|
excluded = ['file_format', 'color_mode', 'color_depth',
|
|
'view_settings', 'views_format']
|
|
|
|
''' ## all attrs
|
|
# 'cineon_black', 'cineon_gamma', 'cineon_white',
|
|
# 'color_depth', 'color_mode', 'compression', 'display_settings',
|
|
# 'exr_codec', 'file_format', 'jpeg2k_codec', 'quality',
|
|
# 'rna_type', 'stereo_3d_format', 'tiff_codec', 'use_cineon_log',
|
|
# 'use_jpeg2k_cinema_48', 'use_jpeg2k_cinema_preset', 'use_jpeg2k_ycc',
|
|
# 'use_preview', 'use_zbuffer']
|
|
'''
|
|
|
|
for attr in dir(fmt):
|
|
if attr.startswith('__') or attr.startswith('bl_') or attr in excluded:
|
|
continue
|
|
if hasattr(scn.render.image_settings, attr) and not scn.render.image_settings.is_property_readonly(attr):
|
|
setattr(scn.render.image_settings, attr, getattr(fmt, attr))
|
|
|
|
def set_file_output_format(fo):
|
|
"""Default fileout format for output file node
|
|
Get from OUTPUT_RENDER_FILE_FORMAT environment variable
|
|
else fallback to multilayer EXR
|
|
"""
|
|
|
|
env_file_format = json.loads(os.environ.get('OUTPUT_RENDER_FILE_FORMAT', '{}'))
|
|
if not env_file_format:
|
|
env_file_format = {
|
|
'file_format': 'OPEN_EXR_MULTILAYER',
|
|
'exr_codec': 'ZIP',
|
|
'color_depth': '16',
|
|
'color_mode': 'RGBA'
|
|
}
|
|
|
|
for k, v in env_file_format.items():
|
|
setattr(fo.format, k, v)
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
node = tree.nodes.new(type)
|
|
for k,v in kargs.items():
|
|
setattr(node, k, v)
|
|
|
|
return node
|
|
|
|
def recursive_node_connect_check(l, target_node):
|
|
'''Get a link and a node
|
|
return True if link is connected to passed target_node further in the node tree
|
|
'''
|
|
if l.to_node == target_node:
|
|
return True
|
|
for o in l.to_node.outputs:
|
|
for sl in o.links:
|
|
if recursive_node_connect_check(sl, target_node):
|
|
return True
|
|
return False
|
|
|
|
|
|
# region create and connect file output
|
|
|
|
def create_and_connect_file_output(node, outputs, file_out, out_name, base_path, out_base,
|
|
scene, remap_names, file_format, suffix='', color_depth=None,
|
|
location_offset=(500, 50), base_path_template=None, use_slot_template=False):
|
|
"""Helper function to create file output node and connect it to outputs
|
|
|
|
Args:
|
|
node: Source node
|
|
outputs: List of outputs to connect
|
|
file_out: Existing file output node or None
|
|
out_name: Name for the file output node
|
|
base_path: Base path for output files
|
|
out_base: Base name for output files
|
|
scene: Blender scene
|
|
remap_names: Dictionary for remapping output names
|
|
file_format: Format settings
|
|
suffix: Optional suffix for paths
|
|
color_depth: Optional override for color depth
|
|
location_offset: Offset for the node position
|
|
|
|
Returns:
|
|
bpy.types.CompositorNode: Created or used file output node
|
|
"""
|
|
if not outputs:
|
|
return None
|
|
|
|
nodes = scene.node_tree.nodes
|
|
links = scene.node_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
|
|
|
|
if file_format:
|
|
for k, v in file_format.items():
|
|
setattr(fo.format, k, v)
|
|
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
|
|
|
|
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:
|
|
# 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}'
|
|
|
|
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')
|
|
|
|
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]
|
|
links.new(o, out_input)
|
|
|
|
clear_disconnected(fo)
|
|
fo.update()
|
|
return fo
|
|
|
|
|
|
def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None,
|
|
file_format=None, split_tech_passes=False, template=None):
|
|
"""Connect selected nodes output to file output(s)
|
|
if a file output is selected, add intputs on it
|
|
|
|
Args:
|
|
node_list (list[bpy.types.Nodes,]): Nodes to connect
|
|
|
|
file_out (bpy.types.CompositorNode, optional): File output node to connect to instead of new
|
|
Defaults to None
|
|
base_path (str, optional): Directory of images to render.
|
|
if not passed, will use source node layer name
|
|
Defaults to ''.
|
|
|
|
file_format (dict, optionnal): converts each dictionary key into a file output format
|
|
attribute and assigns the corresponding value.
|
|
Defaults to None.
|
|
|
|
excludes (dict, optionnal): List of output names to exclude {node_name: [outputs,]}.
|
|
Defaults toNone.
|
|
|
|
remap_names (dict, optionnal): List of output names to remap {node_name: {output_name: new_name}}.
|
|
|
|
split_tech_passes (bool, optional): When True, create a separate file output for technical passes
|
|
Defaults to False.
|
|
|
|
Returns:
|
|
list[bpy.types.CompositorNode]: All nodes created.
|
|
"""
|
|
|
|
scene = bpy.context.scene
|
|
if not isinstance(node_list, list):
|
|
node_list = [node_list]
|
|
node_list = [n for n in node_list if n.type != 'OUTPUT_FILE']
|
|
if not node_list:
|
|
return []
|
|
|
|
created_nodes = []
|
|
excludes = excludes or {}
|
|
remap_names = remap_names or {}
|
|
|
|
|
|
tech_file_format = file_format.copy() if file_format else None
|
|
|
|
if tech_file_format:
|
|
## TODO: Set to EXR if provioded is not ERX or EXR multilayer ?
|
|
# ->> if tech_file_format.get('file_format')) is not in ('OPEN_EXR', 'OPEN_EXR_MULTILAYER'): ...
|
|
|
|
## Force 32 bit
|
|
tech_file_format['color_depth'] = '32'
|
|
## Enforce a lossless format if provided is not
|
|
if (codec := tech_file_format.get('exr_codec')) and codec not in ['ZIP','PIZ','RLE','ZIPS']:
|
|
tech_file_format['exr_codec'] = 'ZIP'
|
|
|
|
for node in node_list:
|
|
exclusions = excludes.get(node.name) or []
|
|
|
|
# Get all available outputs excluding those in exclusions
|
|
all_outputs = [o for o in node.outputs if not o.is_unavailable and o.name not in exclusions]
|
|
|
|
# Base name for output nodes
|
|
if node.label:
|
|
out_base = node.label
|
|
else:
|
|
out_base = node.name
|
|
out_base = bpy.path.clean_name(out_base)
|
|
|
|
# Categorize outputs
|
|
crypto_outputs = [o for o in all_outputs if 'crypto' in o.name.lower()]
|
|
|
|
if split_tech_passes:
|
|
# Filter tech passes
|
|
tech_outputs = [o for o in all_outputs if o.name.lower() in TECH_PASS_KEYWORDS] # any(keyword in o.name.lower() for keyword in TECH_PASS_KEYWORDS)]
|
|
# Regular outputs (excluding crypto and tech passes)
|
|
regular_outputs = [o for o in all_outputs if o not in crypto_outputs and o not in tech_outputs]
|
|
else:
|
|
# If not splitting tech passes, include them with regular outputs
|
|
regular_outputs = [o for o in all_outputs if o not in crypto_outputs]
|
|
tech_outputs = []
|
|
|
|
y_offset = 50
|
|
node_margin = 100
|
|
# Create and connect regular outputs
|
|
if regular_outputs:
|
|
out_name = f'OUT_{out_base}'
|
|
fo_regular = create_and_connect_file_output(
|
|
node, regular_outputs, file_out, out_name, base_path,
|
|
out_base, scene, remap_names, file_format,
|
|
location_offset=(500, y_offset),
|
|
base_path_template=template
|
|
)
|
|
if fo_regular and fo_regular not in created_nodes:
|
|
created_nodes.append(fo_regular)
|
|
|
|
|
|
y_offset += -22 * len(regular_outputs) - node_margin
|
|
# Create and connect tech outputs with 32-bit depth if split_tech_passes is True
|
|
if tech_outputs:
|
|
out_name = f'OUT_{out_base}_tech'
|
|
|
|
fo_tech = create_and_connect_file_output(
|
|
node, tech_outputs, None, out_name, base_path,
|
|
out_base, scene, remap_names, tech_file_format,
|
|
suffix='tech/', color_depth='32',
|
|
location_offset=(500, y_offset),
|
|
base_path_template=template
|
|
)
|
|
|
|
if fo_tech and fo_tech not in created_nodes:
|
|
created_nodes.append(fo_tech)
|
|
|
|
y_offset -= node_margin
|
|
|
|
y_offset += -22 * len(tech_outputs)
|
|
# Create and connect crypto outputs with 32-bit depth
|
|
if crypto_outputs:
|
|
out_name = f'OUT_{out_base}_cryptos'
|
|
|
|
fo_crypto = create_and_connect_file_output(
|
|
node, crypto_outputs, None, out_name, base_path,
|
|
out_base, scene, remap_names, tech_file_format,
|
|
suffix='cryptos/', color_depth='32',
|
|
location_offset=(500, y_offset),
|
|
base_path_template=template
|
|
)
|
|
|
|
if fo_crypto and fo_crypto not in created_nodes:
|
|
created_nodes.append(fo_crypto)
|
|
|
|
return created_nodes
|
|
|
|
# endregion
|
|
|
|
# endregion
|
|
|
|
|
|
# region Collections
|
|
|
|
def get_collection_children_recursive(col, cols=None) -> list:
|
|
'''return a list of all the child collections
|
|
and their subcollections in the passed collection'''
|
|
|
|
cols = cols if cols is not None else []
|
|
for sub in col.children:
|
|
if sub not in cols:
|
|
cols.append(sub)
|
|
if len(sub.children):
|
|
cols = get_collection_children_recursive(sub, cols)
|
|
return cols
|
|
|
|
def get_view_layer_collection(col, vl_col=None, view_layer=None):
|
|
'''return viewlayer collection from collection
|
|
col: the collection to get viewlayer collection from
|
|
view_layer (viewlayer, optional) : viewlayer to search in, if not passed, use active viewlayer
|
|
'''
|
|
if vl_col is None:
|
|
if view_layer:
|
|
vl_col = view_layer.layer_collection
|
|
else:
|
|
vl_col = bpy.context.view_layer.layer_collection
|
|
for sub in vl_col.children:
|
|
if sub.collection == col:
|
|
return sub
|
|
if len(sub.children):
|
|
c = get_view_layer_collection(col, sub)
|
|
if c is not None:
|
|
return c
|
|
|
|
## Alternative implementation
|
|
# def get_view_layer_collection(collection, vl_col=None):
|
|
# """Find the view layer collection corresponding to a given collection"""
|
|
# if vl_col is None:
|
|
# vl_col = bpy.context.view_layer.layer_collection
|
|
# if vl_col.collection == collection:
|
|
# return vl_col
|
|
# for child in vl_col.children:
|
|
# found = get_view_layer_collection(collection, child)
|
|
# if found:
|
|
# return found
|
|
# return None
|
|
|
|
def get_parents_cols(col, root=None, scene=None, cols=None):
|
|
'''Return a list of parents collections of passed col
|
|
root : Pass a collection to search in (recursive)
|
|
Else search in master collection
|
|
scene: scene to search in (active scene if not passed)
|
|
cols: used internally by the function to collect results
|
|
'''
|
|
if cols is None:
|
|
cols = []
|
|
|
|
if root == None:
|
|
scn = scene or bpy.context.scene
|
|
root=scn.collection
|
|
|
|
for sub in root.children:
|
|
if sub == col:
|
|
cols.append(root)
|
|
|
|
if len(sub.children):
|
|
cols = get_parents_cols(col, root=sub, cols=cols)
|
|
return cols
|
|
|
|
# endregion
|
|
|
|
# region utilities
|
|
|
|
def set_properties_editor_tab(tab, skip_if_exists=True):
|
|
'''Take a tab name and apply it to properties editor
|
|
tab: identifier of the tab, possible name in:
|
|
['TOOL', 'SCENE', 'RENDER', 'OUTPUT', 'VIEW_LAYER',
|
|
'WORLD', 'COLLECTION', 'OBJECT', 'CONSTRAINT', 'MODIFIER',
|
|
'DATA', 'BONE', 'BONE_CONSTRAINT', 'MATERIAL', 'TEXTURE',
|
|
'PARTICLES', 'PHYSICS', 'SHADERFX']
|
|
|
|
skip_if_exists: do nothing if a properties editor is alrteady on this tab
|
|
'''
|
|
if bpy.context.area.type == 'PROPERTIES':
|
|
bpy.context.area.spaces.active.context = tab
|
|
return
|
|
|
|
prop_space = None
|
|
for area in bpy.context.screen.areas:
|
|
if area.type == 'PROPERTIES':
|
|
for space in area.spaces:
|
|
if space.type == 'PROPERTIES':
|
|
if skip_if_exists and space.context == tab:
|
|
return
|
|
if prop_space is None:
|
|
prop_space = space
|
|
|
|
if prop_space is not None:
|
|
prop_space.context = tab
|
|
|
|
return 1
|
|
|
|
|
|
def show_and_active_object(obj, make_active=True, select=True, unhide=True):
|
|
'''
|
|
Show the object
|
|
Disable exclude parent collection collection and select with options
|
|
Activate and show all parents collections
|
|
active : Make the object active
|
|
select : Select the object (independent of active state)
|
|
unhide : show object in viewport (activate both visibility status)
|
|
'''
|
|
|
|
# Activate parents collections
|
|
# activate_parent_collections(obj, ensure_visible=ensure_collection_visible)
|
|
if unhide:
|
|
# Show obj object
|
|
if obj.hide_viewport:
|
|
obj.hide_viewport = False
|
|
if obj.hide_get():
|
|
obj.hide_set(False)
|
|
|
|
# Make object Active
|
|
if make_active:
|
|
bpy.context.view_layer.objects.active = obj
|
|
# Select object
|
|
if select:
|
|
obj.select_set(True)
|
|
|
|
def show_message_box(message = "", title = "Message Box", icon = 'INFO'):
|
|
'''Show message box with element passed as string or list
|
|
if message is a list of lists:
|
|
if sublist have 2 element:
|
|
considered a label [text, icon]
|
|
if sublist have 3 element:
|
|
considered as an operator [ops_id_name, text, icon]
|
|
if sublist have 4 element:
|
|
considered as a property [object, propname, text, icon]
|
|
'''
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
for l in message:
|
|
if isinstance(l, str):
|
|
layout.label(text=l)
|
|
elif len(l) == 2: # label with icon
|
|
layout.label(text=l[0], icon=l[1])
|
|
elif len(l) == 3: # ops
|
|
layout.operator_context = "INVOKE_DEFAULT"
|
|
layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
|
|
elif len(l) == 4: # prop
|
|
row = layout.row(align=True)
|
|
row.label(text=l[2], icon=l[3])
|
|
row.prop(l[0], l[1], text='')
|
|
|
|
if isinstance(message, str):
|
|
message = [message]
|
|
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
|
|
|
|
|
def get_rightmost_number_in_string(string) -> str:
|
|
"""Get the rightmost number passed string
|
|
return the number as string, empty string if no number found
|
|
"""
|
|
res = re.search(r'(\d+)(?!.*\d)', string)
|
|
if not res:
|
|
return ''
|
|
return res.group(1)
|
|
|
|
|
|
def build_path_from_template(template: str,
|
|
node_name: str=None,
|
|
node_label: str=None,
|
|
scene_name: str=None,
|
|
viewlayer_name: str=None,
|
|
socket_name: str=None,
|
|
socket_index: str=None,
|
|
) -> str:
|
|
"""Get a name from a template string
|
|
Possible keyword:
|
|
{node_name} : name of the node
|
|
{node_label} : label of the node (fallback to node_name if not set)
|
|
{scene_name} : name of the source render layer node scene
|
|
{viewlayer_name} : name of the source render layer viewlayer
|
|
{socket_name} : name of source the node socket
|
|
{socket_index} : index of the node in the node tree (010, 020, etc.)
|
|
|
|
## Info from Blend
|
|
{blend_name} : name of the blend file
|
|
{blend_version} : version of the blend file
|
|
{date} : current date (YYYYMMDD format)
|
|
{time} : current time (HHMMSS format)
|
|
"""
|
|
|
|
## note: socket index is a bad idea because the final order is not decided before execute
|
|
if not template:
|
|
return ''
|
|
|
|
# Get blend file info
|
|
blend_path = bpy.data.filepath
|
|
blend_name = ''
|
|
blend_version = ''
|
|
|
|
if blend_path:
|
|
blend_name = Path(blend_path).stem
|
|
blend_version = get_rightmost_number_in_string(blend_name)
|
|
|
|
# Get current date and time
|
|
import datetime
|
|
now = datetime.datetime.now()
|
|
current_date = now.strftime("%Y%m%d")
|
|
current_time = now.strftime("%H%M%S")
|
|
|
|
# Handle node_label to name fallback
|
|
if "{node_label}" in template:
|
|
if node_label is None or not node_label:
|
|
node_label = node_name
|
|
|
|
if socket_index:
|
|
# Ensure socket_index is formatted as a zero-padded string and increment on base 10 (allow to insert inbetween renders)
|
|
socket_index = f"{int(socket_index)*10:03d}"
|
|
|
|
def clean_name(name):
|
|
# Clean names to be file-system safe
|
|
if name is None:
|
|
return ''
|
|
return bpy.path.clean_name(name)
|
|
|
|
# Create a dictionary of all available variables
|
|
template_vars = {
|
|
'node_name': clean_name(node_name),
|
|
'node_label': clean_name(node_label),
|
|
'socket_name': clean_name(socket_name),
|
|
'scene_name': clean_name(scene_name),
|
|
'viewlayer_name': clean_name(viewlayer_name),
|
|
'socket_index': socket_index or '',
|
|
'blend_name': clean_name(blend_name),
|
|
'blend_version': blend_version,
|
|
'date': current_date,
|
|
'time': current_time,
|
|
}
|
|
|
|
# Apply template using format_map to handle missing keys gracefully
|
|
try:
|
|
applied_template = template.format_map(template_vars)
|
|
except KeyError as e:
|
|
# If a key is missing, return template with error message
|
|
# print(f"Template error: Unknown variable {e}")
|
|
return f"Template error: Unknown {e}"
|
|
|
|
return applied_template
|
|
|
|
# endregion
|
|
|
|
# region visibility states
|
|
|
|
## targets for conformation
|
|
def get_target_collection(target_name, context):
|
|
"""Get the target collection based on active or the target name, None if nothing"""
|
|
if target_name:
|
|
return next((c for c in context.scene.collection.children_recursive if c.name == target_name), None)
|
|
else:
|
|
return context.collection
|
|
|
|
def get_target_object(target_name, context):
|
|
"""Get the target object based on active or the target name, None if nothing"""
|
|
if target_name:
|
|
return bpy.context.scene.objects.get(target_name)
|
|
else:
|
|
return context.object
|
|
|
|
|
|
|
|
def store_visibility_states(collection=None):
|
|
"""Store visibility states of objects and collections
|
|
|
|
Args:
|
|
collection: Target collection to scan. If None, scans entire scene starting from root.
|
|
|
|
Returns:
|
|
dict: Visibility states for objects, collections, and viewlayer collections
|
|
"""
|
|
visibility_states = {
|
|
'objects': {},
|
|
'collections': {},
|
|
'view_layer_collections': {}
|
|
}
|
|
|
|
# Determine target viewlayer collection
|
|
if collection is None:
|
|
target_vl_collection = bpy.context.view_layer.layer_collection
|
|
else:
|
|
target_vl_collection = get_view_layer_collection(collection)
|
|
if target_vl_collection is None:
|
|
# Collection not found in current viewlayer
|
|
return visibility_states
|
|
|
|
# Store visibility states by iterating through viewlayer collections
|
|
def store_visibility_recursive(vl_col):
|
|
# Get the actual collection from viewlayer collection
|
|
col = vl_col.collection
|
|
|
|
# Store collection visibility states
|
|
visibility_states['collections'][col.name] = {
|
|
'hide_select': getattr(col, 'hide_select', False),
|
|
'hide_viewport': getattr(col, 'hide_viewport', False),
|
|
'hide_render': getattr(col, 'hide_render', False),
|
|
}
|
|
|
|
# Store viewlayer collection visibility states
|
|
visibility_states['view_layer_collections'][vl_col.name] = {
|
|
'exclude': getattr(vl_col, 'exclude', False),
|
|
'hide_viewport': getattr(vl_col, 'hide_viewport', False),
|
|
'indirect_only': getattr(vl_col, 'indirect_only', False),
|
|
'holdout': getattr(vl_col, 'indirect_only', False),
|
|
}
|
|
|
|
# Store objects in this collection
|
|
for obj in col.objects:
|
|
if obj.name not in visibility_states['objects']: # Avoid duplicates
|
|
visibility_states['objects'][obj.name] = {
|
|
'hide_viewport': obj.hide_viewport,
|
|
'hide_render': obj.hide_render,
|
|
'hide_select': obj.hide_select,
|
|
'hide_viewlayer': obj.hide_get(),
|
|
}
|
|
|
|
# Recursively process children
|
|
for child_vl_col in vl_col.children:
|
|
store_visibility_recursive(child_vl_col)
|
|
|
|
store_visibility_recursive(target_vl_collection)
|
|
|
|
return visibility_states
|
|
|
|
|
|
def store_collection_states(collection, context=None):
|
|
'''Pass a colelction and store visibility state'''
|
|
context = context or bpy.context
|
|
|
|
scene = context.scene
|
|
outliner_state = scene.get('outliner_state', {})
|
|
## By name
|
|
# if collection is None:
|
|
# collection = next((c for c in context.scene.collection.children_recursive if c.name == target_name), None)
|
|
state = store_visibility_states(collection)
|
|
key = collection.name if collection else "ALL"
|
|
outliner_state[key] = state
|
|
scene['outliner_state'] = outliner_state
|
|
# return ({'INFO'}, f"Stored visibility states for '{key}'")
|
|
|
|
|
|
def apply_collection_states(collection, context=None):
|
|
context = context or bpy.context
|
|
|
|
scene = context.scene
|
|
outliner_state = scene.get('outliner_state', {})
|
|
if collection is None:
|
|
# If no collection is passed, apply to all collections
|
|
collection = context.scene.collection
|
|
key = "ALL"
|
|
else:
|
|
key = collection.name
|
|
|
|
state = outliner_state.get(key)
|
|
if not state:
|
|
return ({'WARNING'}, f"No stored state for '{key}'")
|
|
# return {'CANCELLED'}
|
|
|
|
for obj_name, obj_state in state['objects'].items():
|
|
# obj = context.scene.objects.get(obj_name)
|
|
obj = collection.all_objects.get(obj_name)
|
|
if obj:
|
|
obj.hide_viewport = obj_state.get('hide_viewport', False)
|
|
obj.hide_render = obj_state.get('hide_render', False)
|
|
obj.hide_select = obj_state.get('hide_select', False)
|
|
try:
|
|
obj.hide_set(obj_state.get('hide_viewlayer', False))
|
|
except Exception:
|
|
pass
|
|
|
|
for col_name, col_state in state['collections'].items():
|
|
col = next((c for c in context.scene.collection.children_recursive if c.name == col_name), None)
|
|
if col:
|
|
col.hide_select = col_state.get('hide_select', False)
|
|
col.hide_viewport = col_state.get('hide_viewport', False)
|
|
col.hide_render = col_state.get('hide_render', False)
|
|
|
|
for vlcol_name, vlcol_state in state['view_layer_collections'].items():
|
|
col = next((c for c in context.scene.collection.children_recursive if c.name == vlcol_name), None)
|
|
vlcol = get_view_layer_collection(col, context.view_layer.layer_collection)
|
|
if vlcol:
|
|
vlcol.exclude = vlcol_state.get('exclude', False)
|
|
vlcol.hide_viewport = vlcol_state.get('hide_viewport', False)
|
|
vlcol.indirect_only = vlcol_state.get('indirect_only', False)
|
|
vlcol.holdout = vlcol_state.get('holdout', False)
|
|
|
|
## ---
|
|
|
|
# # Restore object states
|
|
# for obj, obj_state in state.get('objects', {}).items():
|
|
# # obj = bpy.data.objects.get(obj_name)
|
|
# obj.hide_viewport = obj_state.get('hide_viewport', False)
|
|
# obj.hide_render = obj_state.get('hide_render', False)
|
|
# obj.hide_select = obj_state.get('hide_select', False)
|
|
# try:
|
|
# obj.hide_set(obj_state.get('hide_viewlayer', False))
|
|
# except Exception:
|
|
# pass
|
|
|
|
# # Restore collection states
|
|
# for col, col_state in state.get('collections', {}).items():
|
|
# # col = bpy.data.collections.get(col_name)
|
|
# col.hide_select = col_state.get('hide_select', False)
|
|
# col.hide_viewport = col_state.get('hide_viewport', False)
|
|
# col.hide_render = col_state.get('hide_render', False)
|
|
|
|
# for vlcol, vlcol_state in state.get('view_layer_collections', {}).items():
|
|
# vlcol.hide_select = vlcol_state.get('hide_select', False)
|
|
# vlcol.hide_viewport = vlcol_state.get('hide_viewport', False)
|
|
# vlcol.hide_render = vlcol_state.get('hide_render', False)
|
|
|
|
|
|
### ----
|
|
|
|
# Restore view layer collection states
|
|
# def apply_vl_states(vl_col):
|
|
# vl_state = state.get('view_layer_collections', {}).get(vl_col.name)
|
|
# if vl_state:
|
|
# vl_col.exclude = vl_state.get('exclude', False)
|
|
# vl_col.hide_viewport = vl_state.get('hide_viewport', False)
|
|
# vl_col.indirect_only = vl_state.get('indirect_only', False)
|
|
# vl_col.holdout = vl_state.get('holdout', False)
|
|
# for child in vl_col.children:
|
|
# apply_vl_states(child)
|
|
|
|
# if collection:
|
|
# vl_col = get_view_layer_collection(collection)
|
|
# if vl_col:
|
|
# apply_vl_states(vl_col)
|
|
# else:
|
|
# apply_vl_states(context.view_layer.layer_collection)
|
|
|
|
|
|
def delete_collection_states(collection_name, context=None):
|
|
"""Delete stored visibility states for a collection or all if None"""
|
|
context = context or bpy.context
|
|
|
|
scene = context.scene
|
|
outliner_state = scene.get('outliner_state', {})
|
|
key = collection_name if collection_name else "ALL"
|
|
if key in outliner_state:
|
|
del outliner_state[key]
|
|
scene['outliner_state'] = outliner_state
|
|
return ({'INFO'}, f"Deleted visibility states for '{key}'")
|
|
else:
|
|
return ({'WARNING'}, f"No stored state for '{key}'") |