2025-07-01
This commit is contained in:
@@ -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")
|
||||
+28
@@ -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()
|
||||
+17
@@ -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})"
|
||||
+32
@@ -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)
|
||||
+51
@@ -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)
|
||||
+31
@@ -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()
|
||||
+62
@@ -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)
|
||||
+23
@@ -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""
|
||||
+23
@@ -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"
|
||||
+30
@@ -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)}
|
||||
"""
|
||||
+64
@@ -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())"""
|
||||
+181
@@ -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)}
|
||||
"""
|
||||
Reference in New Issue
Block a user