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,118 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...settings.data_properties import bpy_to_indexed_sections, bpy_to_path_sections
def segment_is_indexable(segment):
""" Returns if a segment can be indexed. A segment is a string part of a data path """
return "[" in segment and "]" in segment
def data_path_from_inputs(inputs, data):
""" Returns the data path based on the given inputs and data """
data_path = ""
inp_index = 0
for segment in data:
if data_path and not segment[0] == "[":
data_path += f".{segment.split('[')[0]}"
else:
data_path += f"{segment.split('[')[0]}"
if segment_is_indexable(segment):
if inputs[inp_index].bl_label == "Property":
data_path = inputs[inp_index].python_value
else:
data_path += f"[{inputs[inp_index].python_value}]"
inp_index += 1
return data_path
class SN_BlenderPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BlenderPropertyNode"
bl_label = "Blender Property"
node_color = "PROPERTY"
def _is_valid_data_path(self, path):
return path and "bpy." in path and not ".ops." in path
def get_data(self):
if self.pasted_data_path:
if self._is_valid_data_path(self.pasted_data_path):
return bpy_to_indexed_sections(self.pasted_data_path)
return None
def create_inputs_from_path(self):
""" Creates the inputs for the given data path """
self.inputs.clear()
data = self.get_data()
if data:
for segment in data:
if segment_is_indexable(segment):
name = segment.split("[")[0].replace("_", " ").title()
if '"' in segment or "'" in segment:
inp = self.add_string_input(name)
inp["default_value"] = segment.split("[")[-1].split("]")[0][1:-1]
inp.index_type = "String"
else:
inp = self.add_integer_input(name)
inp["default_value"] = int(segment.split("[")[-1].split("]")[0])
inp.index_type = "Integer"
inp.indexable = True
def get_pasted_prop_name(self, offset=-1):
data = self.get_data()
if data:
if data[offset][0] == "[":
return data[offset]
return data[offset].replace("_", " ").title()
return "Property"
def on_prop_change(self, context):
name = self.get_pasted_prop_name()
source = self.get_pasted_prop_name(-2).split("[")[0]
self.label = f"{name} ({source})"
self.outputs[0].name = name
self.outputs[0].set_hide(self.pasted_data_path == "")
self.outputs[1].set_hide(self.pasted_data_path == "")
self.create_inputs_from_path()
self._evaluate(context)
pasted_data_path: bpy.props.StringProperty(name="Pasted Path",
description="The full data path to the property",
update=on_prop_change)
def on_create(self, context):
self.add_property_output("Property").set_hide(True)
out = self.add_data_output("Value")
out.changeable = True
out.set_hide(True)
def evaluate(self, context):
data = self.get_data()
if not data:
self.outputs[0].reset_value()
else:
data_path = data_path_from_inputs(self.inputs, data)
self.outputs[0].python_value = data_path
self.outputs[1].python_value = data_path
def draw_node(self, context, layout):
row = layout.row(align=True)
row.scale_y = 1.2
op = row.operator("sn.paste_data_path", text=self.get_pasted_prop_name() if self.pasted_data_path else "Paste Property", icon="PASTEDOWN")
op.node = self.name
op.node_tree = self.node_tree.name
if self.pasted_data_path:
op = row.operator("sn.reset_data_path", text="", icon="LOOP_BACK")
op.node = self.name
op.node_tree = self.node_tree.name
def draw_node_panel(self, context, layout):
layout.prop(self, "pasted_data_path", text="Data Path")
@@ -0,0 +1,28 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddCollectionItemNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddCollectionItemNode"
bl_label = "Add Collection Item"
node_color = "PROPERTY"
def on_create(self, context):
self.add_execute_input()
self.add_collection_property_input(label="Serpens Collection Property")
self.add_execute_output()
self.add_property_output("Item")
def evaluate(self, context):
if self.inputs[1].is_linked:
self.code = f"""
item_{self.static_uid} = {self.inputs[1].python_value}.add()
{self.indent(self.outputs[0].python_value, 6)}
"""
self.outputs["Item"].python_value = f"item_{self.static_uid}"
else:
self.outputs["Item"].reset_value()
@@ -0,0 +1,17 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CollectionLengthNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CollectionLengthNode"
bl_label = "Collection Length"
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_input()
self.add_integer_output("Length")
def evaluate(self, context):
self.outputs[0].python_value = f"len({self.inputs[0].python_value})"
@@ -0,0 +1,32 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IndexCollectionPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IndexCollectionPropertyNode"
bl_label = "Index Collection Property"
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_input()
self.add_integer_input("Index")
self.add_property_output()
def update_index_type(self, context):
inp = self.convert_socket(self.inputs[1], self.socket_names[self.index_type])
inp.name = "Index" if self.index_type == "Integer" else "Name"
self._evaluate(context)
index_type: bpy.props.EnumProperty(name="Index Type",
description="The type of index to use",
items=[("Integer", "Index", "Starts at 0. Negative indices go to the back of the list."),
("String", "Name", "Refers to the name property of the element.")],
update=update_index_type)
def evaluate(self, context):
self.outputs[0].python_value = f"{self.inputs[0].python_value}[{self.inputs[1].python_value}]"
def draw_node(self, context, layout):
layout.prop(self, "index_type", expand=True)
@@ -0,0 +1,51 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IsIndexInCollectionPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IsIndexInCollectionPropertyNode"
bl_label = "Is Index In Collection"
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_input()
self.add_integer_input("Index")
self.add_boolean_output("In Collection")
self.add_integer_output("Index")
def update_index_type(self, context):
inp = self.convert_socket(self.inputs[1], self.socket_names[self.index_type])
inp.name = "Index" if self.index_type == "Integer" else "Name"
self._evaluate(context)
index_type: bpy.props.EnumProperty(name="Index Type",
description="The type of index to use",
items=[("Integer", "Index", "Starts at 0. Negative indices go to the back of the list."),
("String", "Name", "Refers to the name property of the element.")],
update=update_index_type)
def evaluate(self, context):
self.code_imperative = f"""
def property_exists(prop_path, glob, loc):
try:
eval(prop_path, glob, loc)
return True
except:
return False
"""
if self.inputs[0].is_linked:
if self.index_type == "Integer":
self.outputs["In Collection"].python_value = f"""(property_exists("{self.inputs[0].python_value}", globals(), locals()) and len({self.inputs[0].python_value}) > {self.inputs[1].python_value})"""
self.outputs["Index"].python_value = self.inputs[1].python_value
else:
self.outputs["In Collection"].python_value = f"""(property_exists("{self.inputs[0].python_value}", globals(), locals()) and {self.inputs[1].python_value} in {self.inputs[0].python_value})"""
self.outputs["Index"].python_value = f"{self.inputs[0].python_value}.find({self.inputs[1].python_value})"
else:
self.outputs[0].reset_value()
self.outputs[1].reset_value()
def draw_node(self, context, layout):
layout.prop(self, "index_type", expand=True)
@@ -0,0 +1,31 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_MoveCollectionItemNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_MoveCollectionItemNode"
bl_label = "Move Collection Item"
node_color = "PROPERTY"
def on_create(self, context):
self.add_execute_input()
self.add_collection_property_input(label="Serpens Collection Property")
self.add_integer_input("Move From")
self.add_integer_input("Move To")
self.add_execute_output()
self.add_property_output("Item")
def evaluate(self, context):
if self.inputs[1].is_linked:
self.code = f"""
{self.inputs[1].python_value}.move({self.inputs['Move From'].python_value}, {self.inputs['Move To'].python_value})
item_{self.static_uid} = {self.inputs[1].python_value}[{self.inputs['Move To'].python_value}]
{self.indent(self.outputs[0].python_value, 6)}
"""
self.outputs["Item"].python_value = f"item_{self.static_uid}"
else:
self.outputs["Item"].reset_value()
@@ -0,0 +1,62 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RemoveCollectionItemNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RemoveCollectionItemNode"
bl_label = "Remove Collection Item"
node_color = "PROPERTY"
def on_create(self, context):
self.add_execute_input()
self.add_collection_property_input(label="Serpens Collection Property")
self.add_integer_input("Index")
self.add_execute_output()
def update_type(self, context):
if self.remove_type == "Index":
inp = self.convert_socket(self.inputs[2], self.socket_names["Integer"])
inp.name = "Index"
elif self.remove_type == "Item":
inp = self.convert_socket(self.inputs[2], self.socket_names["Property"])
inp.name = "Item"
self.inputs[2].set_hide(self.remove_type == "All")
self._evaluate(context)
remove_type: bpy.props.EnumProperty(name="Type",
description="Remove by this type",
items=[("Index", "Index", "Index"),
("Item", "Item", "Item"),
("All", "All", "All")],
update=update_type)
def evaluate(self, context):
if self.inputs[1].is_linked:
if self.remove_type == "Index":
self.code = f"""
if len({self.inputs[1].python_value}) > {self.inputs[2].python_value}:
{self.inputs[1].python_value}.remove({self.inputs[2].python_value})
{self.indent(self.outputs[0].python_value, 7)}
"""
elif self.remove_type == "Item":
self.code = f"""
for i_{self.static_uid} in range(len({self.inputs[1].python_value})):
if {self.inputs[1].python_value}[i_{self.static_uid}] == {self.inputs[2].python_value}:
{self.inputs[1].python_value}.remove(i_{self.static_uid})
break
{self.indent(self.outputs[0].python_value, 7)}
"""
elif self.remove_type == "All":
self.code = f"""
{self.inputs[1].python_value}.clear()
{self.indent(self.outputs[0].python_value, 7)}
"""
def draw_node(self, context, layout):
layout.prop(self, "remove_type", expand=True)
@@ -0,0 +1,23 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_GetCustomPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_GetCustomPropertyNode"
bl_label = "Get Custom Property"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_input("Blend Data")
self.add_string_input("Property")
self.add_data_output("Property Value")
def evaluate(self, context):
if self.inputs[0].is_linked:
self.outputs[0].python_value = f"{self.inputs[0].python_value}[{self.inputs[1].python_value}]"
else:
self.outputs[0].python_value = f""
@@ -0,0 +1,23 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_HasCustomPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_HasCustomPropertyNode"
bl_label = "Has Custom Property"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_input("Blend Data")
self.add_string_input("Property")
self.add_boolean_output("Property Exists")
def evaluate(self, context):
if self.inputs[0].is_linked:
self.outputs[0].python_value = f"{self.inputs[1].python_value} in {self.inputs[0].python_value}"
else:
self.outputs[0].python_value = f"False"
@@ -0,0 +1,30 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_SetCustomPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SetCustomPropertyNode"
bl_label = "Set Custom Property"
node_color = "PROPERTY"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_property_input("Blend Data")
self.add_string_input("Property")
self.add_data_input("Property Value")
def evaluate(self, context):
if self.inputs[1].is_linked:
self.code = f"""
{self.inputs[1].python_value}[{self.inputs[2].python_value}] = {self.inputs[3].python_value}
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
{self.indent(self.outputs[0].python_value, 6)}
"""
@@ -0,0 +1,64 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ...templates.PropertyReferenceNode import PropertyReferenceNode
class SN_GenerateEnumItemsNode(SN_ScriptingBaseNode, bpy.types.Node, PropertyReferenceNode):
bl_idname = "SN_GenerateEnumItemsNode"
bl_label = "Generate Dynamic Enum Items"
node_color = "PROGRAM"
bl_width_default = 240
is_trigger = True
def on_create(self, context):
self.add_list_input("Items")
def evaluate(self, context):
enum_src = self.get_prop_source()
if enum_src and self.prop_name in enum_src.properties and enum_src.properties[self.prop_name].property_type == "Enum":
prop = enum_src.properties[self.prop_name]
self.code_imperative = f"""
_item_map = dict()
def make_enum_item(_id, name, descr, preview_id, uid):
lookup = str(_id)+"\\0"+str(name)+"\\0"+str(descr)+"\\0"+str(preview_id)+"\\0"+str(uid)
if not lookup in _item_map:
_item_map[lookup] = (_id, name, descr, preview_id, uid)
return _item_map[lookup]
"""
list_code = "[]"
from_out = self.inputs[0].from_socket()
if from_out and from_out.node.bl_idname == "SN_MakeEnumItemNode":
list_code = f"[{self.inputs[0].python_value}]"
else:
list_code = self.inputs[0].python_value
self.code = f"""
def {prop.settings.item_func_name}(self, context):
enum_items = {list_code}
return [make_enum_item(item[0], item[1], item[2], item[3], {'2**i' if prop.settings.enum_flag else 'i'}) for i, item in enumerate(enum_items)]
"""
def draw_node(self, context, layout):
self.draw_reference_selection(layout, True)
prop_src = self.get_prop_source()
if self.prop_name and prop_src and self.prop_name in prop_src.properties:
if prop_src.properties[self.prop_name].property_type != "Enum":
self.draw_warning(layout, "The selected property is not an enum property!")
elif not prop_src.properties[self.prop_name].settings.is_dynamic:
self.draw_warning(layout, "The selected property does not have dynamic items!")
# add item button
op = layout.operator("node.add_node", text="New Enum Item", icon="ADD")
op.type = "SN_MakeEnumItemNode"
op.use_transform = True
# list node info
from_out = self.inputs[0].from_socket()
if from_out and from_out.node.bl_idname == "SN_MakeEnumItemNode":
layout.label(text="Use a list node to combine multiple enum items!", icon="INFO")
@@ -0,0 +1,19 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_MakeEnumItemNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_MakeEnumItemNode"
bl_label = "Make Enum Item"
bl_width_default = 200
def on_create(self, context):
self.add_string_input("Name")
self.add_string_input("Description")
self.add_icon_input("Icon")
self.add_list_output("Item")
def evaluate(self, context):
self.outputs[0].python_value = f"[{self.inputs['Name'].python_value}, {self.inputs['Name'].python_value}, {self.inputs['Description'].python_value}, {self.inputs['Icon'].python_value}]"
@@ -0,0 +1,72 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ..templates.PropertyReferenceNode import PropertyReferenceNode
class SN_OnPropertyUpdateNode(SN_ScriptingBaseNode, bpy.types.Node, PropertyReferenceNode):
bl_idname = "SN_OnPropertyUpdateNode"
bl_label = "On Property Update"
node_color = "PROGRAM"
bl_width_default = 240
is_trigger = True
def on_create(self, context):
self.add_execute_output()
self.add_data_output("Value")
self.add_property_output("Attached To Item")
self.ref_ntree = self.node_tree
def update_func_name(self, prop):
return f"sna_update_{prop.python_name}_{self.static_uid}"
def update_nodes_with_props(self):
# update all nodes with properties when this node changes
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if hasattr(node, "properties"):
node._evaluate(bpy.context)
def on_ref_prop_change(self, context):
self.update_nodes_with_props()
def on_free(self):
self.update_nodes_with_props()
def on_copy(self, old):
self.update_nodes_with_props()
def evaluate(self, context):
prop_src = self.get_prop_source()
if prop_src and self.prop_name in prop_src.properties and not prop_src.properties[self.prop_name].property_type in ["Group", "Collection"]:
prop = prop_src.properties[self.prop_name]
self.outputs["Attached To Item"].python_value = f"self"
self.outputs["Value"].python_value = "sna_updated_prop"
self.code_imperative = f"""
def {self.update_func_name(prop)}(self, context):
sna_updated_prop = self.{prop.python_name}
{self.indent(self.outputs[0].python_value, 5)}
"""
else:
self.outputs["Attached To Item"].reset_value()
self.outputs["Value"].reset_value()
def draw_node(self, context, layout):
self.draw_reference_selection(layout)
prop_src = self.get_prop_source()
if self.prop_name and prop_src and self.prop_name in prop_src.properties:
prop = prop_src.properties[self.prop_name]
if prop.property_type in ["Group", "Collection"]:
self.draw_warning(layout, "Can't update group properties!")
else:
self.draw_warning(layout, "No Property selected!")
@@ -0,0 +1,27 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_PropertyExistsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PropertyExistsNode"
bl_label = "Property Exists"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_input()
self.add_boolean_output("Property Exists")
def evaluate(self, context):
self.code_imperative = f"""
def property_exists(prop_path, glob, loc):
try:
eval(prop_path, glob, loc)
return True
except:
return False
"""
self.outputs[0].python_value = f"""property_exists("{self.inputs[0].python_value}", globals(), locals())"""
@@ -0,0 +1,181 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from .Blender_Property import segment_is_indexable, data_path_from_inputs
from ...settings.data_properties import bpy_to_indexed_sections, bpy_to_path_sections
class SN_RunPropertyFunctionNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunPropertyFunctionNode"
bl_label = "Run Property Function"
node_color = "PROPERTY"
def _disect_data_path(self, path):
# remove assign part
path = "(".join(path.split("(")[:-1])
path = path.strip()
# replace escaped quotes
path = path.replace('\\"', '"')
# split data path in segments
segments = bpy_to_indexed_sections(path)
return segments
def _is_valid_data_path(self, path):
return path and "bpy." in path and not ".ops." in path
def get_data(self):
if self.pasted_data_path:
if self._is_valid_data_path(self.pasted_data_path):
# return bpy_to_indexed_sections(self.pasted_data_path)
return self._disect_data_path(self.pasted_data_path)
return None
def add_socket_from_param(self, param, callback):
""" Adds a socket from the given parameter with the given add callback """
sn = bpy.context.scene.sn
socket_type = param.split(": ")[-1].split("[")[0]
param_name = param.split(": ")[0]
socket_name = param_name.replace("_", " ").title()
if socket_name[-1] == "*":
socket_name = socket_name[:-1]
socket = callback(self.socket_names[socket_type], socket_name)
socket.can_be_disabled = True
socket.disabled = True
if sn.last_copied_datapath == self.pasted_data_path and param_name in sn.last_copied_required.split(";"):
socket.disabled = False
if socket_type == "Enum" or socket_type == "Enum Set":
socket.items = f"[{param.split(': ')[-1].split('[')[-1]}"
def create_inputs_from_path(self):
""" Creates the inputs for the given data path """
# remove existing inputs
for _ in range(len(self.inputs)-1):
self.inputs.remove(self.inputs[1])
# create blend data path inputs
data = self.get_data()
if data:
for segment in data:
if segment_is_indexable(segment):
name = segment.split("[")[0].replace("_", " ").title()
if '"' in segment or "'" in segment:
inp = self.add_string_input(name)
inp["default_value"] = segment.split(
"[")[-1].split("]")[0][1:-1]
inp.index_type = "String"
else:
inp = self.add_integer_input(name)
inp["default_value"] = int(
segment.split("[")[-1].split("]")[0])
inp.index_type = "Integer"
inp.indexable = True
# create parameter inputs
params = self.pasted_data_path.split(
"(")[-1].split(")")[0].split(", ")
for param in params:
if param.strip():
self.add_socket_from_param(param, self._add_input)
def create_outputs_from_path(self):
# remove existing outputs
for _ in range(len(self.outputs)-1):
self.outputs.remove(self.outputs[1])
# add new outputs
if " = " in self.pasted_data_path:
params = self.pasted_data_path.split(" = ")[-1].split(", ")
for param in params:
self.add_socket_from_param(param, self._add_output)
def get_pasted_prop_name(self, offset=-1):
if self.pasted_data_path:
return self.pasted_data_path.split(".")[offset].split("(")[0].replace("_", " ").title()
return "Property Function"
def on_prop_change(self, context):
self.disable_evaluation = True
name = self.get_pasted_prop_name()
source = self.get_pasted_prop_name(-2).split("[")[0]
self.label = f"{name} ({source})"
self.create_inputs_from_path()
self.create_outputs_from_path()
self.disable_evaluation = False
self._evaluate(context)
pasted_data_path: bpy.props.StringProperty(name="Pasted Path",
description="The full data path to the property",
update=on_prop_change)
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
def update_require_execute(self, context):
self.inputs[0].set_hide(not self.require_execute)
self.outputs[0].set_hide(not self.require_execute)
self._evaluate(context)
require_execute: bpy.props.BoolProperty(name="Require Execute", default=True,
description="Removes the execute inputs and only gives you the return value",
update=update_require_execute)
def evaluate(self, context):
data = self.get_data()
if data:
inps = list(filter(lambda inp: inp.indexable, list(self.inputs)))
function = data_path_from_inputs(inps, data) + "("
# add function parameters
inp_params = list(filter(lambda inp: inp.strip(), self.pasted_data_path.split(
"(")[-1].split(")")[0].split(", ")))
for i, param in enumerate(inp_params):
param = param.split(": ")[0]
param_inp = self.inputs[len(self.inputs) - len(inp_params)+i]
if not param_inp.disabled:
if param[-1] == "*":
function += f"{param_inp.python_value}, "
else:
function += f"{param.split(': ')[0]}={param_inp.python_value}, "
function += ")"
# add output parameters
out_params = list(filter(lambda inp: inp.strip(
), self.pasted_data_path.split(" = ")[-1].split(", ")))
if self.require_execute:
results = ""
if " = " in self.pasted_data_path:
for i, param in enumerate(out_params):
name = param.split(": ")[0] + f"_{self.static_uid}"
results += f"{name}, "
self.outputs[i+1].python_value = name
if results:
results = results[:-2] + " = "
# code
self.code = f"""
{results}{function}
{self.indent(self.outputs[0].python_value, 7)}
"""
# no execute
else:
if len(self.outputs) == 2:
self.outputs[1].python_value = function
elif len(self.outputs) > 2:
for i, out in enumerate(self.outputs):
if not i == 0:
out.python_value = f"{function}[{i-1}]"
def draw_node(self, context, layout):
row = layout.row(align=True)
row.scale_y = 1.2
op = row.operator("sn.paste_data_path", text=self.get_pasted_prop_name(
) if self.pasted_data_path else "Paste Function", icon="PASTEDOWN")
op.node = self.name
op.node_tree = self.node_tree.name
if self.pasted_data_path:
op = row.operator("sn.reset_data_path", text="", icon="LOOP_BACK")
op.node = self.name
op.node_tree = self.node_tree.name
if len(self.outputs) > 1:
layout.prop(self, "require_execute")
def draw_node_panel(self, context, layout):
layout.prop(self, "pasted_data_path", text="Function Path")
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ..templates.PropertyReferenceNode import PropertyReferenceNode
class SN_SerpensPropertyNode(SN_ScriptingBaseNode, bpy.types.Node, PropertyReferenceNode):
bl_idname = "SN_SerpensPropertyNode"
bl_label = "Serpens Property"
bl_width_default = 200
node_color = "PROPERTY"
add_indexing_inputs = True
def on_create(self, context):
self.add_property_output("Property")
self.add_data_output("Value")
self.add_property_input("Data")
self.ref_ntree = self.node_tree
def evaluate(self, context):
prop_src = self.get_prop_source()
# valid property selected
if prop_src and self.prop_name in prop_src.properties and not prop_src.properties[self.prop_name].property_type == "Group":
prop = prop_src.properties[self.prop_name]
self.outputs["Property"].python_value = f"{self.inputs[0].python_value}.{prop.python_name}"
self.outputs["Value"].python_value = f"{self.inputs[0].python_value}.{prop.python_name}"
# no valid property selected
else:
self.outputs["Property"].reset_value()
self.outputs["Value"].reset_value()
def draw_node(self, context, layout):
self.draw_reference_selection(layout)
prop_src = self.get_prop_source()
if self.prop_name and prop_src and self.prop_name in prop_src.properties:
if prop_src.properties[self.prop_name].property_type == "Group":
self.draw_warning(layout, "Can't return Group properties!")
@@ -0,0 +1,29 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_SetPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SetPropertyNode"
bl_label = "Set Property"
node_color = "PROPERTY"
def on_create(self, context):
self.add_execute_input()
self.add_property_input("Property")
self.add_data_input("Value").changeable = True
self.add_execute_output()
def evaluate(self, context):
if self.inputs[1].is_linked:
self.code = f"""
{self.inputs[1].python_value} = {self.inputs[2].python_value}
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
{self.indent(self.outputs[0].python_value, 6)}
"""