2025-07-01
This commit is contained in:
@@ -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"}
|
||||
+165
@@ -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
|
||||
+38
@@ -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"}
|
||||
Reference in New Issue
Block a user