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,129 @@
import bpy
from ...addon.properties.property_basic import BasicProperty
from ...addon.properties.property_utils import get_sorted_props
class SN_NodeProperty(BasicProperty, bpy.types.PropertyGroup):
@property
def data_path(self):
data = self.prop_collection_origin.bl_label.replace(" ", "_").upper() + "_PLACEHOLDER"
return f"{data}.{self.python_name}"
@property
def register_code_imperative(self):
# create property groups
if self.property_type == "Group" and hasattr(self, "imperative_code"):
return self.settings.imperative_code()
return ""
@property
def register_code(self):
# register group properties
if self.property_type == "Group":
return f"bpy.utils.register_class(SNA_GROUP_{self.python_name})"
return ""
@property
def register_code_props(self):
# register non group properties
if not self.property_type == "Group":
code = f"{self.python_name}: bpy.props.{self.settings.prop_type_name}(name='{self.name}', description='{self.description}',{self.get_prop_options} {self.settings.register_options})"
# add register code from prop settings
if hasattr(self.settings, "imperative_code"):
return self.settings.imperative_code() + "\n" + code
return code
return ""
@property
def unregister_code(self):
# unregister group properties
if self.property_type == "Group":
return f"bpy.utils.unregister_class(SNA_GROUP_{self.python_name})"
return ""
def compile(self, context=None):
self.prop_collection_origin.on_node_property_change(self)
self.prop_collection_origin._evaluate(bpy.context)
class PropertyNode():
property_index: bpy.props.IntProperty(name="Index",
description="Index of the selected property",
min=0, default=0)
properties: bpy.props.CollectionProperty(type=SN_NodeProperty)
def props_imperative(self, context):
code = ""
props = get_sorted_props(self.properties.values())
for prop in props:
code += prop.register_code_imperative + "\n"
return code
def props_register(self, context):
code = ""
props = get_sorted_props(self.properties.values())
for prop in props:
code += prop.register_code + "\n"
return code
def props_unregister(self, context):
code = ""
props = get_sorted_props(self.properties.values())
props.reverse()
for prop in props:
code += prop.unregister_code + "\n"
return code
def props_code(self, context):
code = ""
props = get_sorted_props(self.properties.values())
for prop in props:
code += prop.register_code_props + "\n"
return code
def draw_list(self, layout):
row = layout.row()
row.template_list("SN_UL_PropertyList", self.static_uid, self, "properties", self, "property_index")
col = row.column(align=True)
col.operator("sn.add_property_node_popup", icon="FORWARD", text="").node = self.name
op = col.operator("sn.edit_node_property", text="", icon="GREASEPENCIL")
op.node_tree = self.node_tree.name
op.node = self.name
col.separator()
op = col.operator("sn.add_node_property", text="", icon="ADD")
op.node_tree = self.node_tree.name
op.node = self.name
op = col.operator("sn.remove_node_property", text="", icon="REMOVE")
op.node_tree = self.node_tree.name
op.node = self.name
col.separator()
subrow = col.row(align=True)
subrow.enabled = self.property_index > 0
op = subrow.operator("sn.move_node_property", text="", icon="TRIA_UP")
op.node_tree = self.node_tree.name
op.node = self.name
op.move_up = True
subrow = col.row(align=True)
subrow.enabled = self.property_index < len(self.properties)-1
op = subrow.operator("sn.move_node_property", text="", icon="TRIA_DOWN")
op.node_tree = self.node_tree.name
op.node = self.name
op.move_up = False
@@ -0,0 +1,96 @@
import bpy
class SN_OT_AddNodeProperty(bpy.types.Operator):
bl_idname = "sn.add_node_property"
bl_label = "Add Property"
bl_description = "Adds a property to this node"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
ntree = bpy.data.node_groups[self.node_tree]
node = ntree.nodes[self.node]
new_prop = node.properties.add()
new_prop.name = "New Property"
node.on_node_property_add(new_prop)
node.property_index = len(node.properties) - 1
return {"FINISHED"}
class SN_OT_RemoveNodeProperty(bpy.types.Operator):
bl_idname = "sn.remove_node_property"
bl_label = "Remove Property"
bl_description = "Removes the selected property from this node"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
ntree = bpy.data.node_groups[self.node_tree]
node = ntree.nodes[self.node]
if node.property_index < len(node.properties):
node.properties.remove(node.property_index)
node.on_node_property_remove(node.property_index)
node.property_index -= 1
return {"FINISHED"}
class SN_OT_MoveNodeProperty(bpy.types.Operator):
bl_idname = "sn.move_node_property"
bl_label = "Move Property"
bl_description = "Moves this property"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
node = bpy.data.node_groups[self.node_tree].nodes[self.node]
if self.move_up:
node.properties.move(node.property_index, node.property_index - 1)
node.on_node_property_move(node.property_index, node.property_index-1)
node.property_index -= 1
else:
node.properties.move(node.property_index, node.property_index + 1)
node.on_node_property_move(node.property_index, node.property_index+1)
node.property_index += 1
return {"FINISHED"}
class SN_OT_EditNodeProperty(bpy.types.Operator):
bl_idname = "sn.edit_node_property"
bl_label = "Edit Property"
bl_description = "Opens a popup for editing the selected property"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
node = bpy.data.node_groups[self.node_tree].nodes[self.node]
prop = node.properties[node.property_index]
prop.draw(context, layout)
layout.separator()
prop.settings.draw(context, layout)
def invoke(self, context, event):
node = bpy.data.node_groups[self.node_tree].nodes[self.node]
if node.property_index < len(node.properties):
return context.window_manager.invoke_popup(self, width=300)
return {"FINISHED"}
@@ -0,0 +1,165 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...addon.properties.settings.settings import prop_to_socket
class PropertyReferenceNode():
add_indexing_inputs = False
allow_prop_group = True
def on_ref_prop_change(self, context): pass
def on_prop_change(self, context):
# if self.add_indexing_inputs:
prop_src = self.get_prop_source()
if self.inputs and self.inputs[0].bl_label == "Property":
self.inputs[0].name = "Data"
if self.prop_name and prop_src:
if self.prop_name in prop_src.properties:
prop = prop_src.properties[self.prop_name]
# property from group -> input is pointer
if self.inputs and self.inputs[0].bl_label == "Property":
if self.from_prop_group and self.inputs:
self.inputs[0].name = "Pointer Property"
# addon property
elif hasattr(prop, "attach_to"):
self.inputs[0].name = prop.attach_to
# node property
else:
self.inputs[0].name = prop_src.bl_label
# convert output to correct type
if "Value" in self.outputs:
socket_name = prop_to_socket(prop)
self.convert_socket(self.outputs["Value"], self.socket_names[socket_name])
self.on_ref_prop_change(context)
self._evaluate(context)
prop_name: bpy.props.StringProperty(name="Property",
description="Select the property you want to generate items for",
update=on_prop_change)
def prop_source_items(self, context):
items = [("ADDON", "Addon", "Addon Properties"),
("NODE", "Node", "Node Properties")]
return items
prop_source: bpy.props.EnumProperty(name="Property Source",
items=prop_source_items,
description="Where the property should be selected from",
update=on_prop_change)
from_prop_group: bpy.props.BoolProperty(name="Use Property Group",
description="Select the property from a property group",
update=on_prop_change)
prop_group: bpy.props.StringProperty(name="Property Group",
description="Select the property group to select the property from",
update=on_prop_change)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Node Tree", description="Node Tree to select the property node from",
poll=lambda _, ntree: ntree.bl_idname == "ScriptingNodesTree",
update=on_prop_change)
from_node: bpy.props.StringProperty(name="Node",
description="The node which to take the property from",
update=on_prop_change)
def get_prop_source(self):
""" Returns the parent of the collection the property should be searched in """
if self.prop_source == "ADDON":
src = bpy.context.scene.sn
elif self.prop_source == "NODE":
if self.ref_ntree and self.from_node and self.from_node in self.ref_ntree.nodes:
src = self.ref_ntree.nodes[self.from_node]
else:
return None
if self.from_prop_group and hasattr(src, "properties") and self.prop_group in src.properties and src.properties[self.prop_group].property_type == "Group":
return src.properties[self.prop_group].settings
elif not self.from_prop_group:
return src
return None
def get_prop_group_src(self):
""" Returns the parent of the collection the property group should be searched in """
if self.prop_source == "ADDON":
return bpy.context.scene.sn
elif self.prop_source == "NODE":
if self.ref_ntree and self.from_node and self.from_node in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.from_node]
if hasattr(node, "properties"):
return node
return None
def draw_warning(self, layout, warning):
row = layout.row()
row.alert = True
row.label(text=warning, icon="ERROR")
def draw_reference_selection(self, layout, unique_selection=False, draw_prop_source=True):
if draw_prop_source:
layout.prop(self, "prop_source", expand=True)
if self.allow_prop_group and not self.prop_source == "NODE":
layout.prop(self, "from_prop_group", text="From Property Group")
prop_src = self.get_prop_source()
prop_group_src = self.get_prop_group_src()
# select node
if self.prop_source == "NODE":
row = layout.row(align=True)
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
if self.ref_ntree:
row.prop_search(self, "from_node", self.ref_ntree, "nodes", text="")
if self.from_node in self.ref_ntree.nodes:
if not hasattr(self.ref_ntree.nodes[self.from_node], "properties"):
self.draw_warning(layout, "The selected node has no properties!")
elif not self.from_node:
self.draw_warning(layout, "No node selected!")
else:
self.draw_warning(layout, "No node tree selected!")
# select prop group and property
row = layout.row(align=True)
if self.from_prop_group and prop_group_src:
row.prop_search(self, "prop_group", prop_group_src, "properties", text="", icon="FILEBROWSER")
if prop_group_src and prop_src:
row.prop_search(self, "prop_name", prop_src, "properties", text="")
# warnings prop group
if self.from_prop_group and self.prop_group and prop_group_src:
if not self.prop_group in prop_group_src.properties:
self.draw_warning(layout, "Can't find this property group!")
elif prop_group_src.properties[self.prop_group].property_type != "Group":
self.draw_warning(layout, "The selected property is not a group!")
# warnings property
if self.prop_name and prop_src:
if not self.prop_name in prop_src.properties:
self.draw_warning(layout, "Can't find this property!")
# multiple nodes warning
if unique_selection:
if self.prop_name:
for node in self.collection.nodes: # TODO for NODE
if node != self and self.prop_source == node.prop_source and self.prop_name == node.prop_name:
if self.prop_source == "ADDON" or (self.prop_source == "NODE" and self.from_node == node.from_node and self.ref_ntree == self.ref_ntree):
if self.from_prop_group and node.from_prop_group and self.prop_group == node.prop_group:
self.draw_warning(layout, "Multiple nodes found for this property!")
break
elif not self.from_prop_group and not node.from_prop_group:
self.draw_warning(layout, "Multiple nodes found for this property!")
break
@@ -0,0 +1,38 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class VariableReferenceNode():
def on_var_changed(self): pass
def var_update(self, context):
self.on_var_changed()
self._evaluate(context)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Node Tree",
description="Node Tree to get the variable from",
poll=lambda _, ntree: ntree.bl_idname == "ScriptingNodesTree",
update=var_update)
var_name: bpy.props.StringProperty(name="Variable",
description="Variable to get the value from",
update=var_update)
def on_var_change(self):
pass
def get_var(self):
if self.ref_ntree and self.var_name in self.ref_ntree.variables:
return self.ref_ntree.variables[self.var_name]
return None
def draw_variable_reference(self, layout):
row = layout.row(align=True)
row.prop(self, "ref_ntree", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
parent_tree = self.node_tree if not self.ref_ntree else self.ref_ntree
subrow.prop_search(self, "var_name", parent_tree, "variables", text="")
@@ -0,0 +1,78 @@
import bpy
class SN_OT_PasteDataPath(bpy.types.Operator):
bl_idname = "sn.paste_data_path"
bl_label = "Paste Data Path"
bl_description = "Paste the copied data path into this node"
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):
# replace bpy.data.screens for space data NOTE: maybe abstract this in the future to a dict like in rightclick ops
new_path = None
if "bpy.data.screens[" in context.window_manager.clipboard:
new_path = "]".join(context.window_manager.clipboard.split("]")[1:])
new_path = f"bpy.context.screen.areas[0].spaces[0].{new_path}"
elif "bpy.context.area.spaces.active" in context.window_manager.clipboard:
new_path = context.window_manager.clipboard.replace("bpy.context.area.spaces.active", "bpy.context.screen.areas[0].spaces[0]")
elif "bpy.context.space_data" in context.window_manager.clipboard:
new_path = context.window_manager.clipboard.replace("bpy.context.space_data", "bpy.context.screen.areas[0].spaces[0]")
if new_path:
if context.scene.sn.last_copied_datapath == context.window_manager.clipboard:
context.scene.sn.last_copied_datapath = new_path
context.window_manager.clipboard = new_path
if "bpy." in context.window_manager.clipboard:
node = bpy.data.node_groups[self.node_tree].nodes[self.node]
# paste in blender property
if node.bl_idname == "SN_BlenderPropertyNode":
node.pasted_data_path = context.window_manager.clipboard
# can set data type
if context.window_manager.clipboard == context.scene.sn.last_copied_datapath and \
context.scene.sn.last_copied_datatype in node.socket_names:
node.outputs["Value"].data_type = node.socket_names[context.scene.sn.last_copied_datatype]
# data type is function
elif "(" in context.window_manager.clipboard and ")" in context.window_manager.clipboard:
loc = node.location
ntree = node.node_tree
ntree.nodes.remove(node)
node = ntree.nodes.new("SN_RunPropertyFunctionNode")
node.location = loc
node.pasted_data_path = context.window_manager.clipboard
# can't set data type
else:
node.outputs["Value"].data_type = node.socket_names["Data"]
self.report({"WARNING"}, message="Couldn't set the output value data type!")
# paste in run property function
elif node.bl_idname == "SN_RunPropertyFunctionNode":
# is function
if "(" in context.window_manager.clipboard and ")" in context.window_manager.clipboard:
node.pasted_data_path = context.window_manager.clipboard
# isn't function
else:
self.report({"ERROR"}, message="Can only paste functions in this node!")
else:
self.report({"ERROR"}, message="Not a valid blender data path. Use the blend data browser.")
return {"FINISHED"}
class SN_OT_ResetDataPath(bpy.types.Operator):
bl_idname = "sn.reset_data_path"
bl_label = "Reset Data Path"
bl_description = "Resets the data path of this node"
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):
bpy.data.node_groups[self.node_tree].nodes[self.node].pasted_data_path = ""
return {"FINISHED"}