first commit, transfer connect selected to file out from gp_render
parent
016d3f827c
commit
d56d32ad57
|
@ -1,5 +1,6 @@
|
|||
# git_template
|
||||
A Template from which to create new repository from.
|
||||
# Render Toolbox
|
||||
|
||||
Blender addon for rendering setup and checks
|
||||
|
||||
## Development
|
||||
### Cloning
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
bl_info = {
|
||||
"name": "Render Toolbox",
|
||||
"description": "Perform checks and setup outputs",
|
||||
"author": "Samuel Bernou",
|
||||
"version": (0, 1, 0),
|
||||
"blender": (3, 0, 0),
|
||||
"location": "View3D",
|
||||
"warning": "",
|
||||
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox",
|
||||
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox/issues",
|
||||
"category": "Object"
|
||||
}
|
||||
|
||||
## TODO:
|
||||
## Transfer useful and generic render function from gp_render:
|
||||
# - mute/unmute all file output nodes
|
||||
# - export crop infos to json (hardcoded path in gp_render. make an export dialog instead, with eventually an environnement variable for the path)
|
||||
# - Viewlayer list view
|
||||
|
||||
## Transfer useful and genereric render stuff from gp_toolbox:
|
||||
# - ? Conflict visibility checker ? This also fit other tasks, but more often use to debug at precomp/rendering step.
|
||||
|
||||
## Improve existing:
|
||||
# - Connect to file output node should have the option to add scene name as output prefix
|
||||
# - Connect to file output node should have search and replace options for output names in window
|
||||
# - Connect to file output node should not remove unlinked output by defaut (should be an option in window)
|
||||
|
||||
|
||||
from . import setup_outputs
|
||||
from . import ui
|
||||
|
||||
bl_modules = (
|
||||
setup_outputs,
|
||||
ui,
|
||||
# prefs,
|
||||
)
|
||||
|
||||
import bpy
|
||||
|
||||
def register():
|
||||
for mod in bl_modules:
|
||||
mod.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
for mod in reversed(bl_modules):
|
||||
mod.unregister()
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
|
@ -0,0 +1,237 @@
|
|||
import bpy
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
### --- Manage nodes --- ###
|
||||
|
||||
def real_loc(n):
|
||||
if not n.parent:
|
||||
return n.location
|
||||
return n.location + real_loc(n.parent)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None, file_format=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}}.
|
||||
|
||||
frame (bpy.types.CompositorNode, optional): If given, create nodes into a frame.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
list[bpy.types.CompositorNode]: All nodes created.
|
||||
"""
|
||||
|
||||
scene = bpy.context.scene
|
||||
nodes = scene.node_tree.nodes
|
||||
links = scene.node_tree.links
|
||||
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
|
||||
|
||||
excludes = excludes or {}
|
||||
|
||||
for node in node_list:
|
||||
exclusions = excludes.get(node.name) or []
|
||||
## create one output facing node and connect all
|
||||
outs = [o for o in node.outputs if not o.is_unavailable and not 'crypto' in o.name.lower() and o.name not in exclusions]
|
||||
cryptout = [o for o in node.outputs if not o.is_unavailable and 'crypto' in o.name.lower() and o.name not in exclusions]
|
||||
|
||||
if node.type == 'R_LAYERS':
|
||||
out_base = node.layer
|
||||
elif node.label:
|
||||
out_base = node.label
|
||||
else:
|
||||
out_base = node.name
|
||||
out_base = bpy.path.clean_name(out_base)
|
||||
out_name = f'OUT_{out_base}'
|
||||
|
||||
if outs:
|
||||
fo = file_out
|
||||
if not fo:
|
||||
fo = nodes.get(out_name)
|
||||
if not fo:
|
||||
# color = (0.2,0.3,0.5)
|
||||
fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+500, real_loc(node)[1]+50), 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)
|
||||
|
||||
fo.name = out_name
|
||||
if node.parent:
|
||||
fo.parent = node.parent
|
||||
|
||||
if base_path:
|
||||
fo.base_path = base_path
|
||||
else:
|
||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
fo.base_path = f'//render/{out_base}/{out_base}_'
|
||||
else:
|
||||
fo.base_path = f'//render/{out_base}'
|
||||
|
||||
for o in outs:
|
||||
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) # clean name ?
|
||||
else:
|
||||
slot_name = bpy.path.clean_name(o.name)
|
||||
|
||||
# if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
# slot_name = slot_name
|
||||
# else:
|
||||
# slot_name = f'{slot_name}/{slot_name}_'
|
||||
# fo.file_slots.new(slot_name)
|
||||
fs = fo.file_slots.new('tmp') # slot_name)
|
||||
ls = fo.layer_slots.new('tmp') # slot_name + 'layer')
|
||||
|
||||
ls = fo.layer_slots[-1]
|
||||
ls.name = slot_name
|
||||
|
||||
fs = fo.file_slots[-1]
|
||||
fs.path = f'{slot_name}/{slot_name}_' # Error 'NodeSocketColor' object has no attribute 'path'
|
||||
|
||||
|
||||
out_input = fo.inputs[-1]
|
||||
links.new(o, out_input)
|
||||
|
||||
clear_disconnected(fo)
|
||||
fo.update()
|
||||
|
||||
## Create separate file out for cryptos
|
||||
if cryptout:
|
||||
out_name += '_cryptos'
|
||||
fo = file_out
|
||||
if not fo:
|
||||
fo = nodes.get(out_name)
|
||||
if not fo:
|
||||
# color = (0.2,0.3,0.5)
|
||||
fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+400, real_loc(node)[1]-200), width=220)
|
||||
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) # OPEN_EXR_MULTILAYER, RGBA, ZIP
|
||||
fo.format.color_depth = '32' # For crypto force 32bit
|
||||
|
||||
fo.name = out_name
|
||||
if node.parent:
|
||||
fo.parent = node.parent
|
||||
|
||||
if base_path:
|
||||
fo.base_path = base_path
|
||||
else:
|
||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
## FIXME: find a better organization for separated crypto pass
|
||||
fo.base_path = f'//render/{out_base}/cryptos/cryptos_'
|
||||
else:
|
||||
fo.base_path = f'//render/{out_base}'
|
||||
|
||||
for o in cryptout:
|
||||
## Skip already connected
|
||||
## TODO Test recusively to find fo (some have interconnected sockets)
|
||||
# if next((l for l in o.links if l.to_node == fo), None):
|
||||
if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
|
||||
continue
|
||||
|
||||
# if remap_names and (custom_name := remap_names.get(o.name)):
|
||||
if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)):
|
||||
slot_name = bpy.path.clean_name(custom_name) # clean name ?
|
||||
else:
|
||||
slot_name = bpy.path.clean_name(o.name) # directly use name in multi layer exr
|
||||
|
||||
# if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
# slot_name = slot_name
|
||||
# else:
|
||||
# slot_name = f'{slot_name}/{slot_name}_'
|
||||
# fo.file_slots.new(slot_name)
|
||||
|
||||
# Setting both file_slots and layer_slots...
|
||||
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}_' # Error 'NodeSocketColor' object has no attribute 'path'
|
||||
|
||||
|
||||
out_input = fo.inputs[-1]
|
||||
links.new(o, out_input)
|
||||
clear_disconnected(fo)
|
||||
fo.update()
|
|
@ -0,0 +1,259 @@
|
|||
import bpy
|
||||
import os
|
||||
from . import fn
|
||||
|
||||
class RT_PG_selectable_prop(bpy.types.PropertyGroup):
|
||||
node_name: bpy.props.StringProperty(name="Node Name")
|
||||
node_label: bpy.props.StringProperty(name="Node Label")
|
||||
name: bpy.props.StringProperty(name="Name or Path")
|
||||
socket_name: bpy.props.StringProperty(name="Source socket Name") # Source socket name as reference
|
||||
select: bpy.props.BoolProperty(name="Selected", default=True)
|
||||
is_linked: bpy.props.BoolProperty(name="Linked", default=False)
|
||||
# is_valid: bpy.props.BoolProperty(name="Valid", default=True)
|
||||
|
||||
## extra output naming options
|
||||
name_from_node: bpy.props.StringProperty(name="Name From Node")
|
||||
name_from_node_and_socket: bpy.props.StringProperty(name="Name From Node And Socket")
|
||||
|
||||
class RT_OT_connect_selected_to_file_out(bpy.types.Operator):
|
||||
bl_idname = "rt.connect_selected_to_file_out"
|
||||
bl_label = "Connect Selected To File Output"
|
||||
bl_description = "Connect Selected Nodes to a new fileoutput node\
|
||||
\nIf a fileoutput node is selected, socket are added to it"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
socket_collection : bpy.props.CollectionProperty(type=RT_PG_selectable_prop)
|
||||
|
||||
show_custom_settings : bpy.props.BoolProperty(
|
||||
name='Settings',
|
||||
default=False)
|
||||
|
||||
## enum choice for naming: socket_name, node_name, node_and_socket_name,
|
||||
name_type : bpy.props.EnumProperty(
|
||||
name='Output Name From',
|
||||
description='Choose the output name\
|
||||
\nNode name use Label (Use node name when there is no Label)',
|
||||
default='node_and_socket_name',
|
||||
items=(
|
||||
('node_and_socket_name', 'Node_Socket Name', 'Use the node name prefix and socket name', 0),
|
||||
('socket_name', 'Socket Name', 'Use the socket name as output name', 1),
|
||||
('node_name', 'Node Name', 'Use the node name as output name', 2),
|
||||
)
|
||||
)
|
||||
|
||||
# prefix_with_node_name : bpy.props.BoolProperty(
|
||||
# name='Prefix With Node Name',
|
||||
# description='Add the node name as prefix to the output name',
|
||||
# default=False)
|
||||
|
||||
base_path : bpy.props.StringProperty(
|
||||
name='Custom base path',
|
||||
default='',
|
||||
description='Set the base path of created file_output (not if already exists)')
|
||||
|
||||
file_format : bpy.props.EnumProperty(
|
||||
name='Output Format',
|
||||
default='NONE',
|
||||
items=(
|
||||
('NONE', 'Default', 'Use default settings'),
|
||||
('OPEN_EXR_MULTILAYER', 'OpenEXR MultiLayer', 'Output image in multilayer OpenEXR format'),
|
||||
('OPEN_EXR', 'OpenEXR', 'Output image in OpenEXR format'),
|
||||
)
|
||||
)
|
||||
|
||||
exr_codec : bpy.props.EnumProperty(
|
||||
name='Codec',
|
||||
default='PIZ',
|
||||
description='Codec settings for OpenEXR',
|
||||
items=(
|
||||
('PIZ', 'PIZ (lossless)', ''),
|
||||
('ZIP', 'ZIP (lossless)', ''),
|
||||
('RLE', 'RLE (lossless)', ''),
|
||||
('ZIPS', 'ZIPS (lossless)', ''),
|
||||
('PXR24', 'Pxr24 (lossy)', ''),
|
||||
('B44', 'B44 (lossy)', ''),
|
||||
('B44A', 'B44A (lossy)', ''),
|
||||
('DWAA', 'DWAA (lossy)', ''),
|
||||
('DWAB', 'DWAB (lossy)', ''),
|
||||
),
|
||||
)
|
||||
|
||||
color_depth : bpy.props.EnumProperty(
|
||||
name='Color Depth',
|
||||
default='16',
|
||||
description='Bit depth per channel',
|
||||
items=(
|
||||
# ('8', '8', '8-bit color channels'),
|
||||
# ('10', '10', '10-bit color channels'),
|
||||
# ('12', '12', '12-bit color channels'),
|
||||
('16', '16 (Half)', '16-bit color channels'),
|
||||
('32', '32 (Full)', '32-bit color channels'),
|
||||
),
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.socket_collection.clear()
|
||||
if event.ctrl:
|
||||
# Direct connect, do not use any options
|
||||
self.base_path = ''
|
||||
return self.execute(context)
|
||||
|
||||
selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE']
|
||||
if not selected:
|
||||
self.report({'ERROR'}, 'No render layer nodes selected')
|
||||
return {'CANCELLED'}
|
||||
for n in selected:
|
||||
for o in n.outputs:
|
||||
if o.is_unavailable:
|
||||
continue
|
||||
item = self.socket_collection.add()
|
||||
item.node_name = n.name
|
||||
item.node_label = n.label.strip()
|
||||
item.socket_name = o.name
|
||||
|
||||
## Set editable names
|
||||
item.name = o.name
|
||||
|
||||
## Store other naming options (cleaned at exec with bpy.path.clean_name)
|
||||
node_name = n.label.strip() if n.label.strip() else n.name
|
||||
|
||||
## Change node_name for render layers: scene_viewlayer_name
|
||||
if n.type == 'R_LAYERS' and node_name != n.label: # skip if a label is set
|
||||
node_name = f'{n.layer}'
|
||||
# node_name = f'{n.scene.name}_{n.layer}'
|
||||
|
||||
item.name_from_node = node_name
|
||||
|
||||
if len(n.outputs) == 1:
|
||||
## Only one output, just pick node name, no need to add socket name
|
||||
item.name_from_node_and_socket = node_name
|
||||
else:
|
||||
item.name_from_node_and_socket = f'{node_name}_{o.name}'
|
||||
|
||||
## TODO: rename item.name according to template pairs in preferences (to add later)
|
||||
if o.is_linked:
|
||||
item.is_linked = True
|
||||
item.select = False
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT'
|
||||
box.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon)
|
||||
## Settings
|
||||
if self.show_custom_settings:
|
||||
box.use_property_split = True
|
||||
box.row().prop(self, 'name_type', expand=False)
|
||||
box.prop(self, 'base_path')
|
||||
col = box.column()
|
||||
col.prop(self, 'file_format')
|
||||
col.prop(self, 'exr_codec')
|
||||
col.row().prop(self, 'color_depth', expand=True)
|
||||
|
||||
## Node Sockets
|
||||
layout.use_property_split = False
|
||||
col = layout.column()
|
||||
current_node_name = ''
|
||||
for item in self.socket_collection:
|
||||
if item.node_name != current_node_name:
|
||||
current_node_name = item.node_name
|
||||
col.separator()
|
||||
## Display node label + name or name only
|
||||
if item.node_label:
|
||||
display_node_name = f'{item.node_label} ({item.node_name})'
|
||||
else:
|
||||
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)
|
||||
display_node_name = f'{rln.scene.name}/{rln.layer} ({display_node_name})'
|
||||
col.label(text=display_node_name, icon='NODE_SEL')
|
||||
|
||||
row = col.row()
|
||||
if item.is_linked:
|
||||
row.label(text='', icon='LINKED') # NODETREE
|
||||
else:
|
||||
row.label(text='', icon='BLANK1')
|
||||
row.prop(item, 'select', text='')
|
||||
|
||||
display_name = item.socket_name
|
||||
if 'crypto' in display_name.lower():
|
||||
display_name = f'{display_name} -> 32bit output node'
|
||||
row.label(text=display_name)
|
||||
row.label(text='', icon='RIGHTARROW')
|
||||
|
||||
if self.name_type == 'socket_name':
|
||||
row.prop(item, 'name', text='')
|
||||
elif self.name_type == 'node_name':
|
||||
row.prop(item, 'name_from_node', text='')
|
||||
elif self.name_type == 'node_and_socket_name':
|
||||
row.prop(item, 'name_from_node_and_socket', text='')
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
# Build exclude dict from selection
|
||||
excludes = {}
|
||||
remap_names = {}
|
||||
## Old system
|
||||
# if len(self.socket_collection):
|
||||
# for item in self.socket_collection:
|
||||
# if not item.select:
|
||||
# # All deselected goes to exclude with {node_name: [socket1, ...]}
|
||||
# excludes.setdefault(item.node_name, []).append(item.name)
|
||||
# elif item.socket_name != item.name:
|
||||
# remap_names[item.socket_name] = item.name
|
||||
|
||||
if len(self.socket_collection):
|
||||
for item in self.socket_collection:
|
||||
final_name = item.name
|
||||
## change name if other options were used
|
||||
if self.name_type == 'node_name':
|
||||
final_name = item.name_from_node
|
||||
elif self.name_type == 'node_and_socket_name':
|
||||
final_name = item.name_from_node_and_socket
|
||||
|
||||
if not item.select:
|
||||
# All deselected goes to exclude with {node_name: [socket1, ...]}
|
||||
excludes.setdefault(item.node_name, []).append(item.socket_name)
|
||||
elif item.socket_name != final_name:
|
||||
remap_names.setdefault(item.node_name, {})[item.socket_name] = final_name
|
||||
# remap_names[item.socket_name] = final_name
|
||||
|
||||
## Handle default file format
|
||||
file_ext = self.file_format
|
||||
if self.file_format == 'NONE':
|
||||
env_file_format = os.environ.get('FILE_FORMAT')
|
||||
file_ext = env_file_format if env_file_format else 'OPEN_EXR_MULTILAYER'
|
||||
|
||||
file_format = {
|
||||
'file_format' : file_ext,
|
||||
'exr_codec' : self.exr_codec,
|
||||
'color_depth' : self.color_depth,
|
||||
}
|
||||
|
||||
scn = context.scene
|
||||
nodes = scn.node_tree.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
|
||||
selected = [n for n in selected if n.type != 'OUTPUT_FILE']
|
||||
|
||||
# fn.connect_to_file_output(selected, outfile)
|
||||
for n in selected:
|
||||
fn.connect_to_file_output(n, outfile, base_path=self.base_path, excludes=excludes, remap_names=remap_names, file_format=file_format)
|
||||
return {"FINISHED"}
|
||||
|
||||
classes=(
|
||||
RT_PG_selectable_prop,
|
||||
RT_OT_connect_selected_to_file_out,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,27 @@
|
|||
import bpy
|
||||
|
||||
from bpy.types import Panel
|
||||
|
||||
|
||||
class RT_PT_gp_node_ui(Panel):
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Render"
|
||||
bl_label = "Render Toolbox"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("rt.connect_selected_to_file_out", icon="NODE")
|
||||
|
||||
|
||||
classes = (
|
||||
RT_PT_gp_node_ui,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
Loading…
Reference in New Issue