2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -0,0 +1,187 @@
import bpy
from ..Program.Run_Operator import on_operator_ref_update
from ..base_node import SN_ScriptingBaseNode
class SN_ButtonNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ButtonNodeNew"
bl_label = "Button"
node_color = "INTERFACE"
bl_width_default = 240
def on_create(self, context):
self.version = 1
self.add_interface_input()
self.add_string_input("Label").default_value = "My Button"
self.add_boolean_input("Emboss").default_value = True
self.add_boolean_input("Depress")
self.add_icon_input("Icon")
self.add_interface_output().passthrough_layout_type = True
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
on_operator_ref_update(self, node, data, self.ref_ntree, self.ref_SN_OperatorNode, 5)
def reset_inputs(self):
""" Remove all operator inputs """
for i in range(len(self.inputs)-1, -1, -1):
inp = self.inputs[i]
if inp.can_be_disabled:
self.inputs.remove(inp)
def create_inputs(self, op_rna):
""" Create inputs for operator """
for prop in op_rna.properties:
if not prop.identifier in ["rna_type", "settings"]:
inp = self.add_input_from_property(prop)
if inp:
inp.can_be_disabled = True
inp.disabled = not prop.is_required
def update_custom_operator(self, context):
""" Updates the nodes settings when a new parent panel is selected """
self.reset_inputs()
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
parent = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
for prop in parent.properties:
if prop.property_type in ["Integer", "Float", "Boolean"] and prop.settings.is_vector:
socket = self._add_input(self.socket_names[prop.property_type + " Vector"], prop.name)
socket.size = prop.settings.size
socket.can_be_disabled = True
elif prop.property_type == "Enum":
if prop.stngs_enum.enum_flag:
socket = self._add_input(self.socket_names["Enum Set"], prop.name)
else:
socket = self._add_input(self.socket_names[prop.property_type], prop.name)
socket.items = str(list(map(lambda item: item.name, prop.stngs_enum.items)))
else:
self._add_input(self.socket_names[prop.property_type], prop.name).can_be_disabled = True
self._evaluate(context)
def update_source_type(self, context):
self.hide_disabled_inputs = False
if self.source_type == "BLENDER":
self.pasted_operator = self.pasted_operator
elif self.source_type == "CUSTOM":
self.ref_SN_OperatorNode = self.ref_SN_OperatorNode
self._evaluate(context)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the operator from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
source_type: bpy.props.EnumProperty(name="Source Type",
description="Use a custom operator or a blender internal",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=update_source_type)
ref_SN_OperatorNode: bpy.props.StringProperty(name="Custom Operator",
description="The operator ran by this button",
update=update_custom_operator)
def update_pasted_operator(self, context):
self.reset_inputs()
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
self.pasted_name = op_rna.name
self.create_inputs(op_rna)
self._evaluate(context)
pasted_operator: bpy.props.StringProperty(default="bpy.ops.sn.dummy_button_operator()",
update=update_pasted_operator)
pasted_name: bpy.props.StringProperty(default="Paste Operator")
def update_hide_disabled_inputs(self, context):
for inp in self.inputs:
if inp.can_be_disabled and inp.disabled:
inp.set_hide(self.hide_disabled_inputs)
hide_disabled_inputs: bpy.props.BoolProperty(default=False,
name="Hide Disabled Inputs",
description="Hides the disabled inputs of this node",
update=update_hide_disabled_inputs)
def evaluate(self, context):
if self.source_type == "BLENDER":
op_name = self.pasted_operator[8:].split("(")[0]
code = f"op = {self.active_layout}.operator('{op_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in op_rna.properties:
if (self.version == 0 and (prop.name and prop.name == inp.name) or (not prop.name and prop.identifier.replace("_", " ").title() == inp.name)) \
or (self.version == 1 and inp.name.replace(" ", "_").lower() == prop.identifier):
code += "\n" + f"op.{prop.identifier} = {inp.python_value}"
self.code = f"""
{self.indent(code, 6)}
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
code = f"op = {self.active_layout}.operator('sna.{node.operator_python_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in node.properties:
if prop.name == inp.name:
code += "\n" + f"op.{prop.python_name} = {inp.python_value}"
else:
code = f"op = {self.active_layout}.operator('sn.dummy_button_operator', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
self.code = f"""
{self.indent(code, 6)}
{self.indent(self.outputs[0].python_value, 6)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "source_type", text="", icon_only=True)
if self.source_type == "BLENDER":
name = "Paste Operator"
if self.pasted_operator:
if self.pasted_name:
name = self.pasted_name
elif len(self.pasted_operator.split(".")) > 2:
name = self.pasted_operator.split(".")[3].split("(")[0].replace("_", " ").title()
else:
name = self.pasted_operator
op = row.operator("sn.paste_operator", text=name, icon="PASTEDOWN")
op.node_tree = self.node_tree.name
op.node = self.name
elif self.source_type == "CUSTOM":
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OperatorNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_OperatorNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_OperatorNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_OperatorNode
row.prop(self, "hide_disabled_inputs", text="", icon="HIDE_ON" if self.hide_disabled_inputs else "HIDE_OFF", emboss=False)
@@ -0,0 +1,100 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...utils import get_python_name
class SN_DisplayCollectionListNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayCollectionListNodeNew"
bl_label = "Display Collection List"
node_color = "INTERFACE"
bl_width_default = 200
def layout_type(self, _): return "layout"
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input()
self.add_property_input("Index Property")
self.add_integer_input("Rows")
self.add_dynamic_interface_output("Item Row")
self.add_dynamic_interface_output(
"Interface").passthrough_layout_type = True
self.add_property_output("Item")
self.add_integer_output("Item Index")
def update_function_node(self, context):
self._evaluate(context)
ref_SN_FunctionNode: bpy.props.StringProperty(name="Function",
description="Filter function which should have a single property input and return a boolean (True keeps the item, False removes it)",
update=update_function_node)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Function Node Tree",
description="The node tree to select the function from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
def draw_node(self, context, layout):
row = layout.row(align=True)
row.label(text="Filter:")
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_FunctionNode", bpy.data.node_groups[parent_tree.name].node_collection(
"SN_FunctionNode"), "refs", text="")
def evaluate(self, context):
if self.inputs["Index Property"].is_linked and self.inputs["Collection Property"].is_linked:
ui_list_idname = f"SNA_UL_{get_python_name(self.name, 'List')}_{self.static_uid}"
func = None
if self.ref_ntree and self.ref_SN_FunctionNode in self.ref_ntree.nodes:
func = self.ref_ntree.nodes[self.ref_SN_FunctionNode]
self.code_imperative = f"""
def display_collection_id(uid, vars):
id = f"coll_{{uid}}"
for var in vars.keys():
if var.startswith("i_"):
id += f"_{{var}}_{{vars[var]}}"
return id
class {ui_list_idname}(bpy.types.UIList):
def draw_item(self, context, layout, data, item_{self.static_uid}, icon, active_data, active_propname, index_{self.static_uid}):
row = layout
{self.indent([out.python_value if out.name == 'Item Row' else '' for out in self.outputs], 11)}
def filter_items(self, context, data, propname):
flt_flags = []
for item in getattr(data, propname):
if not self.filter_name or self.filter_name.lower() in item.name.lower():
if {f'{func.func_name}(item)' if func else 'True'}:
flt_flags.append(self.bitflag_filter_item)
else:
flt_flags.append(0)
else:
flt_flags.append(0)
return flt_flags, []
"""
self.code_register = f"""
bpy.utils.register_class({ui_list_idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({ui_list_idname})
"""
self.code = f"""
coll_id = display_collection_id('{self.static_uid}', locals())
{self.active_layout}.template_list('{ui_list_idname}', coll_id, {self.inputs['Collection Property'].python_source}, '{self.inputs['Collection Property'].python_attr}', {self.inputs['Index Property'].python_source}, '{self.inputs['Index Property'].python_attr}', rows={self.inputs['Rows'].python_value})
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 4)}
"""
self.outputs["Item"].python_value = f"item_{self.static_uid}"
if "Item Index" in self.outputs:
self.outputs["Item Index"].python_value = f"index_{self.static_uid}"
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
self.outputs["Item"].reset_value()
if "Item Index" in self.outputs:
self.outputs["Item Index"].reset_value()
@@ -0,0 +1,35 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayEnumItemNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayEnumItemNode"
bl_label = "Display Enum Item"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_string_input("Item")
self.add_string_input("Label")
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Property"].is_linked and (self.inputs["Item"].is_linked or self.inputs["Item"].default_value):
self.code = f"""
{self.active_layout}.prop_enum({self.inputs['Property'].python_source}, '{self.inputs['Property'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, value={self.inputs['Item'].python_value})
{self.indent(self.outputs[0].python_value, 6)}
"""
elif not self.inputs["Property"].is_linked:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Enum Item specified!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 6)}
"""
@@ -0,0 +1,25 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayIconNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayIconNodeNew"
bl_label = "Display Icon"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_icon_input("Icon")
self.add_float_input("Scale").default_value = 1
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
self.code = f"""
{self.active_layout}.template_icon(icon_value={self.inputs['Icon'].python_value}, scale={self.inputs['Scale'].python_value})
{self.indent(self.outputs[0].python_value, 3)}
"""
def draw_node(self, context, layout):
layout.label(text="Use this node with care! This might slow down your UI", icon="INFO")
@@ -0,0 +1,34 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayPreviewNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPreviewNodeNew"
bl_label = "Display Preview"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_boolean_input("Show Buttons")
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Property"].is_linked:
self.code = f"""
{self.active_layout}.template_preview({self.inputs['Property'].python_value}, show_buttons={self.inputs['Show Buttons'].python_value})
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
box = layout.box()
box.label(text="Careful! This element is prone to crashes!", icon="INFO")
box.label(text="Only materials, textures, lights, worlds and line styles can be displayed")
@@ -0,0 +1,60 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayPropertyNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPropertyNodeNew"
bl_label = "Display Property"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_string_input("Label")
self.add_icon_input("Icon")
self.add_boolean_input("Emboss").default_value = True
inp = self.add_boolean_input("Expand")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Slider")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Toggle")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Invert Checkbox")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Full Shortcut")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_integer_input("Index")
inp.can_be_disabled = True
inp.disabled = True
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Property"].is_linked:
attributes = ""
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
attributes += f", {inp.name.lower().replace(' ', '_')}={inp.python_value}"
attribute = f"attr_{self.static_uid} = '[\"' + str({self.inputs['Property'].python_attr[1:-1]} + '\"]') " if self.inputs["Property"].python_is_attribute else ""
self.code = f"""
{attribute}
{self.active_layout}.prop({self.inputs['Property'].python_source}, {f"attr_{self.static_uid}" if self.inputs["Property"].python_is_attribute else "'" + self.inputs['Property'].python_attr.replace("'", '"') + "'"}, text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}{attributes})
{self.indent(self.outputs[0].python_value, 7)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 7)}
"""
@@ -0,0 +1,31 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplaySearchNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySearchNodeNew"
bl_label = "Display Search"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input("Collection")
self.add_property_input("String/Pointer")
self.add_string_input("Label")
self.add_icon_input("Blender Icon").subtype = "BLENDER_ONLY"
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if len(self.inputs["String/Pointer"].links) and len(self.inputs["Collection"].links):
self.code = f"""
{self.active_layout}.prop_search({self.inputs['String/Pointer'].python_source}, '{self.inputs['String/Pointer'].python_attr.replace("'",'"')}', {self.inputs['Collection'].python_source}, '{self.inputs['Collection'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, icon={self.inputs['Blender Icon'].python_value})
{self.indent(self.outputs[0].python_value, 4)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 4)}
"""
@@ -0,0 +1,70 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplaySerpensShortcutNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySerpensShortcutNodeNew"
bl_label = "Display Serpens Shortcut"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_interface_output().passthrough_layout_type = True
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
ref_SN_OnKeypressNode: bpy.props.StringProperty(name="Shortcut",
description="The shortcut to display",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.ref_ntree and self.ref_SN_OnKeypressNode in self.ref_ntree.nodes:
self.code_imperative = """
def find_user_keyconfig(key):
km, kmi = addon_keymaps[key]
for item in bpy.context.window_manager.keyconfigs.user.keymaps[km.name].keymap_items:
found_item = False
if kmi.idname == item.idname:
found_item = True
for name in dir(kmi.properties):
if not name in ["bl_rna", "rna_type"] and not name[0] == "_":
if name in kmi.properties and name in item.properties and not kmi.properties[name] == item.properties[name]:
found_item = False
if found_item:
return item
print(f"Couldn't find keymap item for {key}, using addon keymap instead. This won't be saved across sessions!")
return kmi
"""
node = self.ref_ntree.nodes[self.ref_SN_OnKeypressNode]
src = f"find_user_keyconfig('{node.static_uid}')"
self.code = f"""
{self.active_layout}.prop({src}, 'type', text={self.inputs['Label'].python_value}, full_event=True)
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OnKeypressNode", parent_tree.node_collection(
"SN_OnKeypressNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_OnKeypressNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="",
icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_OnKeypressNode
layout.label(
text="Copy blender shortcuts from the blend data browser in display property", icon="INFO")
@@ -0,0 +1,45 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...utils import normalize_code
class SN_EnumMapInterfaceNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_EnumMapInterfaceNode"
bl_label = "Enum Map (Interface)"
bl_width_default = 200
node_color = "INTERFACE"
passthrough_layout_type = True
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Enum Value")
self.add_interface_output("Interface")
self.add_interface_output("Other Option")
out = self.add_dynamic_interface_output("Enum Option")
out.is_variable = True
def on_socket_name_change(self, socket):
self._evaluate(bpy.context)
def evaluate(self, context):
options = ""
for out in self.outputs:
if out.is_variable and not out.dynamic:
option_code = self.indent(out.python_value, 7)
option = f"""
{"el" if options else ""}if {self.inputs["Enum Value"].python_value} == "{out.name}":
{option_code}
"""
if option_code.strip():
options += normalize_code(option) + "\n"
other_opt_code = self.indent(self.outputs['Other Option'].python_value, 6)
self.code = f"""
{self.indent(options, 5)}
{"else:" if options.strip() else "if True:"}
{other_opt_code if other_opt_code.strip() else "pass"}
{self.indent(self.outputs[0].python_value, 5)}
"""
@@ -0,0 +1,30 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_IconGalleryNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IconGalleryNodeNew"
bl_label = "Icon Gallery"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input("Enum")
self.add_boolean_input("Show Labels")
self.add_float_input("Scale").default_value = 5
self.add_float_input("Scale Popup").default_value = 5
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Enum"].is_linked:
self.code = f"""
{self.active_layout}.template_icon_view({self.inputs['Enum'].python_source}, '{self.inputs['Enum'].python_attr}', show_labels={self.inputs['Show Labels'].python_value}, scale={self.inputs['Scale'].python_value}, scale_popup={self.inputs['Scale Popup'].python_value})
{self.indent(self.outputs[0].python_value, 4)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 4)}
"""
@@ -0,0 +1,26 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_LabelNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LabelNodeNew"
bl_label = "Label"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label").default_value = "My Label"
self.add_icon_input("Icon")
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
self.code = f"""
{self.active_layout}.label(text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
{self.indent(self.outputs[0].python_value, 3)}
"""
def draw_node(self, context, layout):
pass
@@ -0,0 +1,73 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToMenuNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToMenuNodeNew"
bl_label = "Add To Menu"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Menu")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the menu"),
("APPEND", "Append", "Append this to the end of the menu")],
name="Position",
description="Position of this interface to the selected menu",
update=SN_ScriptingBaseNode._evaluate)
menu_parent: bpy.props.StringProperty(default="VIEW3D_MT_add",
name="Parent",
description="The menu id this interface should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.menu_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value if out.name == 'Menu' else '' for out in self.outputs], 5)}
"""
if self.menu_parent == "WM_MT_button_context":
self.code_imperative = """
class WM_MT_button_context(bpy.types.Menu):
bl_label = ""
def draw(self, context):
pass
"""
self.code_register = f"""
{"if getattr(bpy.types, 'WM_MT_button_context', None) == None: bpy.utils.register_class(WM_MT_button_context)" if self.menu_parent == "WM_MT_button_context" else ""}
bpy.types.{self.menu_parent}.{self.append.lower()}({func_name})
"""
self.code_unregister = f"""
bpy.types.{self.menu_parent}.remove({func_name})
"""
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_menu_picker", text=f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,60 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToPanelNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToPanelNodeNew"
bl_label = "Add To Panel"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Panel")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the panel"),
("APPEND", "Append", "Append this to the end of the panel")],
name="Position",
description="Position of this interface to the selected panel",
update=SN_ScriptingBaseNode._evaluate)
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.panel_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value if out.name == 'Panel' else '' for out in self.outputs], 7)}
"""
self.code_register = f"bpy.types.{self.panel_parent}.{self.append.lower()}({func_name})"
self.code_unregister = f"bpy.types.{self.panel_parent}.remove({func_name})"
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,111 @@
import bpy
from ....utils import get_python_name
from ...base_node import SN_ScriptingBaseNode
class SN_RunInterfaceFunctionNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunInterfaceFunctionNodeNew"
bl_label = "Function Run (Interface)"
bl_width_default = 240
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_dynamic_interface_output().passthrough_layout_type = True
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
if node.bl_idname == "SN_InterfaceFunctionNode" and data:
index = 0
for i, out in enumerate(node.outputs):
if out.bl_idname != "SN_InterfaceSocket":
index = i-1
break
# inputs has been added
if "added" in data:
socket_index = list(data["added"].node.outputs).index(data["added"])
self.add_input_from_socket(data["added"])
self.inputs.move(len(self.inputs)-1, socket_index-index)
# input has been removed
elif "removed" in data:
self.inputs.remove(self.inputs[data["removed"]])
# input has changed
elif "changed" in data:
self.convert_socket(self.inputs[data["changed"].index-index], data["changed"].bl_idname)
# input has updated
elif "updated" in data:
if data["updated"].index-index < len(self.inputs):
self.inputs[data["updated"].index-index].name = data["updated"].name
self._evaluate(bpy.context)
def update_function_reference(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
# remember connections
links = []
for inp in self.inputs[1:]:
links.append(None)
if inp.is_linked:
links[-1] = inp.from_socket()
# remove current data inputs
for i in range(len(self.inputs)-1, 0, -1):
self.inputs.remove(self.inputs[i])
# add new data inputs
if self.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
for out in parent_tree.nodes[self.ref_SN_InterfaceFunctionNode].outputs[1:-1]:
if out.bl_idname != "SN_InterfaceSocket":
self.add_input_from_socket(out)
# restore connections
if len(links) == len(self.inputs)-1:
for i, from_socket in enumerate(links):
if from_socket:
self.node_tree.links.new(from_socket, self.inputs[i+1])
self._evaluate(context)
ref_SN_InterfaceFunctionNode: bpy.props.StringProperty(name="Function",
description="The function to run",
update=update_function_reference)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
if self.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
# get input values
inp_values = []
for inp in self.inputs[1:]:
inp_values.append(inp.python_value)
inp_values = ", ".join(inp_values)
self.code = f"""
layout_function = {self.active_layout}
{parent_tree.nodes[self.ref_SN_InterfaceFunctionNode].func_name}(layout_function, {inp_values})
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 6)}
"""
else:
self.code = f"""
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_InterfaceFunctionNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_InterfaceFunctionNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_InterfaceFunctionNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_InterfaceFunctionNode
@@ -0,0 +1,94 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....utils import get_python_name, unique_collection_name
class SN_InterfaceFunctionNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_InterfaceFunctionNode"
bl_label = "Function (Interface)"
def layout_type(self, _): return "layout_function"
is_trigger = True
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_dynamic_interface_output()
out = self.add_dynamic_data_output("Input")
out.is_variable = True
out.changeable = True
def on_dynamic_socket_add(self, socket):
if not socket.bl_idname == "SN_InterfaceSocket":
sockets = []
for out in self.outputs:
if not out.bl_idname in ["SN_InterfaceSocket", "SN_DynamicInterfaceSocket"]:
sockets.append(out.name)
socket["name"] = get_python_name(socket.name, "Input", lower=False)
socket["name"] = unique_collection_name(socket.name, "Input", sockets[:-1], "_", includes_name=True)
self.trigger_ref_update({ "added": socket })
self._evaluate(bpy.context)
def on_dynamic_socket_remove(self, index, is_output):
first_index = 0
for out in self.outputs:
if out.bl_idname in ["SN_InterfaceSocket", "SN_DynamicInterfaceSocket"]:
first_index += 1
if index >= first_index:
self.trigger_ref_update({ "removed": index - first_index + 1 })
def on_socket_type_change(self, socket):
if not socket.bl_idname == "SN_InterfaceSocket":
self.trigger_ref_update({ "changed": socket })
def on_socket_name_change(self, socket):
if not socket.bl_idname == "SN_InterfaceSocket":
sockets = []
for out in self.outputs:
if not out.bl_idname in ["SN_InterfaceSocket", "SN_DynamicInterfaceSocket"]:
sockets.append(out.name)
socket["name"] = get_python_name(socket.name, "Input", lower=False)
socket["name"] = unique_collection_name(socket.name, "Input", sockets[:-1], "_", includes_name=True)
self.trigger_ref_update({ "updated": socket })
self._evaluate(bpy.context)
@property
def func_name(self):
return f"sna_{get_python_name(self.name, 'func')}_{self.static_uid}"
def evaluate(self, context):
out_values = []
index = 0
for i, out in enumerate(self.outputs):
if out.bl_idname != "SN_InterfaceSocket":
index = i
break
for i, out in enumerate(self.outputs[index:-1]):
out_values.append(get_python_name(out.name, f"parameter_{i}"))
out_names = ", ".join(out_values)
if index > 1:
code = self.indent([out.python_value for out in self.outputs[:index-1]], 6)
else:
code = self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 0)
self.code = f"""
def {self.func_name}(layout_function, {out_names}):
{code if code.strip() else "pass"}
"""
for i, out in enumerate(self.outputs[index:-1]):
out.python_value = out_values[i]
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "name")
op = row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM")
op.node = self.name
op.add_node = "SN_RunInterfaceFunctionNodeNew"
row.operator("sn.copy_python_name", text="", icon="COPYDOWN").name = self.func_name
@@ -0,0 +1,68 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_MenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_MenuNode"
bl_label = "Menu"
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_integer_input("Columns")["default_value"] = 1
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Menu")
idname_override: bpy.props.StringProperty(default="",
name="Idname Override",
description="Use this if you want to define the idname of this panel yourself",
update=SN_ScriptingBaseNode._evaluate)
title: bpy.props.StringProperty(default="",
name="Label",
description="The label shown at the top of this menu",
update=SN_ScriptingBaseNode._evaluate)
@property
def idname(self):
if self.idname_override:
return self.idname_override
return f"SNA_MT_{self.static_uid}"
def evaluate(self, context):
self.trigger_ref_update()
self.code = f"""
class {self.idname}(bpy.types.Menu):
bl_idname = "{self.idname}"
bl_label = "{self.title}"
@classmethod
def poll(cls, context):
return not ({self.inputs["Hide"].python_value})
def draw(self, context):
layout = self.layout.column_flow(columns={self.inputs["Columns"].python_value})
layout.operator_context = "INVOKE_DEFAULT"
{self.indent([out.python_value if out.name == 'Menu' else '' for out in self.outputs], 5)}
"""
self.code_register = f"""
bpy.utils.register_class({self.idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({self.idname})
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "name")
row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM").node = self.name
layout.prop(self, "title")
def draw_node_panel(self, context, layout):
layout.prop(self, "idname_override")
@@ -0,0 +1,295 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....utils import get_python_name
class SN_PanelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PanelNode"
bl_label = "Panel"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Panel")
self.add_dynamic_interface_output("Header")
self.ref_ntree = self.node_tree
def update_from_parent(self, parent):
""" Update this subpanels nodes values from the parent node """
self["space"] = parent.space
self["region"] = parent.region
self["context"] = parent.context
def update_custom_parent(self, context):
""" Updates the nodes settings when a new parent panel is selected """
if self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
parent = self.ref_ntree.nodes[self.ref_SN_PanelNode]
self.update_from_parent(parent)
self._evaluate(context)
def on_ref_update(self, node, data=None):
""" Called when a parent panel is updated, updates this nodes settings """
if self.is_subpanel:
if node.node_tree == self.ref_ntree and node.name == self.ref_SN_PanelNode:
self.update_from_parent(node)
self._evaluate(bpy.context)
def update_is_subpanel(self, context):
""" Updates the compile order when turned into a subpanel """
if self.is_subpanel:
self["parent_type"] = "BLENDER"
self["ref_SN_PanelNode"] = ""
self["space"] = "PROPERTIES"
self["region"] = "WINDOW"
self["context"] = "render"
self.order = 1
else:
self["space"] = "VIEW_3D"
self["region"] = "UI"
self["context"] = ""
self.order = 0
self._evaluate(context)
panel_label: bpy.props.StringProperty(default="New Panel",
name="Label",
description="The label of your panel",
update=SN_ScriptingBaseNode._evaluate)
space: bpy.props.StringProperty(default="VIEW_3D",
name="Space",
description="The space your panel is in",
update=SN_ScriptingBaseNode._evaluate)
region: bpy.props.StringProperty(default="UI",
name="Region",
description="The region your panel is in",
update=SN_ScriptingBaseNode._evaluate)
context: bpy.props.StringProperty(default="",
name="Context",
description="The context your panel is in",
update=SN_ScriptingBaseNode._evaluate)
category: bpy.props.StringProperty(default="New Category",
name="Category",
description="The category your panel is in (Only relevant if in an N-Panel)",
update=SN_ScriptingBaseNode._evaluate)
panel_order: bpy.props.IntProperty(default=0, min=0,
name="Order",
description="The order of your panel compared to other custom panels",
update=SN_ScriptingBaseNode._evaluate)
hide_header: bpy.props.BoolProperty(default=False,
name="Hide Header",
description="Hide the panels header",
update=SN_ScriptingBaseNode._evaluate)
expand_header: bpy.props.BoolProperty(default=False,
name="Expand Header",
description="Expands the header to fill the full panel width",
update=SN_ScriptingBaseNode._evaluate)
default_closed: bpy.props.BoolProperty(default=False,
name="Default Closed",
description="Closes the panel by default",
update=SN_ScriptingBaseNode._evaluate)
def update_shortcut_only(self, context):
if self.shortcut_only:
self.space = "VIEW_3D"
self.region = "WINDOW"
self.context = ""
self.category = ""
self._evaluate(context)
shortcut_only: bpy.props.BoolProperty(default=False,
name="Shortcut Only",
description="Only displays this panel when opened with a shortcut and not in the interface",
update=update_shortcut_only)
is_subpanel: bpy.props.BoolProperty(default=False,
name="Is Subpanel",
description="If this panel should be a subpanel",
update=update_is_subpanel)
panel_parent: bpy.props.StringProperty(default="RENDER_PT_context",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
ref_SN_PanelNode: bpy.props.StringProperty(name="Custom Parent",
description="The panel used as a custom parent panel for this subpanel",
update=update_custom_parent)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
idname_override: bpy.props.StringProperty(default="",
name="Idname Override",
description="Use this if you want to define the idname of this panel yourself",
update=SN_ScriptingBaseNode._evaluate)
panel_width: bpy.props.IntProperty(name="Panel Width", default=0,
description="The panel width for popovers and popups",
update=SN_ScriptingBaseNode._evaluate)
last_idname: bpy.props.StringProperty(name="Last Idname",
description="The last idname that this panel had when it was compiled",
default="")
def update_idname(self):
py_name = get_python_name(self.panel_label)
alt_py_name = get_python_name(self.name)
idname = f"SNA_PT_{py_name.upper() if py_name else alt_py_name}_{self.uuid}"
if self.idname_override:
idname = self.idname_override
self.last_idname = idname
self.trigger_ref_update()
def evaluate(self, context):
self.update_idname()
options = []
if self.hide_header: options.append("'HIDE_HEADER'")
if self.expand_header: options.append("'HEADER_LAYOUT_EXPAND'")
if self.default_closed: options.append("'DEFAULT_CLOSED'")
parent = ""
if self.is_subpanel:
if self.parent_type == "BLENDER":
parent = self.panel_parent
elif self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
parent_node = self.ref_ntree.nodes[self.ref_SN_PanelNode]
parent = parent_node.last_idname
self.code = f"""
class {self.last_idname}(bpy.types.Panel):
bl_label = '{self.panel_label}'
bl_idname = '{self.last_idname}'
bl_space_type = '{self.space}'
bl_region_type = '{self.region}'
bl_context = '{self.context}'
{f"bl_category = '{self.category}'" if self.category and not parent else ""}
bl_order = {self.panel_order}
{f"bl_options = {{{', '.join(options)}}}" if options else ""}
{f"bl_parent_id = '{parent}'" if self.is_subpanel and parent else ""}
bl_ui_units_x={self.panel_width}
@classmethod
def poll(cls, context):
return not ({self.inputs["Hide"].python_value})
def draw_header(self, context):
layout = self.layout
{self.indent([out.python_value if out.name == 'Header' else '' for out in self.outputs], 7)}
def draw(self, context):
layout = self.layout
{self.indent([out.python_value if out.name == 'Panel' else '' for out in self.outputs], 7)}
"""
if self.is_subpanel and parent and not context.scene.sn.is_exporting and self.parent_type == "CUSTOM":
self.code_register = f"if '{parent}' in globals(): bpy.utils.register_class({self.last_idname})"
self.code_unregister = f"if '{parent}' in globals(): bpy.utils.unregister_class({self.last_idname})"
else:
if bpy.context.scene.sn.is_exporting:
self.code_register = f"bpy.utils.register_class({self.last_idname})"
self.code_unregister = f"bpy.utils.unregister_class({self.last_idname})"
else:
self.code_register = f"""
try: bpy.utils.register_class({self.last_idname})
except: pass
"""
self.code_unregister = f"""
try: bpy.utils.unregister_class({self.last_idname})
except: pass
"""
def draw_node(self, context, layout):
if not self.shortcut_only:
if not self.is_subpanel:
row = layout.row(align=True)
row.scale_y = 1.4
op = row.operator("sn.activate_panel_picker", text=f"{self.space.replace('_', ' ').title()} {self.region.replace('_', ' ').title()} {self.context.replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
else:
row = layout.row(align=True)
if self.parent_type == "BLENDER":
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
row.prop(self, "parent_type", text="", icon_only=True)
else:
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_PanelNode", bpy.data.node_groups[parent_tree.name].node_collection(self.bl_idname), "refs", text="")
row.prop(self, "parent_type", text="", icon_only=True)
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_PanelNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_PanelNode
if self.ref_SN_PanelNode == self.name and self.ref_ntree == self.node_tree:
layout.label(text="Can't use self as panel parent!", icon="ERROR")
layout.prop(self, "is_subpanel")
row = layout.row(align=True)
row.prop(self, "name")
row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM").node = self.name
layout.prop(self, "panel_label")
if not self.shortcut_only:
if not self.is_subpanel:
layout.prop(self, "category")
layout.prop(self, "panel_order")
layout.prop(self, "hide_header")
layout.prop(self, "expand_header")
layout.prop(self, "default_closed")
layout.prop(self, "shortcut_only")
def draw_node_panel(self, context, layout):
col = layout.column()
col.enabled = not self.is_subpanel
col.prop(self, "space")
col.prop(self, "region")
col.prop(self, "context")
row = layout.row()
row.enabled = self.is_subpanel
row.prop(self, "panel_parent")
layout.prop(self, "idname_override")
layout.prop(self, "panel_width")
@@ -0,0 +1,66 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_PieMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PieMenuNode"
bl_label = "Pie Menu"
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Menu")
idname_override: bpy.props.StringProperty(default="",
name="Idname Override",
description="Use this if you want to define the idname of this panel yourself",
update=SN_ScriptingBaseNode._evaluate)
title: bpy.props.StringProperty(default="",
name="Label",
description="The label shown at the top of this menu",
update=SN_ScriptingBaseNode._evaluate)
@property
def idname(self):
if self.idname_override:
return self.idname_override
return f"SNA_MT_{self.static_uid}"
def evaluate(self, context):
self.trigger_ref_update()
self.code = f"""
class {self.idname}(bpy.types.Menu):
bl_idname = "{self.idname}"
bl_label = "{self.title}"
@classmethod
def poll(cls, context):
return not ({self.inputs["Hide"].python_value})
def draw(self, context):
layout = self.layout.menu_pie()
{self.indent([out.python_value if out.name == 'Menu' else '' for out in self.outputs], 5)}
"""
self.code_register = f"""
bpy.utils.register_class({self.idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({self.idname})
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "name")
row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM").node = self.name
layout.prop(self, "title")
def draw_node_panel(self, context, layout):
layout.prop(self, "idname_override")
@@ -0,0 +1,103 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ...templates.PropertyNode import PropertyNode
class SN_PreferencesNode(SN_ScriptingBaseNode, bpy.types.Node, PropertyNode):
bl_idname = "SN_PreferencesNode"
bl_label = "Preferences"
bl_width_default = 200
def layout_type(self, _):
return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide").default_value = False
self.add_dynamic_interface_output("Preferences")
def evaluate(self, context):
props_imperative_list = self.props_imperative(context).split("\n")
props_code_list = self.props_code(context).split("\n")
props_register_list = self.props_register(context).split("\n")
props_unregister_list = self.props_unregister(context).split("\n")
idname = f"SNA_TempAddonPreferences_{self.static_uid}"
prop_name = f"sna_addon_prefs_temp"
self.code = f"""
{self.indent(props_imperative_list, 5)}
class {idname}(bpy.types.PropertyGroup):
{self.indent(props_code_list, 6) if self.indent(props_code_list, 6).strip() else "pass"}
def sna_prefs(layout):
if not ({self.inputs["Hide"].python_value}):
self = bpy.context.scene.{prop_name}
{self.indent([out.python_value for out in self.outputs[:-1]], 7)}
"""
self.code_register = f"""
{self.indent(props_register_list, 7)}
bpy.utils.register_class({idname})
bpy.types.Scene.{prop_name} = bpy.props.PointerProperty(type={idname})
bpy.context.scene.sn.preferences.append(sna_prefs)
for a in bpy.context.screen.areas: a.tag_redraw()
"""
self.code_unregister = f"""
bpy.context.scene.sn.preferences.clear()
for a in bpy.context.screen.areas: a.tag_redraw()
del bpy.types.Scene.{prop_name}
bpy.utils.unregister_class({idname})
{self.indent(props_unregister_list, 7)}
"""
def evaluate_export(self, context):
props_imperative_list = self.props_imperative(context).split("\n")
props_code_list = self.props_code(context).split("\n")
props_register_list = self.props_register(context).split("\n")
props_unregister_list = self.props_unregister(context).split("\n")
idname = f"SNA_AddonPreferences_{self.static_uid}"
self.code = f"""
{self.indent(props_imperative_list, 5)}
class {idname}(bpy.types.AddonPreferences):
bl_idname = {"__package__" if bpy.context.scene.sn.is_exporting else "'"+bpy.context.scene.sn.module_name+"'"}
{self.indent(props_code_list, 6)}
def draw(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value if out.name == 'Preferences' else '' for out in self.outputs], 8)}
"""
self.code_register = f"""
{self.indent(props_register_list, 7)}
bpy.utils.register_class({idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({idname})
{self.indent(props_unregister_list, 7)}
"""
def draw_node(self, context, layout):
layout.operator("sn.open_preferences", icon="PREFERENCES").navigation = "CUSTOM"
amount = 0
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
amount += len(ntree.node_collection(self.bl_idname).refs)
if amount > 1:
layout.label(
text="Multiple preferences nodes! Only one will be used", icon="ERROR"
)
self.draw_list(layout)
@@ -0,0 +1,73 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToMenuNode"
bl_label = "Add To Menu (Legacy)"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_interface_output("Menu").prev_dynamic = True
self.add_dynamic_interface_output("Menu")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the menu"),
("APPEND", "Append", "Append this to the end of the menu")],
name="Position",
description="Position of this interface to the selected menu",
update=SN_ScriptingBaseNode._evaluate)
menu_parent: bpy.props.StringProperty(default="VIEW3D_MT_add",
name="Parent",
description="The menu id this interface should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.menu_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value for out in self.outputs[:-1]], 5)}
"""
if self.menu_parent == "WM_MT_button_context":
self.code_imperative = """
class WM_MT_button_context(bpy.types.Menu):
bl_label = "Unused"
def draw(self, context):
pass
"""
self.code_register = f"""
{"if not hasattr(bpy.types, 'WM_MT_button_context'): bpy.utils.register_class(WM_MT_button_context)" if self.menu_parent == "WM_MT_button_context" else ""}
bpy.types.{self.menu_parent}.{self.append.lower()}({func_name})
"""
self.code_unregister = f"""
bpy.types.{self.menu_parent}.remove({func_name})
"""
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_menu_picker", text=f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,61 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToPanelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToPanelNode"
bl_label = "Add To Panel (Legacy)"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_interface_output("Panel").prev_dynamic = True
self.add_dynamic_interface_output("Panel")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the panel"),
("APPEND", "Append", "Append this to the end of the panel")],
name="Position",
description="Position of this interface to the selected panel",
update=SN_ScriptingBaseNode._evaluate)
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.panel_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value for out in self.outputs[:-1]], 7)}
"""
self.code_register = f"bpy.types.{self.panel_parent}.{self.append.lower()}({func_name})"
self.code_unregister = f"bpy.types.{self.panel_parent}.remove({func_name})"
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,164 @@
import bpy
from ...Program.Run_Operator import on_operator_ref_update
from ...base_node import SN_ScriptingBaseNode
class SN_ButtonNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ButtonNode"
bl_label = "Button (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.version = 1
self.add_interface_input()
self.add_string_input("Label").default_value = "My Button"
self.add_boolean_input("Emboss").default_value = True
self.add_boolean_input("Depress")
self.add_icon_input("Icon")
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
on_operator_ref_update(self, node, data, self.ref_ntree, self.ref_SN_OperatorNode, 5)
def reset_inputs(self):
""" Remove all operator inputs """
for i in range(len(self.inputs)-1, -1, -1):
inp = self.inputs[i]
if inp.can_be_disabled:
self.inputs.remove(inp)
def create_inputs(self, op_rna):
""" Create inputs for operator """
for prop in op_rna.properties:
if not prop.identifier in ["rna_type", "settings"]:
inp = self.add_input_from_property(prop)
if inp:
inp.can_be_disabled = True
inp.disabled = not prop.is_required
def update_custom_operator(self, context):
""" Updates the nodes settings when a new parent panel is selected """
self.reset_inputs()
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
parent = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
for prop in parent.properties:
if prop.property_type in ["Integer", "Float", "Boolean"] and prop.settings.is_vector:
socket = self._add_input(self.socket_names[prop.property_type + " Vector"], prop.name)
socket.size = prop.settings.size
socket.can_be_disabled = True
else:
self._add_input(self.socket_names[prop.property_type], prop.name).can_be_disabled = True
self._evaluate(context)
def update_source_type(self, context):
self.hide_disabled_inputs = False
if self.source_type == "BLENDER":
self.pasted_operator = self.pasted_operator
elif self.source_type == "CUSTOM":
self.ref_SN_OperatorNode = self.ref_SN_OperatorNode
self._evaluate(context)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the operator from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
source_type: bpy.props.EnumProperty(name="Source Type",
description="Use a custom operator or a blender internal",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=update_source_type)
ref_SN_OperatorNode: bpy.props.StringProperty(name="Custom Operator",
description="The operator ran by this button",
update=update_custom_operator)
def update_pasted_operator(self, context):
self.reset_inputs()
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
self.pasted_name = op_rna.name
self.create_inputs(op_rna)
self._evaluate(context)
pasted_operator: bpy.props.StringProperty(default="bpy.ops.sn.dummy_button_operator()",
update=update_pasted_operator)
pasted_name: bpy.props.StringProperty(default="Paste Operator")
def update_hide_disabled_inputs(self, context):
for inp in self.inputs:
if inp.can_be_disabled and inp.disabled:
inp.set_hide(self.hide_disabled_inputs)
hide_disabled_inputs: bpy.props.BoolProperty(default=False,
name="Hide Disabled Inputs",
description="Hides the disabled inputs of this node",
update=update_hide_disabled_inputs)
def evaluate(self, context):
if self.source_type == "BLENDER":
op_name = self.pasted_operator[8:].split("(")[0]
self.code = f"op = {self.active_layout}.operator('{op_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in op_rna.properties:
if (self.version == 0 and (prop.name and prop.name == inp.name) or (not prop.name and prop.identifier.replace("_", " ").title() == inp.name)) \
or (self.version == 1 and inp.name.replace(" ", "_").lower() == prop.identifier):
self.code += "\n" + f"op.{prop.identifier} = {inp.python_value}"
else:
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
self.code = f"op = {self.active_layout}.operator('sna.{node.operator_python_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in node.properties:
if prop.name == inp.name:
self.code += "\n" + f"op.{prop.python_name} = {inp.python_value}"
else:
self.code = f"op = {self.active_layout}.operator('sn.dummy_button_operator', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "source_type", text="", icon_only=True)
if self.source_type == "BLENDER":
name = "Paste Operator"
if self.pasted_operator:
if self.pasted_name:
name = self.pasted_name
elif len(self.pasted_operator.split(".")) > 2:
name = self.pasted_operator.split(".")[3].split("(")[0].replace("_", " ").title()
else:
name = self.pasted_operator
op = row.operator("sn.paste_operator", text=name, icon="PASTEDOWN")
op.node_tree = self.node_tree.name
op.node = self.name
elif self.source_type == "CUSTOM":
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OperatorNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_OperatorNode"), "refs", text="")
row.prop(self, "hide_disabled_inputs", text="", icon="HIDE_ON" if self.hide_disabled_inputs else "HIDE_OFF", emboss=False)
@@ -0,0 +1,40 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CopyMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CopyMenuNode"
bl_label = "Copy Menu (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
menu_parent: bpy.props.StringProperty(name="Menu",
default="VIEW3D_MT_add",
description="The menu that should be displayed",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code = f"""
if hasattr(bpy.types,"{self.menu_parent}"):
if not hasattr(bpy.types.{self.menu_parent}, "poll") or bpy.types.{self.menu_parent}.poll(context):
bpy.types.{self.menu_parent}.draw(self, context)
else:
{self.active_layout}.label(text="Can't display this menu here!", icon="ERROR")
else:
{self.active_layout}.label(text="Can't display this menu!", icon="ERROR")
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
name = f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}"
op = row.operator("sn.activate_menu_picker", icon="EYEDROPPER", text=name)
op.node_tree = self.node_tree.name
op.node = self.name
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,40 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CopyPanelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CopyPanelNode"
bl_label = "Copy Panel (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code = f"""
if hasattr(bpy.types,"{self.panel_parent}"):
if not hasattr(bpy.types.{self.panel_parent}, "poll") or bpy.types.{self.panel_parent}.poll(context):
bpy.types.{self.panel_parent}.draw(self, context)
else:
{self.active_layout}.label(text="Can't display this panel here!", icon="ERROR")
else:
{self.active_layout}.label(text="Can't display this panel!", icon="ERROR")
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,48 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....utils import get_python_name
class SN_DisplayCollectionListNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayCollectionListNode"
bl_label = "Display Collection List (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def layout_type(self, _): return "layout"
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input()
self.add_property_input("Index Property")
self.add_integer_input("Rows")
self.add_interface_output("Item Row").prev_dynamic = True
self.add_dynamic_interface_output("Item Row")
self.add_property_output("Item")
self.add_integer_output("Item Index")
def evaluate(self, context):
if self.inputs["Index Property"].is_linked and self.inputs["Collection Property"].is_linked:
ui_list_idname = f"SNA_UL_{get_python_name(self.name, 'List')}_{self.static_uid}"
self.code_imperative = f"""
class {ui_list_idname}(bpy.types.UIList):
def draw_item(self, context, layout, data, item_{self.static_uid}, icon, active_data, active_propname, index_{self.static_uid}):
row = layout
{self.indent([out.python_value for out in self.outputs[:-2]], 11)}
"""
self.code_register = f"""
bpy.utils.register_class({ui_list_idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({ui_list_idname})
"""
self.code = f"{self.active_layout}.template_list('{ui_list_idname}', '{self.static_uid}', {self.inputs['Collection Property'].python_source}, '{self.inputs['Collection Property'].python_attr}', {self.inputs['Index Property'].python_source}, '{self.inputs['Index Property'].python_attr}', rows={self.inputs['Rows'].python_value})"
self.outputs["Item"].python_value = f"item_{self.static_uid}"
if "Item Index" in self.outputs:
self.outputs["Item Index"].python_value = f"index_{self.static_uid}"
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
self.outputs["Item"].reset_value()
if "Item Index" in self.outputs:
self.outputs["Item Index"].reset_value()
@@ -0,0 +1,21 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplayIconNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayIconNode"
bl_label = "Display Icon (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_icon_input("Icon")
self.add_float_input("Scale").default_value = 1
def evaluate(self, context):
self.code = f"{self.active_layout}.template_icon(icon_value={self.inputs['Icon'].python_value}, scale={self.inputs['Scale'].python_value})"
def draw_node(self, context, layout):
layout.label(text="Use this node with care! This might slow down your UI", icon="INFO")
@@ -0,0 +1,29 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplayPreviewNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPreviewNode"
bl_label = "Display Preview (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_boolean_input("Show Buttons")
def evaluate(self, context):
if self.inputs["Property"].is_linked:
self.code = f"""
{self.active_layout}.template_preview({self.inputs['Property'].python_value}, show_buttons={self.inputs['Show Buttons'].python_value})
"""
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
def draw_node(self, context, layout):
box = layout.box()
box.label(text="Careful! This element is prone to crashes!", icon="INFO")
box.label(text="Only materials, textures, lights, worlds and line styles can be displayed")
@@ -0,0 +1,55 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplayPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPropertyNode"
bl_label = "Display Property (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_string_input("Label")
self.add_icon_input("Icon")
self.add_boolean_input("Emboss").default_value = True
inp = self.add_boolean_input("Expand")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Slider")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Toggle")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Invert Checkbox")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Full Shortcut")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_integer_input("Index")
inp.can_be_disabled = True
inp.disabled = True
def evaluate(self, context):
if self.inputs["Property"].is_linked:
attributes = ""
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
attributes += f", {inp.name.lower().replace(' ', '_')}={inp.python_value}"
self.code = f"""
{self.active_layout}.prop({self.inputs['Property'].python_source}, '{self.inputs['Property'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}{attributes})
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
"""
@@ -0,0 +1,24 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplaySearchNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySearchNode"
bl_label = "Display Search (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input("Collection")
self.add_property_input("String/Pointer")
self.add_string_input("Label")
self.add_icon_input("Blender Icon").subtype = "BLENDER_ONLY"
def evaluate(self, context):
if len(self.inputs["String/Pointer"].links) and len(self.inputs["Collection"].links):
self.code = f"""{self.active_layout}.prop_search({self.inputs['String/Pointer'].python_source}, '{self.inputs['String/Pointer'].python_attr.replace("'",'"')}', {self.inputs['Collection'].python_source}, '{self.inputs['Collection'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, icon={self.inputs['Blender Icon'].python_value})"""
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
@@ -0,0 +1,58 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplaySerpensShortcutNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySerpensShortcutNode"
bl_label = "Display Serpens Shortcut (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
ref_SN_OnKeypressNode: bpy.props.StringProperty(name="Shortcut",
description="The shortcut to display",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.ref_ntree and self.ref_SN_OnKeypressNode in self.ref_ntree.nodes:
self.code_imperative = """
def find_user_keyconfig(key):
km, kmi = addon_keymaps[key]
for item in bpy.context.window_manager.keyconfigs.user.keymaps[km.name].keymap_items:
found_item = False
if kmi.idname == item.idname:
found_item = True
for name in dir(kmi.properties):
if not name in ["bl_rna", "rna_type"] and not name[0] == "_":
if not kmi.properties[name] == item.properties[name]:
found_item = False
if found_item:
return item
print(f"Couldn't find keymap item for {key}, using addon keymap instead. This won't be saved across sessions!")
return kmi
"""
node = self.ref_ntree.nodes[self.ref_SN_OnKeypressNode]
src = f"find_user_keyconfig('{node.static_uid}')"
self.code = f"{self.active_layout}.prop({src}, 'type', text={self.inputs['Label'].python_value}, full_event=True)"
def draw_node(self, context, layout):
row = layout.row(align=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OnKeypressNode", parent_tree.node_collection("SN_OnKeypressNode"), "refs", text="")
layout.label(text="Copy blender shortcuts from the blend data browser in display property", icon="INFO")
@@ -0,0 +1,27 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IconGalleryNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IconGalleryNode"
bl_label = "Icon Gallery (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input("Enum")
self.add_boolean_input("Show Labels")
self.add_float_input("Scale").default_value = 5
self.add_float_input("Scale Popup").default_value = 5
def evaluate(self, context):
if self.inputs["Enum"].is_linked:
self.code = f"""
{self.active_layout}.template_icon_view({self.inputs['Enum'].python_source}, '{self.inputs['Enum'].python_attr}', show_labels={self.inputs['Show Labels'].python_value}, scale={self.inputs['Scale'].python_value}, scale_popup={self.inputs['Scale Popup'].python_value})
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
"""
@@ -0,0 +1,29 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IfElseInterfaceNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IfElseInterfaceNode"
bl_label = "If/Else (Interface) (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
passthrough_layout_type = True
def on_create(self, context):
self.add_interface_input()
self.add_boolean_input("Condition").default_value = True
self.add_interface_output("True")
self.add_interface_output("False")
self.add_interface_output("Continue")
def evaluate(self, context):
self.code = f"""
if {self.inputs['Condition'].python_value}:
{self.indent(self.outputs['True'].python_value, 6) if self.outputs['True'].python_value.strip() else 'pass'}
{"else:" if self.outputs['False'].python_value.strip() else ""}
{self.indent(self.outputs['False'].python_value, 6) if self.outputs['False'].python_value.strip() else ''}
{self.indent(self.outputs['Continue'].python_value, 5)}
"""
@@ -0,0 +1,100 @@
import bpy
from ....utils import get_python_name
from ...base_node import SN_ScriptingBaseNode
class SN_RunInterfaceFunctionNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunInterfaceFunctionNode"
bl_label = "Function Run (Interface) (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
def on_ref_update(self, node, data=None):
if node.bl_idname == "SN_InterfaceFunctionNode" and data:
index = 0
for i, out in enumerate(node.outputs):
if out.bl_idname != "SN_InterfaceSocket":
index = i-1
break
# inputs has been added
if "added" in data:
socket_index = list(data["added"].node.outputs).index(data["added"])
self.add_input_from_socket(data["added"])
self.inputs.move(len(self.inputs)-1, socket_index-index)
# input has been removed
elif "removed" in data:
self.inputs.remove(self.inputs[data["removed"]])
# input has changed
elif "changed" in data:
self.convert_socket(self.inputs[data["changed"].index-index], data["changed"].bl_idname)
# input has updated
elif "updated" in data:
if data["updated"].index-index < len(self.inputs):
self.inputs[data["updated"].index-index].name = data["updated"].name
self._evaluate(bpy.context)
def update_function_reference(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
# remember connections
links = []
for inp in self.inputs[1:]:
links.append(None)
if inp.is_linked:
links[-1] = inp.from_socket()
# remove current data inputs
for i in range(len(self.inputs)-1, 0, -1):
self.inputs.remove(self.inputs[i])
# add new data inputs
if self.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
for out in parent_tree.nodes[self.ref_SN_InterfaceFunctionNode].outputs[1:-1]:
if out.bl_idname != "SN_InterfaceSocket":
self.add_input_from_socket(out)
# restore connections
if len(links) == len(self.inputs)-1:
for i, from_socket in enumerate(links):
if from_socket:
self.node_tree.links.new(from_socket, self.inputs[i+1])
self._evaluate(context)
ref_SN_InterfaceFunctionNode: bpy.props.StringProperty(name="Function",
description="The function to run",
update=update_function_reference)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
if self.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
# get input values
inp_values = []
for inp in self.inputs[1:]:
inp_values.append(inp.python_value)
inp_values = ", ".join(inp_values)
self.code = f"""
layout_function = {self.active_layout}
{parent_tree.nodes[self.ref_SN_InterfaceFunctionNode].func_name}(layout_function, {inp_values})
"""
else:
self.code = f""
def draw_node(self, context, layout):
row = layout.row(align=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_InterfaceFunctionNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_InterfaceFunctionNode"), "refs", text="")
@@ -0,0 +1,22 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LabelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LabelNode"
bl_label = "Label (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label").default_value = "My Label"
self.add_icon_input("Icon")
def evaluate(self, context):
self.code = f"{self.active_layout}.label(text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})"
def draw_node(self, context, layout):
pass
@@ -0,0 +1,42 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutBoxNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutBoxNode"
bl_label = "Box (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
@property
def layout_type(self):
return f"box_{self.static_uid}"
def on_create(self, context):
self.add_interface_input()
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(["Expand", "Left", "Center", "Right"])
self.add_interface_output().prev_dynamic = True
self.add_dynamic_interface_output()
def evaluate(self, context):
self.code = f"""
box_{self.static_uid} = {self.active_layout}.box()
box_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
box_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"box_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
box_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
box_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
box_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
box_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
box_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
{self.indent([out.python_value for out in self.outputs[:-1]], 5)}
"""
@@ -0,0 +1,43 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutColumnNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutColumnNode"
bl_label = "Column (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, _):
return f"col_{self.static_uid}"
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_boolean_input("Align")
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(["Expand", "Left", "Center", "Right"])
self.add_interface_output().prev_dynamic = True
self.add_dynamic_interface_output()
def evaluate(self, context):
self.code = f"""
col_{self.static_uid} = {self.active_layout}.column(heading={self.inputs["Label"].python_value}, align={self.inputs["Align"].python_value})
col_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
col_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"col_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
col_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
col_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
col_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
col_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
col_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
{self.indent([out.python_value for out in self.outputs[:-1]], 5)}
"""
@@ -0,0 +1,43 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutRowNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutRowNode"
bl_label = "Row (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, _):
return f"row_{self.static_uid}"
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_boolean_input("Align")
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(["Expand", "Left", "Center", "Right"])
self.add_interface_output().prev_dynamic = True
self.add_dynamic_interface_output()
def evaluate(self, context):
self.code = f"""
row_{self.static_uid} = {self.active_layout}.row(heading={self.inputs["Label"].python_value}, align={self.inputs["Align"].python_value})
row_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
row_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"row_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
row_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
row_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
row_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
row_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
row_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
{self.indent([out.python_value for out in self.outputs[:-1]], 5)}
"""
@@ -0,0 +1,46 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutSplitNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutSplitNode"
bl_label = "Split (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, _):
return f"split_{self.static_uid}"
def on_create(self, context):
self.add_interface_input()
inp = self.add_float_input("Factor")
inp.subtype = "FACTOR"
inp.default_value = 0.5
self.add_boolean_input("Align")
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(["Expand", "Left", "Center", "Right"])
self.add_interface_output()
self.add_interface_output()
def evaluate(self, context):
self.code = f"""
split_{self.static_uid} = {self.active_layout}.split(factor={self.inputs["Factor"].python_value}, align={self.inputs["Align"].python_value})
split_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
split_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"split_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
split_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
split_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
split_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
split_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
split_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
{self.indent(self.outputs[0].python_value, 5)}
{self.indent(self.outputs[1].python_value, 5)}
"""
@@ -0,0 +1,61 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ForInterfaceNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ForInterfaceNode"
bl_label = "Loop For (Interface) (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
passthrough_layout_type = True
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input("Collection")
self.add_interface_output("Repeat")
self.add_interface_output("Continue")
self.add_property_output("Item").changeable = True
self.add_integer_output("Index")
def update_type(self, context):
inp = self.convert_socket(self.inputs[1], self.socket_names[self.for_type])
inp.name = self.for_type
self.convert_socket(self.outputs["Item"], self.socket_names["Data"] if self.for_type == "List" else self.socket_names["Property"])
self._evaluate(context)
for_type: bpy.props.EnumProperty(name="Type",
description="Collection Type",
items=[("List", "List", "List"),
("Collection", "Collection", "Collection")],
default="Collection",
update=update_type)
reverse: bpy.props.BoolProperty(name="Reverse",
description="Reverse the order the loop runs through the items",
default=False,
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.inputs[1].is_linked:
self.outputs["Index"].python_value = f"i_{self.static_uid}"
self.outputs["Item"].python_value = f"{self.inputs[1].python_value}[i_{self.static_uid}]"
self.code = f"""
for i_{self.static_uid} in range({f"len({self.inputs[1].python_value})" if not self.reverse else f"len({self.inputs[1].python_value})-1,-1,-1"}):
{self.indent(self.outputs['Repeat'].python_value, 7) if self.outputs['Repeat'].python_value.strip() else 'pass'}
{self.indent(self.outputs['Continue'].python_value, 6)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Collection connected!', icon='ERROR')
{self.indent(self.outputs['Continue'].python_value, 6)}
"""
self.outputs["Index"].reset_value()
self.outputs["Item"].reset_value()
def draw_node(self, context, layout):
layout.prop(self, "for_type")
layout.prop(self, "reverse")
@@ -0,0 +1,28 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RepeatInterfaceNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RepeatInterfaceNode"
bl_label = "Loop Repeat (Interface) (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
passthrough_layout_type = True
def on_create(self, context):
self.add_interface_input()
self.add_integer_input("Repetitions").default_value = 2
self.add_interface_output("Repeat")
self.add_interface_output("Continue")
self.add_integer_output("Step")
def evaluate(self, context):
self.outputs["Step"].python_value = f"i_{self.static_uid}"
self.code = f"""
for i_{self.static_uid} in range({self.inputs['Repetitions'].python_value}):
{self.indent(self.outputs['Repeat'].python_value, 6) if self.outputs['Repeat'].python_value.strip() else 'pass'}
{self.indent(self.outputs['Continue'].python_value, 5)}
"""
@@ -0,0 +1,78 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_PopoverNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PopoverNode"
bl_label = "Popover (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_icon_input()
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
ref_SN_PanelNode: bpy.props.StringProperty(name="Panel",
description="The panel to display with this popover",
update=SN_ScriptingBaseNode._evaluate)
def on_ref_update(self, node, data=None):
if node.bl_idname == "SN_PanelNode":
self._evaluate(bpy.context)
def evaluate(self, context):
if self.parent_type == "CUSTOM":
if self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.ref_SN_PanelNode]
self.code = f"""
{self.active_layout}.popover('{node.last_idname}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
"""
else:
self.code = f"""
{self.active_layout}.popover('{self.panel_parent}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
if self.parent_type == "CUSTOM":
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
subrow.prop_search(self, "ref_SN_PanelNode", parent_tree.node_collection("SN_PanelNode"), "refs", text="", icon="VIEWZOOM")
else:
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
row.prop(self, "parent_type", text="", icon_only=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,20 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_SeparatorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SeparatorNode"
bl_label = "Separator (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_float_input("Factor").default_value = 1.0
def evaluate(self, context):
self.code = f"""
{self.active_layout}.separator(factor={self.inputs['Factor'].python_value})
"""
@@ -0,0 +1,69 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_SubmenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SubmenuNode"
bl_label = "Submenu (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_icon_input()
def on_ref_update(self, node, data=None):
if node.bl_idname in ["SN_PanelNode", "SN_MenuNode", "SN_PieMenuNode"]:
self._evaluate(bpy.context)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
default="CUSTOM",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
ref_SN_MenuNode: bpy.props.StringProperty(name="Custom Parent",
description="The panel used as a custom parent panel for this subpanel",
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
menu_parent: bpy.props.StringProperty(name="Menu",
default="VIEW3D_MT_add",
description="The menu that should be displayed",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.parent_type == "CUSTOM":
if self.ref_ntree and self.ref_SN_MenuNode in self.ref_ntree.nodes:
self.code = f"{self.active_layout}.menu('{self.ref_ntree.nodes[self.ref_SN_MenuNode].idname}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})"
else:
self.code = f"{self.active_layout}.menu('{self.menu_parent}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})"
def draw_node(self, context, layout):
row = layout.row(align=True)
if self.parent_type == "CUSTOM":
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_MenuNode", parent_tree.node_collection("SN_MenuNode"), "refs", text="")
else:
name = f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}"
op = row.operator("sn.activate_menu_picker", icon="EYEDROPPER", text=name)
op.node_tree = self.node_tree.name
op.node = self.name
row.prop(self, "parent_type", text="", icon_only=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,25 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_ProgressNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ProgressNode"
bl_label = "Progress Indicator"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label").default_value = "My Label"
self.add_float_input("Factor").default_value = 0.0
self.add_boolean_input("Ring").default_value = False
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
self.code = f"""
{self.active_layout}.progress(text={self.inputs['Label'].python_value}, factor={self.inputs['Factor'].python_value}, type='RING' if {self.inputs['Ring'].python_value} else 'BAR')
{self.indent(self.outputs[0].python_value, 3)}
"""
def draw_node(self, context, layout):
pass
@@ -0,0 +1,22 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_SeparatorNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SeparatorNodeNew"
bl_label = "Separator"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_float_input("Factor").default_value = 1.0
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
self.code = f"""
{self.active_layout}.separator(factor={self.inputs['Factor'].python_value})
{self.indent(self.outputs[0].python_value, 3)}
"""
@@ -0,0 +1,42 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CopyMenuNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CopyMenuNodeNew"
bl_label = "Copy Menu"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_interface_output().passthrough_layout_type = True
menu_parent: bpy.props.StringProperty(name="Menu",
default="VIEW3D_MT_add",
description="The menu that should be displayed",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code = f"""
if hasattr(bpy.types,"{self.menu_parent}"):
if not hasattr(bpy.types.{self.menu_parent}, "poll") or bpy.types.{self.menu_parent}.poll(context):
bpy.types.{self.menu_parent}.draw(self, context)
else:
{self.active_layout}.label(text="Can't display this menu here!", icon="ERROR")
else:
{self.active_layout}.label(text="Can't display this menu!", icon="ERROR")
{self.indent(self.outputs[0].python_value, 5)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
name = f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}"
op = row.operator("sn.activate_menu_picker", icon="EYEDROPPER", text=name)
op.node_tree = self.node_tree.name
op.node = self.name
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,42 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CopyPanelNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CopyPanelNodeNew"
bl_label = "Copy Panel"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_interface_output().passthrough_layout_type = True
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code = f"""
if hasattr(bpy.types,"{self.panel_parent}"):
if not hasattr(bpy.types.{self.panel_parent}, "poll") or bpy.types.{self.panel_parent}.poll(context):
bpy.types.{self.panel_parent}.draw(self, context)
else:
{self.active_layout}.label(text="Can't display this panel here!", icon="ERROR")
else:
{self.active_layout}.label(text="Can't display this panel!", icon="ERROR")
{self.indent(self.outputs[0].python_value, 5)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,27 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IfElseInterfaceNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IfElseInterfaceNodeNew"
bl_label = "If/Else (Interface)"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_boolean_input("Condition").default_value = True
self.add_interface_output("True").passthrough_layout_type = True
self.add_interface_output("False").passthrough_layout_type = True
self.add_interface_output("Interface").passthrough_layout_type = True
def evaluate(self, context):
self.code = f"""
if {self.inputs['Condition'].python_value}:
{self.indent(self.outputs['True'].python_value, 6) if self.outputs['True'].python_value.strip() else 'pass'}
{"else:" if self.outputs['False'].python_value.strip() else ""}
{self.indent(self.outputs['False'].python_value, 6) if self.outputs['False'].python_value.strip() else ''}
{self.indent(self.outputs['Interface'].python_value, 5)}
"""
@@ -0,0 +1,44 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutBoxNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutBoxNodeNew"
bl_label = "Box"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, socket):
if socket.name == "Box":
return f"box_{self.static_uid}"
return self.active_layout
def on_create(self, context):
self.add_interface_input()
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_boolean_input("Use Invoke")["default_value"] = True
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(["Expand", "Left", "Center", "Right"])
self.add_dynamic_interface_output("Box")
def evaluate(self, context):
self.code = f"""
box_{self.static_uid} = {self.active_layout}.box()
box_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
box_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"box_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
box_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
box_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
box_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
box_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
box_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
if not {self.inputs['Use Invoke'].python_value if 'Use Invoke' in self.inputs else 'False'}: box_{self.static_uid}.operator_context = "EXEC_DEFAULT"
{self.indent([out.python_value if out.name == 'Box' else '' for out in self.outputs], 5)}
"""
@@ -0,0 +1,46 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutColumnNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutColumnNodeNew"
bl_label = "Column"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, socket):
if socket.name == "Column":
return f"col_{self.static_uid}"
return self.active_layout
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_boolean_input("Align")
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_boolean_input("Use Invoke")["default_value"] = True
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(
["Expand", "Left", "Center", "Right"]
)
self.add_dynamic_interface_output("Column")
def evaluate(self, context):
self.code = f"""
col_{self.static_uid} = {self.active_layout}.column(heading={self.inputs["Label"].python_value}, align={self.inputs["Align"].python_value})
col_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
col_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"col_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
col_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
col_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
col_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
col_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
col_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
col_{self.static_uid}.operator_context = "INVOKE_DEFAULT" if {self.inputs['Use Invoke'].python_value if 'Use Invoke' in self.inputs else 'True'} else "EXEC_DEFAULT"
{self.indent([out.python_value if out.name == 'Column' else '' for out in self.outputs], 5)}
"""
@@ -0,0 +1,47 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutGridNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutGridNode"
bl_label = "Grid"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, socket):
if socket.name == "Grid":
return f"grid_{self.static_uid}"
return self.active_layout
def on_create(self, context):
self.add_interface_input()
self.add_integer_input("Columns").default_value = 6
self.add_boolean_input("Fill Row By Row").default_value = False
self.add_boolean_input("Even Columns").default_value = False
self.add_boolean_input("Even Rows").default_value = False
self.add_boolean_input("Align")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_boolean_input("Use Invoke")["default_value"] = True
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(["Expand", "Left", "Center", "Right"])
self.add_dynamic_interface_output("Grid")
def evaluate(self, context):
self.code = f"""
grid_{self.static_uid} = {self.active_layout}.grid_flow(columns={self.inputs["Columns"].python_value}, row_major={self.inputs["Fill Row By Row"].python_value}, even_columns={self.inputs["Even Columns"].python_value}, even_rows={self.inputs["Even Rows"].python_value}, align={self.inputs["Align"].python_value})
grid_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"grid_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
grid_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
grid_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
grid_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
grid_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
grid_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
if not {self.inputs['Use Invoke'].python_value if 'Use Invoke' in self.inputs else 'False'}: grid_{self.static_uid}.operator_context = "EXEC_DEFAULT"
{self.indent([out.python_value if out.name == 'Grid' else '' for out in self.outputs], 5)}
"""
@@ -0,0 +1,46 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutRowNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutRowNodeNew"
bl_label = "Row"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, socket):
if socket.name == "Row":
return f"row_{self.static_uid}"
return self.active_layout
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_boolean_input("Align")
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_boolean_input("Use Invoke")["default_value"] = True
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(
["Expand", "Left", "Center", "Right"]
)
self.add_dynamic_interface_output("Row")
def evaluate(self, context):
self.code = f"""
row_{self.static_uid} = {self.active_layout}.row(heading={self.inputs["Label"].python_value}, align={self.inputs["Align"].python_value})
row_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
row_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"row_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
row_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
row_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
row_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
row_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
row_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
row_{self.static_uid}.operator_context = "INVOKE_DEFAULT" if {self.inputs['Use Invoke'].python_value if 'Use Invoke' in self.inputs else 'True'} else "EXEC_DEFAULT"
{self.indent([out.python_value if out.name == 'Row' else '' for out in self.outputs], 5)}
"""
@@ -0,0 +1,50 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_LayoutSplitNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LayoutSplitNodeNew"
bl_label = "Split"
bl_width_default = 200
node_color = "INTERFACE"
def layout_type(self, socket):
if socket.name == "Split":
return f"split_{self.static_uid}"
return self.active_layout
def on_create(self, context):
self.add_interface_input()
inp = self.add_float_input("Factor")
inp.subtype = "FACTOR"
inp.default_value = 0.5
self.add_boolean_input("Align")
self.add_boolean_input("Alert")
self.add_boolean_input("Enabled")["default_value"] = True
self.add_boolean_input("Active")["default_value"] = True
self.add_boolean_input("Split Layout")
self.add_boolean_input("Decorate Layout")
self.add_boolean_input("Use Invoke")["default_value"] = True
self.add_float_input("Scale X")["default_value"] = 1
self.add_float_input("Scale Y")["default_value"] = 1
self.add_enum_input("Alignment")["items"] = str(["Expand", "Left", "Center", "Right"])
self.add_interface_output("Split")
self.add_interface_output("Split")
def evaluate(self, context):
self.code = f"""
split_{self.static_uid} = {self.active_layout}.split(factor={self.inputs["Factor"].python_value}, align={self.inputs["Align"].python_value})
split_{self.static_uid}.alert = {self.inputs["Alert"].python_value}
split_{self.static_uid}.enabled = {self.inputs["Enabled"].python_value}
{f"split_{self.static_uid}.active = {self.inputs['Active'].python_value}" if "Active" in self.inputs else ""}
split_{self.static_uid}.use_property_split = {self.inputs["Split Layout"].python_value}
split_{self.static_uid}.use_property_decorate = {self.inputs["Decorate Layout"].python_value}
split_{self.static_uid}.scale_x = {self.inputs["Scale X"].python_value}
split_{self.static_uid}.scale_y = {self.inputs["Scale Y"].python_value}
split_{self.static_uid}.alignment = {self.inputs["Alignment"].python_value}.upper()
if not {self.inputs['Use Invoke'].python_value if 'Use Invoke' in self.inputs else 'False'}: split_{self.static_uid}.operator_context = "EXEC_DEFAULT"
{self.indent(self.outputs[0].python_value, 5)}
{self.indent(self.outputs[1].python_value, 5)}
"""
@@ -0,0 +1,59 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ForInterfaceNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ForInterfaceNodeNew"
bl_label = "Loop For (Interface)"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input("Collection")
self.add_interface_output("Repeat").passthrough_layout_type = True
self.add_dynamic_interface_output().passthrough_layout_type = True
self.add_property_output("Item").changeable = True
self.add_integer_output("Index")
def update_type(self, context):
inp = self.convert_socket(self.inputs[1], self.socket_names[self.for_type])
inp.name = self.for_type
self.convert_socket(self.outputs["Item"], self.socket_names["Data"] if self.for_type == "List" else self.socket_names["Property"])
self._evaluate(context)
for_type: bpy.props.EnumProperty(name="Type",
description="Collection Type",
items=[("List", "List", "List"),
("Collection", "Collection", "Collection")],
default="Collection",
update=update_type)
reverse: bpy.props.BoolProperty(name="Reverse",
description="Reverse the order the loop runs through the items",
default=False,
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.inputs[1].is_linked:
self.outputs["Index"].python_value = f"i_{self.static_uid}"
self.outputs["Item"].python_value = f"{self.inputs[1].python_value}[i_{self.static_uid}]"
self.code = f"""
for i_{self.static_uid} in range({f"len({self.inputs[1].python_value})" if not self.reverse else f"len({self.inputs[1].python_value})-1,-1,-1"}):
{self.indent(self.outputs['Repeat'].python_value, 7) if self.outputs['Repeat'].python_value.strip() else 'pass'}
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 6)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Collection connected!', icon='ERROR')
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 6)}
"""
self.outputs["Index"].reset_value()
self.outputs["Item"].reset_value()
def draw_node(self, context, layout):
layout.prop(self, "for_type")
layout.prop(self, "reverse")
@@ -0,0 +1,26 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RepeatInterfaceNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RepeatInterfaceNodeNew"
bl_label = "Loop Repeat (Interface)"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_integer_input("Repetitions").default_value = 2
self.add_interface_output("Repeat").passthrough_layout_type = True
self.add_interface_output().passthrough_layout_type = True
self.add_integer_output("Step")
def evaluate(self, context):
self.outputs["Step"].python_value = f"i_{self.static_uid}"
self.code = f"""
for i_{self.static_uid} in range({self.inputs['Repetitions'].python_value}):
{self.indent(self.outputs['Repeat'].python_value, 6) if self.outputs['Repeat'].python_value.strip() else 'pass'}
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 5)}
"""
@@ -0,0 +1,91 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_PopoverNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PopoverNodeNew"
bl_label = "Popover"
bl_width_default = 240
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_icon_input()
self.add_interface_output().passthrough_layout_type = True
self.ref_ntree = self.node_tree
self.order = 1
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
ref_SN_PanelNode: bpy.props.StringProperty(name="Panel",
description="The panel to display with this popover",
update=SN_ScriptingBaseNode._evaluate)
def on_ref_update(self, node, data=None):
if node.bl_idname == "SN_PanelNode":
self._evaluate(bpy.context)
def evaluate(self, context):
if self.parent_type == "CUSTOM":
if self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.ref_SN_PanelNode]
self.code = f"""
{self.active_layout}.popover('{node.last_idname}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
{self.indent(self.outputs[0].python_value, 5)}
"""
else:
self.code = f"""
{self.active_layout}.popover('{self.panel_parent}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
if self.parent_type == "CUSTOM":
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
subrow.prop_search(self, "ref_SN_PanelNode", parent_tree.node_collection("SN_PanelNode"), "refs", text="", icon="VIEWZOOM")
row.prop(self, "parent_type", text="", icon_only=True)
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_PanelNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_PanelNode
else:
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
row.prop(self, "parent_type", text="", icon_only=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,86 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_SubmenuNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SubmenuNodeNew"
bl_label = "Submenu"
node_color = "INTERFACE"
bl_width_default = 240
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_icon_input()
self.add_interface_output().passthrough_layout_type = True
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
if node.bl_idname in ["SN_PanelNode", "SN_MenuNode", "SN_PieMenuNode"]:
self._evaluate(bpy.context)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
default="CUSTOM",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
ref_SN_MenuNode: bpy.props.StringProperty(name="Custom Parent",
description="The menu used for this submenu",
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Menu Node Tree",
description="The node tree to select the menu from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
menu_parent: bpy.props.StringProperty(name="Menu",
default="VIEW3D_MT_add",
description="The menu that should be displayed",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.parent_type == "CUSTOM":
if self.ref_ntree and self.ref_SN_MenuNode in self.ref_ntree.nodes:
self.code = f"""
{self.active_layout}.menu('{self.ref_ntree.nodes[self.ref_SN_MenuNode].idname}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
{self.indent(self.outputs[0].python_value, 5)}
"""
else:
self.code = f"""
{self.active_layout}.menu('{self.menu_parent}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
if self.parent_type == "CUSTOM":
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_MenuNode", parent_tree.node_collection("SN_MenuNode"), "refs", text="")
row.prop(self, "parent_type", text="", icon_only=True)
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_MenuNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_MenuNode
else:
name = f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}"
op = row.operator("sn.activate_menu_picker", icon="EYEDROPPER", text=name)
op.node_tree = self.node_tree.name
op.node = self.name
row.prop(self, "parent_type", text="", icon_only=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,31 @@
import bpy
class SN_OT_DummyButtonOperator(bpy.types.Operator):
bl_idname = "sn.dummy_button_operator"
bl_label = "Dummy Button"
bl_description = "This button has no operator associated"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
self.report({"INFO"}, message="No operator associated with this button!")
return {"FINISHED"}
class SN_OT_PasteOperator(bpy.types.Operator):
bl_idname = "sn.paste_operator"
bl_label = "Paste Operator"
bl_description = "Paste a copied operator into this button"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
if "bpy.ops." in context.window_manager.clipboard:
bpy.data.node_groups[self.node_tree].nodes[self.node].pasted_operator = context.window_manager.clipboard
else:
self.report({"ERROR"}, message="Not a valid blender operator. Use the Rightclick Menu -> Get Serpens Operator button")
return {"FINISHED"}
@@ -0,0 +1,85 @@
import bpy
registered_menus = []
def register_dummy_interface(ntree, node, menu):
func_name = f"sna_dummy_{menu.bl_rna.identifier}"
label = getattr(menu, "bl_label", menu.bl_rna.identifier.replace('_PT_', ' ').replace('_', ' ').title())
menu = f"""
def {func_name}(self, context):
layout = self.layout
row = layout.row()
row.alert = True
op = row.operator("sn.pick_menu_location", text="Select '{label}'", icon="EYEDROPPER")
op.node_tree = "{ntree}"
op.node = "{node}"
op.menu = "{menu.bl_rna.identifier}"
bpy.types.{menu.bl_rna.identifier}.append({func_name})
registered_menus.append([bpy.types.{menu.bl_rna.identifier}, {func_name}])
"""
try: exec(menu, globals())
except: pass
class SN_OT_ActivateMenuPicker(bpy.types.Operator):
bl_idname = "sn.activate_menu_picker"
bl_label = "Select Menu Location"
bl_description = "Select the location of this menu in the Interface"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
@classmethod
def poll(cls, context):
return not context.scene.sn.picker_active
def execute(self, context):
# register menus
for menu in bpy.types.Menu.__subclasses__() + bpy.types.Header.__subclasses__():
register_dummy_interface(self.node_tree, self.node,menu)
# redraw screen
for area in context.screen.areas:
area.tag_redraw()
# set picker active
context.scene.sn.picker_active = True
return {"FINISHED"}
class SN_OT_PickMenuLocation(bpy.types.Operator):
bl_idname = "sn.pick_menu_location"
bl_label = "Pick Location"
bl_description = "Pick this location to put the menu into"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
menu: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
# find node and set properties
if self.node_tree in bpy.data.node_groups:
ntree = bpy.data.node_groups[self.node_tree]
if self.node in ntree.nodes:
node = ntree.nodes[self.node]
node.menu_parent = self.menu
# unregister menus
for menu in registered_menus:
try: menu[0].remove(menu[1])
except: pass
# reset saves and picker
registered_menus.clear()
context.scene.sn.picker_active = False
return {"FINISHED"}
@@ -0,0 +1,123 @@
import bpy
registered_panels = []
def register_dummy_panel(ntree, node, space, region):
idname = f"SNA_PT_Dummy{space}{region}"
panel = f"""
class {idname}(bpy.types.Panel):
bl_label = "test"
bl_space_type = '{space}'
bl_region_type = '{region}'
bl_category = 'SERPENS'
bl_options = {{"HIDE_HEADER"}}
bl_order = 0
def draw(self, context):
layout = self.layout
row = layout.row()
row.scale_y = 1.5
row.alert = True
op = row.operator("sn.pick_panel_location", text="Select {space.replace("_", " ").title()} {region.replace("_", " ").title()}")
op.node_tree = "{ntree}"
op.node = "{node}"
op.space = "{space}"
op.region = "{region}"
op.context = ""
if hasattr(context.space_data, "context"):
row = layout.row()
row.scale_y = 1.5
row.alert = True
op = row.operator("sn.pick_panel_location", text="Select {space.replace("_", " ").title()} {region.replace("_", " ").title()} "+context.space_data.context.replace("_", " ").title())
op.node_tree = "{ntree}"
op.node = "{node}"
op.space = "{space}"
op.region = "{region}"
op.context = context.space_data.context.lower()
bpy.utils.register_class({idname})
registered_panels.append({idname})
"""
#register panel
try: exec(panel, globals())
except: pass
class SN_OT_ActivatePanelPicker(bpy.types.Operator):
bl_idname = "sn.activate_panel_picker"
bl_label = "Select Panel Location"
bl_description = "Select the location of this panel in the Interface"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
@classmethod
def poll(cls, context):
return not context.scene.sn.picker_active
def execute(self, context):
# possible panel locations
space_types = ["EMPTY", "VIEW_3D", "IMAGE_EDITOR", "NODE_EDITOR",
"SEQUENCE_EDITOR", "CLIP_EDITOR", "DOPESHEET_EDITOR",
"GRAPH_EDITOR", "NLA_EDITOR", "TEXT_EDITOR", "CONSOLE",
"INFO", "TOPBAR", "STATUSBAR", "OUTLINER", "PROPERTIES",
"FILE_BROWSER", "SPREADSHEET", "PREFERENCES"]
region_types = ["WINDOW", "HEADER", "CHANNELS", "TEMPORARY", "UI", "TOOLS",
"TOOL_PROPS", "PREVIEW", "HUD", "NAVIGATION_BAR", "EXECUTE",
"FOOTER", "TOOL_HEADER", "XR"]
# register panels
registered_panels.clear()
for space in space_types:
for region in region_types:
register_dummy_panel(self.node_tree, self.node, space, region)
# redraw screen
for area in context.screen.areas:
area.tag_redraw()
# set picker active
context.scene.sn.picker_active = True
return {"FINISHED"}
class SN_OT_PickPanelLocation(bpy.types.Operator):
bl_idname = "sn.pick_panel_location"
bl_label = "Pick Location"
bl_description = "Pick this location to put the panel into"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
space: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
region: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
context: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
# find node and set properties
if self.node_tree in bpy.data.node_groups:
ntree = bpy.data.node_groups[self.node_tree]
if self.node in ntree.nodes:
node = ntree.nodes[self.node]
node.space = self.space
node.region = self.region
node.context = self.context
# unregister panels
for panel in registered_panels:
try: bpy.utils.unregister_class(panel)
except: pass
# reset saves and picker
registered_panels.clear()
context.scene.sn.picker_active = False
return {"FINISHED"}
@@ -0,0 +1,103 @@
import bpy
registered_subpanels = []
def register_dummy_subpanel(ntree, node, panel):
func_name = f"sna_dummy_{panel.bl_rna.identifier}"
label = getattr(panel, "bl_label", panel.bl_rna.identifier.replace('_PT_', ' ').replace('_', ' ').title())
panel = f"""
def {func_name}(self, context):
layout = self.layout
row = layout.row()
row.alert = True
# if "SNA_PT_" in "{panel.bl_rna.identifier}":
# row.label(text="Select this custom panel on the subpanel node", icon="ERROR")
# else:
row.scale_y = 1.5
op = row.operator("sn.pick_subpanel_location", text="Select {label}")
op.node_tree = "{ntree}"
op.node = "{node}"
op.panel = "{panel.bl_rna.identifier}"
bpy.types.{panel.bl_rna.identifier}.append({func_name})
registered_subpanels.append([bpy.types.{panel.bl_rna.identifier}, {func_name}])
"""
#
#register panel
try: exec(panel, globals())
except: pass
class SN_OT_ActivateSubpanelPicker(bpy.types.Operator):
bl_idname = "sn.activate_subpanel_picker"
bl_label = "Select Subpanel Location"
bl_description = "Select the location of this subpanel in the Interface"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
allow_subpanels: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE", "HIDDEN"})
@classmethod
def poll(cls, context):
return not context.scene.sn.picker_active
def execute(self, context):
# register panels
for panel in bpy.types.Panel.__subclasses__():
if not getattr(panel, "bl_parent_id", False) or self.allow_subpanels:
register_dummy_subpanel(self.node_tree, self.node, panel)
# redraw screen
for area in context.screen.areas:
area.tag_redraw()
# set picker active
context.scene.sn.picker_active = True
return {"FINISHED"}
class SN_OT_PickSubpanelLocation(bpy.types.Operator):
bl_idname = "sn.pick_subpanel_location"
bl_label = "Pick Location"
bl_description = "Pick this location to put the subpanel into"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
panel: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
# find node and set properties
if self.node_tree in bpy.data.node_groups:
ntree = bpy.data.node_groups[self.node_tree]
if self.node in ntree.nodes:
node = ntree.nodes[self.node]
node.panel_parent = self.panel
panel = getattr(bpy.types, self.panel)
if hasattr(panel, "bl_space_type") and hasattr(node, "space"):
node.space = panel.bl_space_type
if hasattr(panel, "bl_region_type") and hasattr(node, "region"):
node.region = panel.bl_region_type
if hasattr(panel, "bl_context") and hasattr(node, "context"):
node.context = panel.bl_context
else:
node.context = ""
# unregister panels
for panel in registered_subpanels:
try: panel[0].remove(panel[1])
except: pass
# reset saves and picker
registered_subpanels.clear()
context.scene.sn.picker_active = False
return {"FINISHED"}
@@ -0,0 +1,25 @@
import bpy
class SN_OT_OpenPreferences(bpy.types.Operator):
bl_idname = "sn.open_preferences"
bl_label = "Open Preferences"
bl_description = "Open Preferences"
bl_options = {"REGISTER", "UNDO"}
navigation: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
has_prefs = False
for area in context.screen.areas:
if area.type == "PREFERENCES":
has_prefs = True
break
if not has_prefs:
bpy.ops.screen.userpref_show("INVOKE_DEFAULT")
context.preferences.active_section = "ADDONS"
context.window_manager.addon_search = "Serpens"
context.preferences.addons[__name__.partition('.')[ 0]].preferences.navigation = self.navigation
bpy.ops.preferences.addon_expand(module="blender_visual_scripting_addon")
return {"FINISHED"}