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,147 @@
from bpy_extras.io_utils import ExportHelper
import bpy
import os
import shutil
from ...nodes.compiler import (
format_blender_manifest,
format_multifile,
format_single_file,
)
from ...utils import normalize_code
class SN_OT_ExportAddon(bpy.types.Operator, ExportHelper):
bl_idname = "sn.export_addon"
bl_label = "Export Addon"
bl_description = "Exports this addon to an installable zip file"
bl_options = {"REGISTER", "INTERNAL"}
filepath: bpy.props.StringProperty(
name="File Path",
description="Filepath used for exporting the file",
maxlen=1024,
subtype="FILE_PATH",
)
filename_ext = ".zip"
filter_glob: bpy.props.StringProperty(default="*.zip", options={"HIDDEN"})
def add_easy_bpy(self, path, code):
"""Adds the easybpy file to the addon if needed"""
if "easybpy" in code and bpy.context.scene.sn.easy_bpy_path:
shutil.copyfile(
src=bpy.context.scene.sn.easy_bpy_path,
dst=os.path.join(path, "easybpy.py"),
)
def add_assets(self, asset_path):
"""Adds the addon assets to the folder"""
for asset in bpy.context.scene.sn.assets:
if os.path.exists(asset.path):
if os.path.isdir(asset.path):
dirname = os.path.basename(asset.path)
if not dirname:
dirname = os.path.basename(os.path.dirname(asset.path))
shutil.copytree(
asset.path,
os.path.join(asset_path, dirname),
dirs_exist_ok=True,
)
else:
shutil.copy(
asset.path,
os.path.join(asset_path, os.path.basename(asset.path)),
)
def add_icons(self, icon_path):
"""Adds the icons to the folder"""
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if node.bl_idname == "SN_IconNode":
if node.icon_source == "CUSTOM" and node.icon_file:
img_path = bpy.path.abspath(node.icon_file.filepath)
if os.path.exists(img_path):
filepath = os.path.join(
icon_path, os.path.basename(img_path)
)
shutil.copy(img_path, filepath)
else:
raise FileNotFoundError(
f"Could not find the icon file at {icon_path}"
)
def add_code(self, path):
"""Creates the index file"""
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
ntree.reevaluate()
if bpy.context.scene.sn.multifile:
files = format_multifile()
else:
files = {
"__init__.py": format_single_file(),
"blender_manifest.toml": format_blender_manifest(),
}
for name in files.keys():
with open(os.path.join(path, name), "a") as code_file:
code = files[name]
code = code.replace("from easybpy import", "from .easybpy import")
code_file.write(code)
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
ntree.reevaluate()
return code
def create_files(self, path):
"""Creates the addon files in the folder structure"""
self.add_assets(os.path.join(path, "assets"))
self.add_icons(os.path.join(path, "icons"))
code = self.add_code(path)
self.add_easy_bpy(path, code)
def create_structure(self, path):
"""Sets up the addons folder structure at the given filepath"""
os.mkdir(path)
baseDir = os.path.join(path, bpy.context.scene.sn.module_name)
os.mkdir(baseDir)
os.mkdir(os.path.join(baseDir, "assets"))
os.mkdir(os.path.join(baseDir, "icons"))
return baseDir
def zip_addon(self, path):
"""Zips the given path"""
shutil.make_archive(path, "zip", root_dir=path)
try:
shutil.rmtree(path)
except OSError as e:
self.report({"WARNING"}, message=f"Error: {e.filename} - {e.strerror}.")
def execute(self, context):
bpy.context.scene.sn.is_exporting = True
context.window_manager.progress_begin(0, 100)
try:
name, _ = os.path.splitext(self.filepath)
if os.path.exists(name):
self.report(
{"ERROR"},
message=f"Please delete the '{os.path.basename(name)}' folder before exporting.",
)
else:
baseDir = self.create_structure(name)
context.window_manager.progress_update(30)
self.create_files(baseDir)
context.window_manager.progress_update(90)
self.zip_addon(name)
bpy.ops.sn.export_to_marketplace("INVOKE_DEFAULT")
except Exception as e:
self.report({"ERROR"}, message=f"Error: {e}")
bpy.context.scene.sn.is_exporting = False
context.window_manager.progress_end()
return {"FINISHED"}
def invoke(self, context, event):
version = ".".join([str(i) for i in context.scene.sn.version])
self.filepath = f"{context.scene.sn.module_name}_{version}.blend"
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -0,0 +1,135 @@
import bpy
from ...interface.panels.graph_ui_list import get_selected_graph
class SN_GraphCategory(bpy.types.PropertyGroup):
def set_name(self, value):
for ntree in bpy.data.node_groups:
if hasattr(ntree, "category"):
if ntree.category and ntree.category == self.name:
ntree.category = value
self["name"] = value
def get_name(self):
return self.get("name", "New Category")
name: bpy.props.StringProperty(name="Name", default="New Category",
description="The name of this graph category",
set=set_name, get=get_name)
class SN_OT_AddGraphCategory(bpy.types.Operator):
bl_idname = "sn.add_graph_category"
bl_label = "Add Graph Category"
bl_description = "Adds a graph category"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
def execute(self, context):
context.scene.sn.graph_categories.add()
return {"FINISHED"}
class SN_OT_RemoveGraphCategory(bpy.types.Operator):
bl_idname = "sn.remove_graph_category"
bl_label = "Remove Graph Category"
bl_description = "Removes a graph category"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
context.scene.sn.graph_categories.remove(self.index)
return {"FINISHED"}
class SN_OT_EditGraphCategories(bpy.types.Operator):
bl_idname = "sn.edit_graph_categories"
bl_label = "Edit Graph Categories"
bl_description = "Edit the addon graph categories"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.label(text="Categories")
for i, cat in enumerate(context.scene.sn.graph_categories):
row = layout.row()
row.scale_y = 1.2
row.prop(cat, "name", text="")
row.operator("sn.remove_graph_category", text="", icon="REMOVE", emboss=False).index = i
if not context.scene.sn.graph_categories:
row = layout.row()
row.enabled = False
row.label(text="No categories added", icon="ERROR")
row = layout.row()
row.scale_y = 1.2
row.operator("sn.add_graph_category", text="Add Category", icon="ADD")
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=250)
class SN_OT_MoveGraphToCategory(bpy.types.Operator):
bl_idname = "sn.move_graph_to_category"
bl_label = "Move Graph Category"
bl_description = "Move the selected graph to a different category"
bl_options = {"REGISTER", "INTERNAL"}
category: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
ntree = get_selected_graph()
if ntree:
if self.category == -1:
ntree.category = "OTHER"
else:
ntree.category = context.scene.sn.graph_categories[self.category].name
context.area.tag_redraw()
return {"FINISHED"}
class SN_OT_MoveGraphCategory(bpy.types.Operator):
bl_idname = "sn.move_graph_category"
bl_label = "Move Graph Category"
bl_description = "Move the selected graph to a different category"
bl_options = {"REGISTER", "INTERNAL"}
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
ntree = get_selected_graph()
layout.label(text="Categories")
for i, cat in enumerate(context.scene.sn.graph_categories):
row = layout.row()
row.enabled = ntree != None and ntree.category != cat.name
row.scale_y = 1.2
row.operator("sn.move_graph_to_category", text=f"Move to '{cat.name}'", icon="FORWARD").category = i
row = layout.row()
row.enabled = ntree != None and ntree.category and ntree.category != "OTHER"
row.scale_y = 1.2
row.operator("sn.move_graph_to_category", text=f"Remove Category", icon="REMOVE").category = -1
if not len(context.scene.sn.graph_categories):
row = layout.row()
row.enabled = False
row.label(text="No categories added", icon="ERROR")
def invoke(self, context, event):
context.scene.sn.node_tree_index = self.index
return context.window_manager.invoke_popup(self, width=250)
@@ -0,0 +1,235 @@
from bpy_extras.io_utils import ImportHelper
import bpy
import os
from ...interface.panels.graph_ui_list import get_selected_graph, get_selected_graph_offset
from ...nodes.compiler import unregister_addon, compile_addon
def get_serpens_graphs():
graphs = []
for group in bpy.data.node_groups:
if group.bl_idname == "ScriptingNodesTree":
graphs.append(group)
return graphs
def reassign_tree_indices():
trees = []
for ngroup in bpy.data.node_groups:
if ngroup.bl_idname == "ScriptingNodesTree":
trees.append(ngroup)
trees = sorted(trees, key=lambda tree: tree.index)
for i in range(len(trees)):
trees[i].index = i
return trees
class SN_OT_AddGraph(bpy.types.Operator):
bl_idname = "sn.add_graph"
bl_label = "Add Node Tree"
bl_description = "Adds a node tree to the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
sn = context.scene.sn
trees = reassign_tree_indices()
curr_index = 0
if sn.node_tree_index < len(bpy.data.node_groups) and bpy.data.node_groups[sn.node_tree_index].bl_idname == "ScriptingNodesTree":
curr_index = bpy.data.node_groups[sn.node_tree_index].index
for i in range(curr_index+1, len(trees)):
trees[i].index += 1
graph = bpy.data.node_groups.new("NodeTree", "ScriptingNodesTree")
graph.index = curr_index - 1
if sn.active_graph_category != "ALL":
graph.category = sn.active_graph_category
for i, group in enumerate(bpy.data.node_groups):
if group == graph:
sn.node_tree_index = i
return {"FINISHED"}
class SN_OT_RemoveGraph(bpy.types.Operator):
bl_idname = "sn.remove_graph"
bl_label = "Remove Node Tree"
bl_description = "Removes this node tree from the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
if context.scene.sn.node_tree_index < len(bpy.data.node_groups):
return bpy.data.node_groups[context.scene.sn.node_tree_index].bl_idname == "ScriptingNodesTree"
def execute(self, context):
sn = context.scene.sn
group = bpy.data.node_groups[sn.node_tree_index]
curr_index = group.index
bpy.data.node_groups.remove(group)
trees = reassign_tree_indices()
for tree in trees:
if tree.index == curr_index:
for i, ntree in enumerate(bpy.data.node_groups):
if ntree == tree:
sn.node_tree_index = i
break
elif tree.index == curr_index - 1:
for i, ntree in enumerate(bpy.data.node_groups):
if ntree == tree:
sn.node_tree_index = i
break
else:
sn.node_tree_index = 0
compile_addon()
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class SN_OT_AppendGraph(bpy.types.Operator, ImportHelper):
bl_idname = "sn.append_graph"
bl_label = "Append Node Tree"
bl_description = "Appends a node tree from another file to this addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
filter_glob: bpy.props.StringProperty( default='*.blend', options={'HIDDEN'} )
def execute(self, context):
_, extension = os.path.splitext(self.filepath)
if extension == ".blend":
bpy.ops.sn.append_popup("INVOKE_DEFAULT", path=self.filepath)
return {"FINISHED"}
class SN_OT_AppendPopup(bpy.types.Operator):
bl_idname = "sn.append_popup"
bl_label = "Append Node Tree"
bl_description = "Appends this node tree from the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def get_graph_items(self, context):
""" Returns all node trees that can be found in the selected file """
items = []
with bpy.data.libraries.load(self.path) as (data_from, _):
for group in data_from.node_groups:
items.append((group, group, group))
if not items:
items = [("NONE", "NONE", "NONE")]
return items
path: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
graph: bpy.props.EnumProperty(name="Node Tree",
description="Node Tree to import",
items=get_graph_items,
options={"HIDDEN", "SKIP_SAVE"})
def execute(self, context):
if self.graph != "NONE":
# save previous groups
prev_groups = bpy.data.node_groups.values()
# append node group
with bpy.data.libraries.load(self.path) as (_, data_to):
data_to.node_groups = [self.graph]
# register new graph
new_groups = set(prev_groups) ^ set(bpy.data.node_groups.values())
for group in new_groups:
context.scene.sn.node_tree_index = bpy.data.node_groups.values().index(group)
compile_addon()
# redraw screen
context.area.tag_redraw()
return {"FINISHED"}
def draw(self, context):
if self.graph == "NONE":
self.layout.label(text="No Node Trees found in this blend file",icon="ERROR")
else:
self.layout.prop(self, "graph", text="Node Tree")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class SN_OT_ForceCompile(bpy.types.Operator):
bl_idname = "sn.force_compile"
bl_label = "This might be slow for large addons!"
bl_description = "Forces all node trees to compile"
bl_options = {"REGISTER", "INTERNAL"}
def fix_compile_order(self, refs):
for node in refs.nodes:
if node.order == 0:
node.order = 3
def execute(self, context):
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for refs in ntree.node_refs:
refs.clear_unused_refs()
refs.fix_ref_names()
if refs.name == "SN_OnKeypressNode":
self.fix_compile_order(refs)
ntree.reevaluate()
compile_addon()
self.report({"INFO"}, message="Compiled successfully!")
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class SN_OT_ForceUnregister(bpy.types.Operator):
bl_idname = "sn.force_unregister"
bl_label = "Force Unregister"
bl_description = "Forces all node trees to unregister"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
unregister_addon()
return {"FINISHED"}
class SN_OT_MoveNodeTree(bpy.types.Operator):
bl_idname = "sn.move_node_tree"
bl_label = "Move Node Tree"
bl_description = "Moves this node tree in the list"
bl_options = {"REGISTER", "INTERNAL"}
move_up: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
reassign_tree_indices()
ntree = get_selected_graph()
before = get_selected_graph_offset(-1)
after = get_selected_graph_offset(1)
# move trees
if ntree:
if self.move_up and before:
temp_index = ntree.index
ntree.index = before.index
before.index = temp_index
elif not self.move_up and after:
temp_index = ntree.index
ntree.index = after.index
after.index = temp_index
return {"FINISHED"}
@@ -0,0 +1,138 @@
import bpy
import json
class SN_OT_CopyCommand(bpy.types.Operator):
bl_idname = "sn.copy_command"
bl_label = "Copy Command"
bl_description = "Copies the command to post in the discord server"
bl_options = {"REGISTER","UNDO","INTERNAL"}
url: bpy.props.StringProperty(default="",options={"SKIP_SAVE"})
price: bpy.props.StringProperty(default="Free",options={"SKIP_SAVE"})
blender: bpy.props.BoolProperty(default=False,options={"SKIP_SAVE"})
def execute(self, context):
sn = bpy.context.scene.sn
addon_info = {
"name": sn.addon_name,
"description": sn.description,
"category": sn.category if not sn.category == 'CUSTOM' else sn.custom_category,
"author": sn.author,
"blender_version": list(tuple(sn.blender)),
"addon_version": list(tuple(sn.version)),
"external": self.url != "",
"url": self.url,
"price": self.price,
"blend": self.blender,
"blend_url": "",
"user": 0,
"serpens_version": 3
}
if self.url == "":
addon_info["url"] = ""
addon_info["price"] = ""
bpy.context.window_manager.clipboard = json.dumps(addon_info)
self.report({"INFO"},message="Copied successfully!")
return {"FINISHED"}
class SN_OT_ExportToMarketplaceAddon(bpy.types.Operator):
bl_idname = "sn.export_to_marketplace"
bl_label = "Export Addon To Marketplace"
bl_description = "Exports the active node tree to the marketplace"
bl_options = {"REGISTER","UNDO","INTERNAL"}
url: bpy.props.StringProperty(default="",name="Addon URL",description="Enter the url to your addon here")
price: bpy.props.StringProperty(default="Free",name="Addon Price",description="Enter the price of your addon here")
blender: bpy.props.BoolProperty(default=False,options={"SKIP_SAVE"})
upload_type: bpy.props.EnumProperty(name="Upload Type",items=[("DIRECT","Direct Upload","Upload the addon directly"),("URL","External Link","Provide an external url for your addon")])
expand_1: bpy.props.BoolProperty(default=True,name="Expand")
expand_2: bpy.props.BoolProperty(default=False,name="Expand")
expand_3: bpy.props.BoolProperty(default=False,name="Expand")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
return {"FINISHED"}
def draw(self,context):
box = self.layout.box()
box.label(text="If you think your addon is interesting to others you can share it!",icon="FUND")
self.layout.separator()
self.layout.prop(self,"upload_type",expand=True,text=" ")
self.layout.separator()
box = self.layout.box()
box.prop(self,"expand_1",text="Step 1",emboss=False,toggle=True,icon="DISCLOSURE_TRI_DOWN" if self.expand_1 else "DISCLOSURE_TRI_RIGHT")
if self.expand_1:
col = box.column(align=True)
col.label(text=" • Set your addons name, description, version, ...")
col.label(text=" • If you want to update an addon use the same name")
col.label(text=" • Select if you want to upload your blend file with the node tree")
col.separator()
row = col.row(align=True)
row.label(icon="BLANK1")
row.prop(self,"blender", text="Upload blend file")
row.label(icon="BLANK1")
self.layout.separator()
box = self.layout.box()
box.prop(self,"expand_2",text="Step 2",emboss=False,toggle=True,icon="DISCLOSURE_TRI_DOWN" if self.expand_2 else "DISCLOSURE_TRI_RIGHT")
if self.expand_2:
col = box.column(align=False)
if self.upload_type == "DIRECT":
col.label(text=" • Go in the #marketplace channel on discord and post the following:")
row = col.row(align=True)
split = row.split(factor=0.03)
split.label(text=" ")
op = split.operator("sn.copy_command",text="Click To Copy!",icon="COPYDOWN")
op.blender = self.blender
row.operator("wm.url_open",text="",icon_value=bpy.context.scene.sn_icons[ "discord" ].icon_id).url = "https://discord.com/invite/NK6kyae"
row.label(text=" ")
col.separator()
col.label(text=" • You will be asked to upload your addon. Export it and do so.")
elif self.upload_type == "URL":
row = col.row()
row.label(text=" • Paste the link to your addon in here:")
row.prop(self,"url", text="")
col.separator()
row = col.row()
row.label(text=" • Enter the price of your addon here:")
row.prop(self,"price", text="")
if self.upload_type == "URL":
self.layout.separator()
box = self.layout.box()
box.prop(self,"expand_3",text="Step 3",emboss=False,toggle=True,icon="DISCLOSURE_TRI_DOWN" if self.expand_3 else "DISCLOSURE_TRI_RIGHT")
if self.expand_3:
col = box.column(align=False)
col.label(text=" • Go in the #marketplace channel on discord and post the following:")
row = col.row(align=True)
split = row.split(factor=0.03)
split.label(text=" ")
op = split.operator("sn.copy_command",text="Click To Copy!",icon="COPYDOWN")
op.url = self.url
op.price = self.price
op.blender = self.blender
row.operator("wm.url_open",text="",icon_value=bpy.context.scene.sn_icons[ "discord" ].icon_id).url = "https://discord.com/invite/NK6kyae"
row.label(text=" ")
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=500)
@@ -0,0 +1,85 @@
import bpy
node_cache = {} # stores a cache of the nodes with key f"{node_tree.name};{node.static_uid}"
class NodeRef(bpy.types.PropertyGroup):
@property
def node(self):
node_tree = self.id_data
# retrieve node from cache
if f"{node_tree.name};{self.uid}" in node_cache:
return node_cache[f"{node_tree.name};{self.uid}"]
# save node to cache
for node in node_tree.nodes:
if getattr(node, "static_uid", None) == self.uid:
node_cache[f"{node_tree.name};{node.static_uid}"] = node
return node
return None
def set_name(self, value):
prev_name = self.get("name", "")
self["name"] = value
# update references
if prev_name:
ref_node = self.node
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if getattr(node, "ref_ntree", None) == ref_node.node_tree:
# update specific node type references
if getattr(node, f"ref_{ref_node.collection_key}", None) == prev_name:
setattr(node, f"ref_{ref_node.collection_key}", value)
# update node names for any type of node
elif getattr(node, "from_node", None) == prev_name:
setattr(node, f"from_node", value)
def get_name(self):
return self.get("name", "")
uid: bpy.props.StringProperty(name="UID",
description="The static_uid of the node that belongs to this reference")
name: bpy.props.StringProperty(name="Name",
set=set_name,
get=get_name,
description="The name of the node this reference belongs to")
class NodeRefCollection(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="Node Name",
description="The idname of the nodes in this collection")
refs: bpy.props.CollectionProperty(type=NodeRef,
name="References",
description="References to the nodes of this type")
def get_ref_by_uid(self, uid):
for ref in self.refs:
if ref.uid == uid:
return ref
return None
def clear_unused_refs(self):
""" Removes all references that don't match a node """
for i in range(len(self.refs)-1, -1, -1):
if not self.refs[i].node:
self.refs.remove(i)
def fix_ref_names(self):
""" Makes sure all ref names match the node names """
for ref in self.refs:
if ref.get("name") != ref.node.name:
ref["name"] = ref.node.name
ref.node.on_node_name_change()
# ref.node._evaluate(bpy.context)
@property
def nodes(self):
""" Returns all the nodes for this collection """
return [ref.node for ref in self.refs]
@@ -0,0 +1,295 @@
import bpy
from ..sockets.conversions import CONVERSIONS
from .node_refs import NodeRefCollection
from ...addon.variables.variables import SN_VariableProperties
from ...utils import unique_collection_name, get_python_name
class ScriptingNodesTree(bpy.types.NodeTree):
bl_idname = "ScriptingNodesTree"
bl_label = "Visual Scripting Editor"
bl_icon = "FILE_SCRIPT"
is_sn = True
type: bpy.props.EnumProperty(items=[("SCRIPTING", "Scripting", "Scripting")
],
name="Type")
index: bpy.props.IntProperty(
default=0,
description="The index of this node tree in the node tree list",
name="Index",
)
category: bpy.props.StringProperty(
name="Category",
default="OTHER",
description="The category this property is displayed in",
)
link_cache = (
{}
) # stores cache of the links from the previous update for all node trees based on their memory adress
variables: bpy.props.CollectionProperty(
type=SN_VariableProperties,
name="Variables",
description="The variables of this node tree",
)
variable_index: bpy.props.IntProperty(
name="Variable Index",
min=0,
description="Index of the selected variable")
node_refs: bpy.props.CollectionProperty(
type=NodeRefCollection,
name="Node References",
description=
"A collection of groups that hold references to nodes of a specific idname",
)
show_debug: bpy.props.BoolProperty(
name="Show Debug",
default=True,
description="Show node tree data in the debug panel",
)
# cache python names so they only have to be generated once
cached_python_names = {}
cached_python_name: bpy.props.StringProperty()
cached_human_name: bpy.props.StringProperty()
@property
def python_name(self):
if self.name == self.cached_human_name and self.cached_python_name:
return self.cached_python_name
if self.name in self.cached_python_names:
return self.cached_python_names[self.name]
names = []
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
if ntree == self:
break
names.append(ntree.python_name)
name = unique_collection_name(
f"{get_python_name(self.name, 'node_tree')}", "node_tree", names,
"_")
try:
self.cached_python_name = name
self.cached_human_name = self.name
except AttributeError:
pass
self.cached_python_names[self.name] = name
return name
def node_collection(self, idname):
"""Returns the collection for the given node idname refs in this node trees"""
if idname in self.node_refs:
return self.node_refs[idname]
return self.node_refs["empty"]
def _map_link_to_sockets(self, link):
"""Maps the given link to a tuple of the from socket, to socket and the link itself"""
from_real = None
if getattr(link.to_socket, "is_sn", False):
from_real = link.to_socket.from_socket()
return (link.from_socket, link.to_socket, from_real, link)
def is_valid_connection(self, from_out, to_inp):
"""Check if a connection between the given sockets would be valid"""
if from_out and from_out.is_program == to_inp.is_program:
# check if multiple program sockets are connected
if to_inp.is_program:
to_sockets = from_out.to_sockets(check_validity=False)
if from_out.bl_label == to_inp.bl_label:
# check if first same program socket
for socket in to_sockets:
if socket.bl_label == to_inp.bl_label:
if socket == to_inp:
return True
else:
break
return False
# data types are the same
elif from_out.bl_label == to_inp.bl_label:
return True
# check if data types are convertible
else:
if not to_inp.convert_data:
return True
if from_out.bl_label in CONVERSIONS:
if to_inp.bl_label in CONVERSIONS[from_out.bl_label]:
return True
return False
return False
def is_valid_link(self, link):
"""Checks if the given link is valid"""
# all links connected to reroutes inputs are valid
if not getattr(link.to_socket, "is_sn", False):
return True
# get the sockets and return their validity
to_inp = link.to_socket
from_out = link.to_socket.from_socket()
return self.is_valid_connection(from_out, to_inp)
def _insert_define_data_nodes(self, links):
"""Inserts define data nodes for all links invalid links"""
for link in links:
# add define data node
if not getattr(link.from_socket, "changeable", False):
node = self.nodes.new("SN_DefineDataType")
if link.to_socket.bl_idname in list(
map(lambda item: item[0],
node.get_data_items(bpy.context))):
node.convert_to = link.to_socket.bl_idname
node.location = (
(link.from_node.location[0] + link.to_node.location[0]) /
2,
(link.from_node.location[1] + link.to_node.location[1]) /
2,
)
self.links.new(link.from_socket, node.inputs[0])
self.links.new(node.outputs[0], link.to_socket)
# change data output
elif link.from_socket.bl_label == "Data":
to_sockets = link.from_socket.to_sockets(False)
if not link.from_socket.dynamic and len(to_sockets) == 1:
to_socket = to_sockets[0]
if to_socket.bl_idname in list(
map(
lambda item: item[0],
link.from_socket.get_data_type_items(
bpy.context),
)):
link.from_socket.data_type = to_socket.bl_idname
# link.from_socket.subtype = to_socket.subtype
def _update_post(self):
"""Only do visual aspects in here as this is run after evaluating the nodes"""
# mark links as invalid
data_links = []
for link in self.links:
if not self.is_valid_link(link):
link.is_valid = False
if link.from_socket.bl_label == "Data":
data_links.append(link)
self._insert_define_data_nodes(data_links)
def _find_node_from_socket(self, socket):
for node in self.nodes:
for s in [*node.inputs, *node.outputs]:
if s == socket:
return node
return None
def _update_changed_links(self, links):
"""Forces the affected nodes to update depending on if it's a program or data socket"""
for from_out, to_inp, _, _ in links:
to_inp_node = self._find_node_from_socket(to_inp)
from_out_node = self._find_node_from_socket(from_out)
# update data sockets
try:
if (getattr(to_inp, "is_sn", False) and to_inp_node
and not to_inp.is_program):
to_inp.force_update()
# update program sockets
elif (from_out_node and getattr(from_out, "is_sn", False)
and from_out.is_program):
from_out.force_update()
except:
pass
def _call_link_inserts(self, added):
"""Calls link_insert for all new links"""
for from_inp, to_inp, from_real, _ in added:
if from_real:
from_real.node.link_insert(from_real, to_inp, is_output=True)
to_inp.node.link_insert(from_real, to_inp, is_output=False)
elif (from_inp and getattr(from_inp.node, "is_sn", False)
and to_inp and getattr(to_inp.node, "is_sn", False)):
from_inp.node.link_insert(from_inp, to_inp, is_output=True)
def _call_link_removes(self, removed):
"""Calls link_remove for all removed links"""
for _, to_inp, from_real, _ in removed:
if from_real:
node = self._find_node_from_socket(from_real)
if node:
node.link_remove(from_real, to_inp, is_output=True)
node = self._find_node_from_socket(to_inp)
if node:
node.link_remove(from_real, to_inp, is_output=False)
def _update_added_links(self, added):
"""Triggers an update on the given links data outputs and program inputs to update the affected program"""
self._update_changed_links(added)
self._call_link_inserts(added)
def _update_removed_links(self, removed):
"""Triggers an update on the given links data inputs and program outputs to update the affected program"""
self._update_changed_links(removed)
self._call_link_removes(removed)
def _update_tree_links(self):
"""Finds all changed node links and updates the connections"""
# get current links
curr_links = list(map(self._map_link_to_sockets, self.links.values()))
if id(self) in self.link_cache:
# update added links
added = list(set(curr_links) - set(self.link_cache[id(self)]))
self._update_added_links(added)
# update removed links
removed = list(set(self.link_cache[id(self)]) - set(curr_links))
self._update_removed_links(removed)
# update cached current links
self.link_cache[id(self)] = curr_links
# calls a function after the links are realized
bpy.app.timers.register(self._update_post, first_interval=0.001)
def _update_reroutes(self):
"""Updates all inputs and display shapes of the reroutes in this node tree"""
for reroute in self.nodes:
if reroute.bl_idname == "NodeReroute":
try:
connections_left = [
x.from_socket for x in reroute.inputs[0].links
]
connections_right = [
x.to_socket for x in reroute.outputs[0].links
]
if reroute.inputs[0].bl_idname != "SN_RerouteSocket":
reroute.inputs.remove(reroute.inputs[0])
reroute.outputs.remove(reroute.outputs[0])
i = reroute.inputs.new("SN_RerouteSocket", "Input")
o = reroute.outputs.new("SN_RerouteSocket", "Output")
for c in connections_left:
self.links.new(c, i)
for c in connections_right:
self.links.new(c, o)
reroute.inputs[0].display_shape = (
connections_left[0].display_shape
if connections_left else "CIRCLE")
reroute.outputs[0].display_shape = (
connections_left[0].display_shape
if connections_left else "CIRCLE")
except:
pass
def update(self):
# update tree links
self._update_tree_links()
self._update_reroutes()
def reevaluate(self):
"""Reevaluates all nodes in this node tree"""
# evaluate all nodes
for node in self.nodes:
if getattr(node, "is_sn", False):
node._evaluate(bpy.context)
@@ -0,0 +1,69 @@
import bpy
class SN_OT_FindReferencingNodes(bpy.types.Operator):
bl_idname = "sn.find_referencing_nodes"
bl_label = "Find Referencing Nodes"
bl_description = "Find all nodes that reference this node"
bl_options = {'REGISTER', 'UNDO', "INTERNAL"}
node: bpy.props.StringProperty(name="Node", options={'HIDDEN', 'SKIP_SAVE'})
add_node: bpy.props.StringProperty(name="Add Node", options={'HIDDEN', 'SKIP_SAVE'})
references = {}
def execute(self, context):
pass
def draw(self, context):
layout = self.layout
if not self.node in context.space_data.node_tree.nodes: return
ref_node = context.space_data.node_tree.nodes[self.node]
for key in self.references:
layout.label(text=key)
for ref in self.references[key]:
op = layout.operator("sn.find_node", text=ref, icon="RESTRICT_SELECT_OFF")
op.node_tree = key
op.node = ref
layout.separator()
if not self.references:
layout.label(text="No references found", icon="INFO")
if self.add_node:
op = layout.operator("sn.add_referencing_node", text="Add Node", icon="FORWARD")
op.idname = self.add_node
key = ref_node.bl_idname if not ref_node.collection_key_overwrite else ref_node.collection_key_overwrite
op.ref_attr = f"ref_{key}"
op.node = ref_node.name
def invoke(self, context, event):
self.references = {}
ref_node = context.space_data.node_tree.nodes[self.node]
for ngroup in bpy.data.node_groups:
for node in ngroup.nodes:
idname = ref_node.collection_key_overwrite if ref_node.collection_key_overwrite else ref_node.bl_idname
if getattr(node, f"ref_{idname}", None) == ref_node.name and getattr(node, "ref_ntree", None) == ref_node.node_tree:
if not ngroup.name in self.references:
self.references[ngroup.name] = []
self.references[ngroup.name].append(node.name)
return context.window_manager.invoke_popup(self, width=250)
class SN_OT_AddReferencingNode(bpy.types.Operator):
bl_idname = "sn.add_referencing_node"
bl_label = "Add Node"
bl_description = "Adds the referenced node to the node tree"
bl_options = {"REGISTER", "INTERNAL"}
idname: bpy.props.StringProperty(name="ID Name", options={'HIDDEN', 'SKIP_SAVE'})
ref_attr: bpy.props.StringProperty(name="Attribute", options={'HIDDEN', 'SKIP_SAVE'})
node: bpy.props.StringProperty(name="Node", options={'HIDDEN', 'SKIP_SAVE'})
def execute(self, context):
bpy.ops.node.add_node("INVOKE_DEFAULT", type=self.idname, use_transform=True)
node = context.space_data.node_tree.nodes.active
setattr(node, self.ref_attr, self.node)
return {"FINISHED"}
@@ -0,0 +1,178 @@
from .. import auto_load
import os
import inspect
import bpy
from ..extensions import snippet_ops
def flatten_snippets(data):
flat = []
if type(data) == str:
flat.append(data)
else:
cat = bpy.context.scene.sn.snippet_categories[data["name"]]
for item in data["snippets"]:
flat.extend(
list(map(lambda x: os.path.join(cat.path, x), flatten_snippets(item)))
)
return flat
def get_snippet_list():
flat_snippets = []
for snippet in snippet_ops.loaded_snippets:
flat_snippets.extend(flatten_snippets(snippet))
flat_snippets = list(set(flat_snippets))
for i, snippet in enumerate(flat_snippets):
if os.path.basename(snippet) == snippet:
flat_snippets[i] = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"extensions",
"snippets",
snippet,
)
return flat_snippets
class SN_MT_LayoutMenu(bpy.types.Menu):
bl_idname = "SN_MT_LayoutMenu"
bl_label = "Layout"
def draw(self, context):
layout = self.layout
op = layout.operator("node.add_node", text="Frame")
op.type = "NodeFrame"
op.use_transform = True
op = layout.operator("node.add_node", text="Portal")
op.type = "SN_PortalNode"
op.use_transform = True
op = layout.operator("node.add_node", text="Reroute")
op.type = "NodeReroute"
op.use_transform = True
_node_categories = {}
def get_node_categories():
global _node_categories
if _node_categories:
return _node_categories
else:
node_categories = {}
for cls in auto_load.ordered_classes:
if cls.bl_rna.base and cls.bl_rna.base.identifier == "Node":
path = os.path.dirname(inspect.getfile(cls))
dirs = path.split(os.sep)
if "nodes" in dirs:
node_path = dirs[dirs.index("nodes") + 1 :]
parent = node_categories
for dir in node_path:
if not dir in parent:
parent[dir] = {}
parent = parent[dir]
if not "nodes" in parent:
parent["nodes"] = []
parent["nodes"].append(cls)
_node_categories = node_categories
return node_categories
blocklist = ["nodes", "Snippets", "Layout", "Legacy"]
_registered_menus = []
def register_node_menus():
categories = get_node_categories()
for cat in sorted(categories.keys()):
if not cat in blocklist:
register_menu(cat, cat)
register_category_menus(categories[cat], cat)
def register_category_menus(category, path):
for cat in sorted(category.keys()):
if not cat in blocklist:
register_menu(cat, f"{path}.{cat}")
register_category_menus(category[cat], f"{path}.{cat}")
@classmethod
def poll(cls, context):
return context.space_data.tree_type == "ScriptingNodesTree"
def register_menu(name, path):
menu_type = type(
"SN_MT_category_" + name.replace(" ", "_"),
(bpy.types.Menu,),
{
"bl_space_type": "NODE_EDITOR",
"bl_label": name.replace("_", " ").title(),
"path": path,
"poll": poll,
"draw": draw_submenu,
},
)
bpy.utils.register_class(menu_type)
_registered_menus.append(menu_type)
def unregister_node_menus():
for menu in _registered_menus:
try:
bpy.utils.unregister_class(menu)
except:
pass
_registered_menus.clear()
def draw_submenu(self, context):
layout = self.layout
category = get_node_categories()
for path in self.path.split("."):
category = category[path]
for cat in sorted(category.keys()):
if not cat in blocklist:
layout.menu(
"SN_MT_category_" + cat.replace(" ", "_"),
text=cat.replace("_", " ").title(),
)
if "nodes" in category and len(category["nodes"]) and len(category.keys()) > 1:
layout.separator()
if "nodes" in category:
for node in sorted(category["nodes"], key=lambda n: n.bl_label):
op = layout.operator("node.add_node", text=node.bl_label)
op.type = node.bl_idname
op.use_transform = True
def draw_node_menu(self, context):
if context.space_data.tree_type != "ScriptingNodesTree":
return
categories = get_node_categories()
layout = self.layout
layout.separator()
for cat in sorted(categories.keys()):
if not cat in blocklist:
layout.menu(
"SN_MT_category_" + cat.replace(" ", "_"),
text=cat.replace("_", " ").title(),
)
layout.menu("SN_MT_LayoutMenu", text="Layout")
layout.separator()
layout.menu("SN_MT_PresetMenu", text="Presets")
layout.menu("SN_MT_SnippetsMenu", text="Snippets")
@@ -0,0 +1,498 @@
import bpy
from .conversions import CONVERSIONS
from ...addon.properties.settings.settings import property_icons
import time
class ScriptingSocket:
### SOCKET GENERAL
is_sn = True
output_limit = 9999
# OVERWRITE
socket_shape = "CIRCLE" # CIRCLE | SQUARE | DIAMOND
def update_socket_name(self, context):
self.node.on_socket_name_change(self)
name: bpy.props.StringProperty(name="Socket Name",
description="Name of this socket",
update=update_socket_name)
### SOCKET OPTIONS
# OVERWRITE
is_program = False # Only Interface and Execute sockets are programs
dynamic: bpy.props.BoolProperty(
default=False,
name="Dynamic",
description="If this socket adds another socket when connected",
)
prev_dynamic: bpy.props.BoolProperty(
default=False,
name="Previously Dynamic",
description=
"True if this socket was previously dynamic and can now be removed",
)
def update_conversion(self, context):
if self.is_linked:
from_out = self.links[0].from_socket
self.node.node_tree.links.remove(self.links[0])
self.node.node_tree.links.new(from_out, self)
convert_data: bpy.props.BoolProperty(
default=True,
name="Convert Data",
description="Convert the incoming data to this sockets type",
update=update_conversion,
)
def update_disabled(self, context):
self.force_update()
disabled: bpy.props.BoolProperty(
default=False,
name="Disabled",
description="Disable this socket for this node",
update=update_disabled,
)
can_be_disabled: bpy.props.BoolProperty(
default=False,
name="Can Be Hidden",
description=
"Lets the user disable this socket which can be used for evaluation",
)
# OVERWRITE
subtypes = [
"NONE"
] # possible subtypes for this data socket. Vector sockets should be seperate socket types, not subtypes (their size is a subtype)!
subtype_values = {
"NONE": "default_value"
} # the matching propertie names for this data sockets subtype
def on_subtype_update(self):
pass
def get_subtype_items(self, _):
return [(name, name, name) for name in self.subtypes]
def update_subtype(self, _):
self.force_update()
self.on_subtype_update()
self.node.location = self.node.location
subtype: bpy.props.EnumProperty(
name="Subtype",
description="The subtype of this socket",
items=get_subtype_items,
update=update_subtype,
)
@property
def subtype_attr(self):
return self.subtype_values[self.subtype]
# INDEXING OPTIONS
def set_hide(self, value):
"""Sets the hide value of this socket and disconnects all links if hidden"""
if value:
for link in self.links:
self.node.node_tree.links.remove(link)
self.hide = value
def update_index_type(self, context):
if self.indexable and self.bl_idname != self.node.socket_names[
self.index_type]:
# hide all index sockets before blend data input
hide = self.index_type == "Property"
for inp in self.node.inputs:
if inp == self:
hide = False
if inp.indexable:
inp.set_hide(hide)
# convert socket
self.node.convert_socket(self,
self.node.socket_names[self.index_type])
indexable: bpy.props.BoolProperty(
default=False,
name="Indexable",
description=
"If this socket is indexable. Switches between String, Integer and Blend Data",
)
index_type: bpy.props.EnumProperty(
name="Index Type",
description="The type of index this socket indexes the property with",
items=[
("String", "Name", "Name", "SYNTAX_OFF", 0),
("Integer", "Index", "Index", "DRIVER_TRANSFORM", 1),
("Property", "Property", "Property", "MONKEY", 2),
],
update=update_index_type,
)
def update_data_type(self, context):
if self.changeable and self.data_type != self.bl_idname:
self.node.convert_socket(self, self.data_type)
def get_data_type_items(self, context):
items = []
used_idnames = []
for name in list(self.node.socket_names.keys())[2:]:
if not self.node.socket_names[name] in used_idnames:
items.append((
self.node.socket_names[name],
name,
name,
property_icons[name],
len(items),
))
used_idnames.append(self.node.socket_names[name])
return items
changeable: bpy.props.BoolProperty(
default=False,
name="Changeable",
description="If this data socket type can be changed",
)
data_type: bpy.props.EnumProperty(
name="The type this socket has right now",
update=update_data_type,
items=get_data_type_items,
)
# VARIABLE SOCKET OPTIONS
is_variable: bpy.props.BoolProperty(
name="Is Variable",
description="If this socket is a variable socket that can be renamed",
)
### DRAW SOCKET
# OVERWRITE
def draw_socket(self, context, layout, node, text, minimal=False):
pass
def _draw_removable_socket(self, layout, node):
"""Draws the operators for removable sockets"""
op = layout.operator("sn.remove_socket",
text="",
emboss=False,
icon="REMOVE")
op.node = node.name
op.is_output = self.is_output
op.index = self.index
def _draw_dynamic_socket(self, layout, node, text):
"""Draws the operators for dynamic sockets"""
# draw socket label
if self.is_output:
layout.label(text=text)
# draw add operator
op = layout.operator("sn.add_dynamic",
text="",
emboss=False,
icon="ADD")
op.node = node.name
op.is_output = self.is_output
op.insert_above = False
op.index = self.index
# draw socket label
if not self.is_output:
layout.label(text=text)
def _draw_prev_dynamic_socket(self, context, layout, node):
"""Draws the operators for previously dynamic sockets"""
# draw remove socket
self._draw_removable_socket(layout, node)
# draw add above operator
if context.scene.sn.insert_sockets:
op = layout.operator("sn.add_dynamic",
text="",
emboss=False,
icon="TRIA_UP")
op.node = node.name
op.is_output = self.is_output
op.insert_above = True
op.index = self.index
def draw(self, context, layout, node, text):
"""Draws this socket"""
sn = context.scene.sn
text = self.name
# draw debug text for sockets
if sn.debug_python_sockets and self.python_value:
if not sn.debug_selected_only or (sn.debug_selected_only
and self.node.select):
text = self.python_value.replace("\n", " || ")
# draw dynamic sockets
if self.dynamic:
self._draw_dynamic_socket(layout, node, text)
# draw variable socket
elif self.is_variable:
# draw previously dynamic socket (with insert socket)
if not self.is_output and self.prev_dynamic:
self._draw_prev_dynamic_socket(context, layout, node)
layout.prop(self, "name", text="")
self.draw_socket(context, layout, node, "", minimal=True)
# draw changeable socket
if self.changeable:
layout.separator()
layout.prop(self, "data_type", icon_only=True)
# draw previously dynamic socket (with insert socket)
if self.is_output and self.prev_dynamic:
self._draw_prev_dynamic_socket(context, layout, node)
# draw normal socket
else:
# draw output
if self.is_output:
self.draw_socket(context, layout, node, text)
# draw changeable socket
if self.changeable:
layout.separator()
layout.prop(self, "data_type", icon_only=True)
# draw previously dynamic socket (with insert socket)
if self.prev_dynamic:
self._draw_prev_dynamic_socket(context, layout, node)
# draw inputs
if not self.is_output:
# draw disable icon
if self.can_be_disabled:
layout.prop(
self,
"disabled",
icon_only=True,
icon="HIDE_ON" if self.disabled else "HIDE_OFF",
emboss=False,
)
layout = layout.row()
layout.enabled = not self.disabled
# draw disabled socket
if self.can_be_disabled and self.disabled:
layout.label(text=text)
# draw enabled socket
else:
self.draw_socket(context, layout, node, text)
# draw indexable socket
if self.indexable:
layout.prop(self, "index_type", icon_only=True)
# draw changeable socket
if self.changeable:
layout.separator()
layout.prop(self, "data_type", icon_only=True)
### SOCKET COLOR
# OVERWRITE
def get_color(self, context, node):
return (0, 0, 0)
def draw_color(self, context, node):
"""Draws the color of this node based on the get_color function and the status of this socket"""
c = self.get_color(context, node)
alpha = 1
# if self.dynamic:
# alpha = 0
return (c[0], c[1], c[2], alpha)
### PASS CODE AND DATA
# OVERWRITE
default_python_value = "None"
default_prop_value = ""
def get_python_repr(self):
return "None"
def reset_value(self):
"""Resets this sockets python value back to the default"""
self.python_value = self.default_python_value
def _get_python(self):
"""Returns the python value for this socket"""
if self.is_program:
if self.is_output:
# returns the connected program inputs python value or this sockets default
to_socket = self.to_sockets()
if to_socket:
return to_socket[0].python_value
return self.get("python_value", self.default_python_value)
else:
# returns this program inputs python value or its default
return self.get("python_value", self.default_python_value)
else:
if self.is_output:
# returns this data outputs current python value or its default
return self.get("python_value", self.default_python_value)
else:
# returns the connected data outputs current python value or the python representation for this input
from_out = self.from_socket()
if from_out:
value = from_out.python_value
if self.convert_data:
# convert different socket types
if from_out.bl_label != self.bl_label:
value = CONVERSIONS[from_out.bl_label][
self.bl_label](from_out, self)
# convert convertable subtypes of the same socket
elif from_out.subtype != self.subtype:
if from_out.subtype in CONVERSIONS[
from_out.bl_label]:
if (self.subtype in CONVERSIONS[
from_out.bl_label][from_out.subtype]):
value = CONVERSIONS[from_out.bl_label][
from_out.subtype][self.subtype](
from_out, self)
return value
return self.get_python_repr()
def _set_python(self, value):
"""Sets the python value of this socket if it has changed and triggers an update"""
if self.get("python_value") == None or value != self["python_value"]:
self["python_value"] = value
self._trigger_update()
def _trigger_update(self):
"""Triggers node evaluation depending on the type of this socket"""
if self.is_program:
# evaluate this node if this is a program output
if self.is_output:
self.node._evaluate(bpy.context)
# evaluate all connected nodes if this is a program input
else:
from_socket = self.from_socket()
if from_socket:
from_socket.node._evaluate(bpy.context)
else:
# evaluate all connected nodes if this is a data output
if self.is_output:
for socket in self.to_sockets():
socket.node._evaluate(bpy.context)
# evaluate this node if this is a data input
else:
self.node._evaluate(bpy.context)
python_value: bpy.props.StringProperty(
name="Python Value",
description="Python representation of this sockets value",
get=_get_python,
set=_set_python,
)
def _get_value(self):
"""Returns the current value of this socket"""
return self.get(self.subtype_attr, self.default_prop_value)
def _set_value(self, value):
"""Sets the default value depending on the current subtype and updates the python value"""
self[self.subtype_attr] = value
self.python_value = self._get_python()
def _update_value(self, _):
"""Update function for the subtype properties to force an update on the node"""
self.force_update()
# OVERWRITE
default_value: bpy.props.StringProperty(name="Value",
description="Value of this socket",
get=_get_value,
set=_set_value)
def force_update(self):
"""Triggers an update to the connected sockets, for both data and program sockets. Used to pretend the data of this node changed"""
self._trigger_update()
### CONNECTED SOCKETS
def _get_to_sockets(self, socket, check_validity=True):
"""Recursively returns the inputs connected to the given output, skipping over reroutes"""
to_sockets = []
# recursively find all sockets when splitting at reroutes
if socket.node.bl_idname == "NodeReroute":
for link in socket.node.outputs[0].links:
to_sockets += self._get_to_sockets(link.to_socket,
check_validity)
else:
# check validity of connection
if not check_validity or self.node.node_tree.is_valid_connection(
self, socket):
to_sockets.append(socket)
return to_sockets
def to_sockets(self, check_validity=True):
"""Returns all inputs connected to this output, skipping over reroutes"""
sockets = []
for link in self.links:
sockets += self._get_to_sockets(link.to_socket, check_validity)
return sockets
def from_socket(self, check_validity=True):
"""Returns the socket this input comes from skipping over reroutes"""
if len(self.links) > 0:
from_out = self.links[0].from_socket
# find the first socket that is not a reroute
while from_out.node.bl_idname == "NodeReroute":
if len(from_out.node.inputs[0].links) > 0:
from_out = from_out.node.inputs[0].links[0].from_socket
else:
return None
# check connection validity
if not check_validity or self.node.node_tree.is_valid_connection(
from_out, self):
return from_out
return None
### DYNAMIC SOCKETS
@property
def index(self):
"""Returns the index of this socket on the node or -1 if it can't be found"""
for index, socket in enumerate(
self.node.outputs if self.is_output else self.node.inputs):
if socket == self:
return index
return -1
def trigger_dynamic(self, insert_above=False):
"""Adds another socket like this one after itself and turns itself into a normal socket"""
if self.dynamic or self.prev_dynamic:
# add new socket
if self.is_output:
socket = self.node._add_output(self.bl_idname, self.name)
# move socket
self.node.outputs.move(
len(self.node.outputs) - 1,
self.index + 1 if not insert_above else self.index,
)
else:
socket = self.node._add_input(self.bl_idname, self.name)
# move socket
self.node.inputs.move(
len(self.node.inputs) - 1,
self.index + 1 if not insert_above else self.index,
)
self.node.location = self.node.location
# set new socket
socket.dynamic = self.dynamic
socket.prev_dynamic = self.prev_dynamic
socket.subtype = self.subtype
socket.changeable = self.changeable
socket.is_variable = self.is_variable
socket.data_type = self.data_type
if hasattr(socket, "passthrough_layout_type"):
socket.passthrough_layout_type = self.passthrough_layout_type
# set this socket
self.dynamic = False
self.prev_dynamic = True
if socket.dynamic:
self.node.on_dynamic_socket_add(self)
else:
self.node.on_dynamic_socket_add(socket)
socket.node._evaluate(bpy.context)
@@ -0,0 +1,36 @@
import bpy
from .base_socket import ScriptingSocket
class SN_BooleanSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_BooleanSocket"
group = "DATA"
bl_label = "Boolean"
default_python_value = "False"
default_prop_value = False
def get_python_repr(self):
return f"{self.default_value}"
default_value: bpy.props.BoolProperty(name="Value",
default=False,
description="Value of this socket",
get=ScriptingSocket._get_value,
set=ScriptingSocket._set_value)
subtypes = ["NONE"]
subtype_values = {"NONE": "default_value"}
def get_color(self, context, node):
return (0.95, 0.73, 1)
def draw_socket(self, context, layout, node, text, minimal=False):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, self.subtype_attr, text=text)
@@ -0,0 +1,68 @@
import bpy
import mathutils
from .base_socket import ScriptingSocket
class SN_BooleanVectorSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_BooleanVectorSocket"
group = "DATA"
bl_label = "Boolean Vector"
default_python_value = "(False, False, False)"
default_prop_value = tuple([False]*32)
def get_python_repr(self):
return f"{tuple(getattr(self, self.subtype_attr))[:self.size]}"
def _get_value(self):
value = ScriptingSocket._get_value(self)
value = tuple(map(lambda x: bool(x), value))
return tuple(value)
def _set_value(self, value):
value = list(value)
while len(value) < 32:
value.append(False)
ScriptingSocket._set_value(self, tuple(value))
def update_size(self, context):
self.default_python_value = str(tuple([False]*self.size))
self._set_value(self.default_value)
self.node.on_socket_type_change(self)
size: bpy.props.IntProperty(default=3, min=2, max=32,
name="Size",
description="Size of this boolean vector",
update=update_size)
size_editable: bpy.props.BoolProperty(default=False,
name="Size Editable",
description="Let's you edit the vectors size on the socket")
default_value: bpy.props.BoolVectorProperty(name="Value",
size=32,
description="Value of this socket",
get=_get_value,
set=_set_value)
subtypes = ["NONE"]
subtype_values = {"NONE": "default_value"}
def get_color(self, context, node):
return (0.38, 0.34, 0.84)
def draw_socket(self, context, layout, node, text, minimal=False):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
col = layout.column(heading=text, align=True)
for i in range(self.size):
col.prop(self, self.subtype_attr, index=i, text=str(getattr(self, self.subtype_attr)[i]), toggle=True)
if self.size_editable:
layout.prop(self, "size", text="")
@@ -0,0 +1,54 @@
import bpy
from .base_socket import ScriptingSocket
from ...settings.data_properties import bpy_to_path_sections, bpy_to_indexed_sections, join_sections
class SN_CollectionPropertySocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_CollectionPropertySocket"
group = "DATA"
bl_label = "Collection Property"
socket_shape = "SQUARE"
default_python_value = "None"
default_prop_value = ""
def get_python_repr(self):
return self.default_python_value
@property
def python_attr(self):
sections = bpy_to_path_sections(self.python_value)
if sections:
return sections[-1]
return self.python_value
@property
def python_source(self):
sections = bpy_to_path_sections(self.python_value)
if sections:
if self.python_value.startswith("bpy."): sections.insert(0, "bpy")
return join_sections(sections[:-1])
return self.python_value
@property
def python_sections(self):
sections = bpy_to_path_sections(self.python_value)
if sections:
if self.python_value.startswith("bpy."): sections.insert(0, "bpy")
return sections
return []
subtypes = ["NONE"]
subtype_values = {"NONE": "default_value"}
def get_color(self, context, node):
return (0, 0.87, 0.7)
def draw_socket(self, context, layout, node, text, minimal=False):
layout.label(text=text)
@@ -0,0 +1,140 @@
CONVERT_UTILS = """
def string_to_int(value):
if value.isdigit():
return int(value)
return 0
def string_to_icon(value):
if value in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items.keys():
return bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items[value].value
return string_to_int(value)
def icon_to_string(value):
for icon in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items:
if icon.value == value:
return icon.name
return "NONE"
def enum_set_to_string(value):
if type(value) == set:
if len(value) > 0:
return "[" + (", ").join(list(value)) + "]"
return "[]"
return value
def string_to_type(value, to_type, default):
try:
value = to_type(value)
except:
value = default
return value
"""
CONVERSIONS = { # convert KEY to OPTIONS
"String": {
"Data": lambda from_out, to_inp: from_out.python_value,
"Boolean": lambda from_out, to_inp: f"bool({from_out.python_value})",
"Integer": lambda from_out, to_inp: f"string_to_type({from_out.python_value}, int, 0)",
"Float": lambda from_out, to_inp: f"string_to_type({from_out.python_value}, float, 0)",
"Icon": lambda from_out, to_inp: f"string_to_icon({from_out.python_value})" if to_inp.subtype == "NONE" else from_out.python_value,
"Enum": lambda from_out, to_inp: from_out.python_value,
"Enum Set": lambda from_out, to_inp: f"set([{from_out.python_value}])",
},
"Boolean": {
"Data": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str({from_out.python_value})",
"Integer": lambda from_out, to_inp: f"int({from_out.python_value})",
"Float": lambda from_out, to_inp: f"int({from_out.python_value})",
"Icon": lambda from_out, to_inp: f"int({from_out.python_value})",
},
"Boolean Vector": {
"Data": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str(tuple({from_out.python_value}))",
"List": lambda from_out, to_inp: f"list({from_out.python_value})",
},
"Icon": {
"Data": lambda from_out, to_inp: from_out.python_value,
"Integer": lambda from_out, to_inp: from_out.python_value,
"Float": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"icon_to_string({from_out.python_value})",
"Boolean": lambda from_out, to_inp: f"bool({from_out.python_value})",
"NONE": {
"BLENDER_ONLY": lambda from_out, to_inp: f"icon_to_string({from_out.python_value})",
},
"BLENDER_ONLY": {
"NONE": lambda from_out, to_inp: f"string_to_icon({from_out.python_value})",
},
},
"Enum": {
"Data": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: from_out.python_value,
"List": lambda from_out, to_inp: f"[{from_out.python_value}]",
"Boolean": lambda from_out, to_inp: f"bool({from_out.python_value})",
"Integer": lambda from_out, to_inp: f"string_to_type({from_out.python_value}, int, 0)",
"Float": lambda from_out, to_inp: f"string_to_type({from_out.python_value}, float, 0)",
"Enum Flag": lambda from_out, to_inp: f"set([{from_out.python_value}])",
},
"Enum Set": {
"Data": lambda from_out, to_inp: f"list({from_out.python_value})",
"String": lambda from_out, to_inp: f"str(list({from_out.python_value}))",
"List": lambda from_out, to_inp: f"list({from_out.python_value})",
"Boolean": lambda from_out, to_inp: f"bool({from_out.python_value})",
"Integer": lambda from_out, to_inp: f"len({from_out.python_value})",
"Float": lambda from_out, to_inp: f"len({from_out.python_value})",
"Enum": lambda from_out, to_inp: f"{from_out.python_value}[0]",
},
"Integer": {
"Data": lambda from_out, to_inp: from_out.python_value,
"Icon": lambda from_out, to_inp: from_out.python_value,
"Float": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str({from_out.python_value})",
"Boolean": lambda from_out, to_inp: f"bool({from_out.python_value})",
},
"Integer Vector": {
"Data": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str(tuple({from_out.python_value}))",
"List": lambda from_out, to_inp: f"list({from_out.python_value})",
"Float Vector": lambda from_out, to_inp: f"{from_out.python_value}",
},
"Float": {
"Data": lambda from_out, to_inp: from_out.python_value,
"Integer": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str({from_out.python_value})",
"Boolean": lambda from_out, to_inp: f"bool({from_out.python_value})",
},
"Float Vector": {
"Data": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str(tuple({from_out.python_value}))",
"List": lambda from_out, to_inp: f"list({from_out.python_value})",
"Integer Vector": lambda from_out, to_inp: f"tuple(map(lambda v: int(v), {from_out.python_value}))",
},
"List": {
"Data": lambda from_out, to_inp: from_out.python_value,
"Boolean": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str({from_out.python_value})",
"Enum Set": lambda from_out, to_inp: f"set({from_out.python_value})",
"Float Vector": lambda from_out, to_inp: f"tuple({from_out.python_value})",
"Integer Vector": lambda from_out, to_inp: f"tuple({from_out.python_value})",
"Boolean Vector": lambda from_out, to_inp: f"tuple({from_out.python_value})",
"Property": lambda from_out, to_inp: f"{from_out.python_value}[0]",
},
"Property": {
"Data": lambda from_out, to_inp: from_out.python_value,
"Boolean": lambda from_out, to_inp: from_out.python_value,
"Collection Property": lambda from_out, to_inp: from_out.python_value,
"String": lambda from_out, to_inp: f"str({from_out.python_value})",
},
"Collection Property": {
"Data": lambda from_out, to_inp: from_out.python_value,
"Boolean": lambda from_out, to_inp: from_out.python_value,
"Property": lambda from_out, to_inp: f"{from_out.python_value}[0]",
"String": lambda from_out, to_inp: f"str({from_out.python_value})",
"List": lambda from_out, to_inp: f"list({from_out.python_value})",
},
}
@@ -0,0 +1,24 @@
import bpy
from .base_socket import ScriptingSocket
class SN_DataSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_DataSocket"
group = "DATA"
bl_label = "Data"
default_python_value = "None"
default_prop_value = None
def get_python_repr(self):
return f"None"
def get_color(self, context, node):
return (0.2, 0.2, 0.2)
def draw_socket(self, context, layout, node, text, minimal=False):
layout.label(text=text)
@@ -0,0 +1,99 @@
import bpy
from .base_socket import ScriptingSocket
class SocketEnumItem(bpy.types.PropertyGroup):
def update_name(self, context):
node = context.space_data.node_tree.nodes.active
for socket in list(node.inputs) + list(node.outputs):
if socket.bl_label == "Enum" or socket.bl_label == "Enum Set":
for item in socket.custom_items:
if item == self:
node.on_socket_type_change(socket)
return
name: bpy.props.StringProperty(name="Name", default="New Item",
description="Name of this enum item",
update=update_name)
_item_map = dict()
class SN_EnumSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_EnumSocket"
group = "DATA"
bl_label = "Enum"
default_python_value = "\'\'"
default_prop_value = ""
def get_python_repr(self):
return f"\'{getattr(self, self.subtype_attr)}\'"
def make_enum_item(self, _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]
def get_items(self, _):
if self.subtype == "CUSTOM_ITEMS":
items = [self.make_enum_item(item.name, item.name, item.name, 0, i) for i, item in enumerate(self.custom_items)]
if items: return items
else:
names = eval(self.items)
if names: return [self.make_enum_item(name, name, name, 0, i) for i, name in enumerate(names)]
return [("NONE", "NONE", "NONE")]
def _get_value(self):
value = ScriptingSocket._get_value(self)
if value:
return value
return 0
custom_items: bpy.props.CollectionProperty(type=SocketEnumItem)
custom_items_editable: bpy.props.BoolProperty(default=True,
name="Editable Custom Items",
description="Lets you edit the custom items")
items: bpy.props.StringProperty(name="Items",
description="Stringified items for this socket",
default="['NONE']")
default_value: bpy.props.EnumProperty(name="Value",
description="Value of this socket",
items=get_items,
get=_get_value,
set=ScriptingSocket._set_value)
subtypes = ["NONE", "CUSTOM_ITEMS"]
subtype_values = {"NONE": "default_value", "CUSTOM_ITEMS": "default_value"}
def get_color(self, context, node):
return (0.44, 0.7, 1)
def draw_socket(self, context, layout, node, text, minimal=False):
if not minimal:
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, self.subtype_attr, text=text)
if self.subtype == "CUSTOM_ITEMS" and self.custom_items_editable:
op = layout.operator("sn.edit_enum_items", text="", icon="GREASEPENCIL")
op.node = self.node.name
op.is_output = self.is_output
op.index = self.index
@@ -0,0 +1,121 @@
import bpy
class SN_OT_EditEnumItems(bpy.types.Operator):
bl_idname = "sn.edit_enum_items"
bl_label = "Edit Enum Items"
bl_description = "Edit the enum items of this socket"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
is_output: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.label(text="Enum Items")
node = context.space_data.node_tree.nodes[self.node]
socket = node.outputs[self.index] if self.is_output else node.inputs[self.index]
for i, item in enumerate(socket.custom_items):
row = layout.row(align=True)
row.prop(item, "name", text="")
subcol = row.column(align=True)
subcol.enabled = i > 0
op = subcol.operator("sn.move_enum_item_socket", text="", icon="TRIA_UP")
op.node = self.node
op.is_output = self.is_output
op.index = self.index
op.item_index = i
op.move_up = True
subcol = row.column(align=True)
subcol.enabled = i < len(socket.custom_items)-1
op = subcol.operator("sn.move_enum_item_socket", text="", icon="TRIA_DOWN")
op.node = self.node
op.is_output = self.is_output
op.index = self.index
op.item_index = i
op.move_up = False
row.separator()
op = row.operator("sn.remove_enum_item_socket", text="", icon="PANEL_CLOSE", emboss=False)
op.node = self.node
op.is_output = self.is_output
op.index = self.index
op.item_index = i
op = layout.operator("sn.add_enum_item_socket")
op.node = self.node
op.is_output = self.is_output
op.index = self.index
def invoke(self, context, event):
node = context.space_data.node_tree.nodes[self.node]
context.space_data.node_tree.nodes.active = node
return context.window_manager.invoke_popup(self, width=250)
class SN_OT_AddEnumItemSocket(bpy.types.Operator):
bl_idname = "sn.add_enum_item_socket"
bl_label = "Add Item"
bl_description = "Adds an enum item to this socket"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
is_output: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
node = context.space_data.node_tree.nodes[self.node]
socket = node.outputs[self.index] if self.is_output else node.inputs[self.index]
socket.custom_items.add()
socket.node.on_socket_type_change(socket)
return {"FINISHED"}
class SN_OT_RemoveEnumItemSocket(bpy.types.Operator):
bl_idname = "sn.remove_enum_item_socket"
bl_label = "Remove Item"
bl_description = "Removes this enum item from this socket"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
is_output: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
item_index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
node = context.space_data.node_tree.nodes[self.node]
socket = node.outputs[self.index] if self.is_output else node.inputs[self.index]
socket.custom_items.remove(self.item_index)
socket.node.on_socket_type_change(socket)
return {"FINISHED"}
class SN_OT_MoveEnumItemSocket(bpy.types.Operator):
bl_idname = "sn.move_enum_item_socket"
bl_label = "Move Item"
bl_description = "Moves this enum item in this socket"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
is_output: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
item_index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
node = context.space_data.node_tree.nodes[self.node]
socket = node.outputs[self.index] if self.is_output else node.inputs[self.index]
if self.move_up:
socket.custom_items.move(self.item_index, self.item_index-1)
else:
socket.custom_items.move(self.item_index, self.item_index+1)
socket.node.on_socket_type_change(socket)
return {"FINISHED"}
@@ -0,0 +1,89 @@
import bpy
from .base_socket import ScriptingSocket
from .enum import SocketEnumItem
_item_map = dict()
class SN_EnumSetSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_EnumSetSocket"
group = "DATA"
bl_label = "Enum Set"
socket_shape = "SQUARE"
default_python_value = "{}"
default_prop_value = {}
def get_python_repr(self):
return f"{getattr(self, self.subtype_attr)}"
def make_enum_item(self, _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]
def get_items(self, _):
if self.subtype == "CUSTOM_ITEMS":
items = [self.make_enum_item(item.name, item.name, item.name, 0, 2**i) for i, item in enumerate(self.custom_items)]
items = items[:32]
if items: return items
else:
names = eval(self.items)
if names:
names = names[:32]
return [self.make_enum_item(name, name, name, 0, 2**i) for i, name in enumerate(names)]
return [("NONE", "NONE", "NONE", 1)]
def _get_value(self):
value = ScriptingSocket._get_value(self)
if value:
return value
return 1
custom_items: bpy.props.CollectionProperty(type=SocketEnumItem)
custom_items_editable: bpy.props.BoolProperty(default=True,
name="Editable Custom Items",
description="Lets you edit the custom items")
items: bpy.props.StringProperty(name="Items",
description="Stringified items for this socket",
default="['NONE']")
default_value: bpy.props.EnumProperty(name="Value",
description="Value of this socket",
items=get_items,
get=_get_value,
options={"ENUM_FLAG"},
set=ScriptingSocket._set_value)
subtypes = ["NONE", "CUSTOM_ITEMS"]
subtype_values = {"NONE": "default_value", "CUSTOM_ITEMS": "default_value"}
def get_color(self, context, node):
return (0.44, 0.7, 1)
def draw_socket(self, context, layout, node, text, minimal=False):
if not minimal:
if self.is_output or self.is_linked:
layout.label(text=text)
else:
col = layout.column(heading=text)
col.prop(self, self.subtype_attr, text=text)
if self.subtype == "CUSTOM_ITEMS" and self.custom_items_editable:
op = layout.operator("sn.edit_enum_items", text="", icon="GREASEPENCIL")
op.node = self.node.name
op.is_output = self.is_output
op.index = self.index
@@ -0,0 +1,19 @@
import bpy
from .base_socket import ScriptingSocket
class SN_ExecuteSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_ExecuteSocket"
output_limit = 1
socket_shape = "DIAMOND"
is_program = True
bl_label = "Execute"
default_python_value = ""
def get_color(self, context, node):
return (1, 1, 1)
def draw_socket(self, context, layout, node, text, minimal=False):
layout.label(text=text)
@@ -0,0 +1,43 @@
import bpy
from .base_socket import ScriptingSocket
class SN_FloatSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_FloatSocket"
group = "DATA"
bl_label = "Float"
default_python_value = "0"
default_prop_value = 0
def get_python_repr(self):
return f"{getattr(self, self.subtype_attr)}"
default_value: bpy.props.FloatProperty(name="Value",
default=0,
description="Value of this socket",
get=ScriptingSocket._get_value,
set=ScriptingSocket._set_value)
factor_value: bpy.props.FloatProperty(name="Value",
default=0,
description="Value of this socket",
soft_min=0,
soft_max=1,
update=ScriptingSocket._update_value)
subtypes = ["NONE", "FACTOR"]
subtype_values = {"NONE": "default_value", "FACTOR": "factor_value"}
def get_color(self, context, node):
return (0.6, 0.6, 0.6)
def draw_socket(self, context, layout, node, text, minimal=False):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, self.subtype_attr, text=text, slider=self.subtype == "FACTOR")
@@ -0,0 +1,101 @@
import bpy
import mathutils
from .base_socket import ScriptingSocket
class SN_FloatVectorSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_FloatVectorSocket"
group = "DATA"
bl_label = "Float Vector"
default_python_value = "(1.0, 1.0, 1.0)"
default_prop_value = tuple([1.0]*32)
def get_python_repr(self):
return f"{tuple(getattr(self, self.subtype_attr))[:self.size]}"
def _get_value(self):
value = ScriptingSocket._get_value(self)
value = tuple(map(lambda x: float(x), value))
value = list(value)
while len(value) < 32:
value.append(1.0)
return tuple(value)
def _set_value(self, value):
value = list(value)
while len(value) < self.size:
value.append(1.0)
value = value[:self.size]
ScriptingSocket._set_value(self, tuple(value))
def update_size(self, context):
self.default_python_value = str(tuple([1]*self.size))
self._set_value(self.default_value)
self.node.on_socket_type_change(self)
def on_subtype_update(self):
if self.subtype == "COLOR":
self.size = 3
elif self.subtype == "COLOR_ALPHA":
self.size = 4
size: bpy.props.IntProperty(default=3, min=2, max=32,
name="Size",
description="Size of this float vector",
update=update_size)
size_editable: bpy.props.BoolProperty(default=False,
name="Size Editable",
description="Let's you edit the vectors size on the socket")
default_value: bpy.props.FloatVectorProperty(name="Value",
size=32,
description="Value of this socket",
get=_get_value,
set=_set_value)
color_value: bpy.props.FloatVectorProperty(name="Value",
description="Value of this socket",
size=3, min=0, max=1,
default=(0.5,0.5,0.5),
subtype="COLOR",
update=ScriptingSocket._update_value)
color_alpha_value: bpy.props.FloatVectorProperty(name="Value",
description="Value of this socket",
size=4, min=0, max=1,
default=(0.5,0.5,0.5,0.5),
subtype="COLOR",
update=ScriptingSocket._update_value)
subtypes = ["NONE", "COLOR", "COLOR_ALPHA"]
subtype_values = {"NONE": "default_value", "COLOR": "color_value", "COLOR_ALPHA": "color_alpha_value"}
def get_color(self, context, node):
if "COLOR" in self.subtype:
return (0.93, 0.85, 0.25)
return (0.38, 0.34, 0.84)
def draw_socket(self, context, layout, node, text, minimal=False):
if self.is_output or self.is_linked:
layout.label(text=text)
elif self.subtype == "COLOR":
col = layout.column(heading=text, align=True)
col.prop(self, "color_value", text="")
elif self.subtype == "COLOR_ALPHA":
col = layout.column(heading=text, align=True)
col.prop(self, "color_alpha_value", text="")
else:
col = layout.column(heading=text, align=True)
for i in range(self.size):
col.prop(self, self.subtype_attr, index=i, text="", toggle=True)
if self.size_editable:
layout.prop(self, "size", text="")
@@ -0,0 +1,47 @@
import bpy
from .base_socket import ScriptingSocket
class SN_IconSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_IconSocket"
group = "DATA"
bl_label = "Icon"
default_python_value = "0"
default_prop_value = 0
def get_python_repr(self):
value = self.default_value
if self.subtype == "BLENDER_ONLY":
for icon in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items:
if icon.value == value:
return f"'{icon.name}'"
return "NONE"
return f"{value}"
default_value: bpy.props.IntProperty(name="Value",
description="Value of this socket",
get=ScriptingSocket._get_value,
set=ScriptingSocket._set_value)
subtypes = ["NONE", "BLENDER_ONLY"]
subtype_values = {"NONE": "default_value", "BLENDER_ONLY": "default_value"}
def get_color(self, context, node):
return (1,0.4,0.2)
def draw_socket(self, context, layout, node, text, minimal=False):
layout.label(text=text)
if (not self.is_linked or self.subtype == "BLENDER_ONLY") and not self.is_output:
row = layout.row()
row.scale_x = 1.74
op = row.operator("sn.select_icon", text="Choose Icon", icon_value=self.default_value)
op.icon_data_path = f"bpy.data.node_groups['{node.node_tree.name}'].nodes['{node.name}'].inputs[{self.index}]"
op.prop_name = "default_value"
@@ -0,0 +1,36 @@
import bpy
from .base_socket import ScriptingSocket
class SN_IntegerSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_IntegerSocket"
group = "DATA"
bl_label = "Integer"
default_python_value = "0"
default_prop_value = 0
def get_python_repr(self):
return f"{self.default_value}"
default_value: bpy.props.IntProperty(name="Value",
default=0,
description="Value of this socket",
get=ScriptingSocket._get_value,
set=ScriptingSocket._set_value)
subtypes = ["NONE"]
subtype_values = {"NONE": "default_value"}
def get_color(self, context, node):
return (0.15, 0.52, 0.17)
def draw_socket(self, context, layout, node, text, minimal=False):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, self.subtype_attr, text=text)
@@ -0,0 +1,68 @@
import bpy
import mathutils
from .base_socket import ScriptingSocket
class SN_IntegerVectorSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_IntegerVectorSocket"
group = "DATA"
bl_label = "Integer Vector"
default_python_value = "(1, 1, 1)"
default_prop_value = tuple([1]*32)
def get_python_repr(self):
return f"{tuple(getattr(self, self.subtype_attr))[:self.size]}"
def _get_value(self):
value = ScriptingSocket._get_value(self)
value = tuple(map(lambda x: int(x), value))
return tuple(value)
def _set_value(self, value):
value = list(value)
while len(value) < 32:
value.append(1)
ScriptingSocket._set_value(self, tuple(value))
def update_size(self, context):
self.default_python_value = str(tuple([False]*self.size))
self._set_value(self.default_value)
self.node.on_socket_type_change(self)
size: bpy.props.IntProperty(default=3, min=2, max=32,
name="Size",
description="Size of this integer vector",
update=update_size)
size_editable: bpy.props.BoolProperty(default=False,
name="Size Editable",
description="Let's you edit the vectors size on the socket")
default_value: bpy.props.IntVectorProperty(name="Value",
size=32,
description="Value of this socket",
get=_get_value,
set=_set_value)
subtypes = ["NONE"]
subtype_values = {"NONE": "default_value"}
def get_color(self, context, node):
return (0.38, 0.34, 0.84)
def draw_socket(self, context, layout, node, text, minimal=False):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
col = layout.column(heading=text, align=True)
for i in range(self.size):
col.prop(self, self.subtype_attr, index=i, text="", toggle=True)
if self.size_editable:
layout.prop(self, "size", text="")
@@ -0,0 +1,23 @@
import bpy
from .base_socket import ScriptingSocket
class SN_InterfaceSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_InterfaceSocket"
output_limit = 1
socket_shape = "DIAMOND"
is_program = True
bl_label = "Interface"
default_python_value = ""
passthrough_layout_type: bpy.props.BoolProperty(default=False,
description="Pass through layout type of the node before",
name="Pass Through Layout Type")
def get_color(self, context, node):
return (0.9, 0.6, 0)
def draw_socket(self, context, layout, node, text, minimal=False):
layout.label(text=text)
@@ -0,0 +1,34 @@
import bpy
from .base_socket import ScriptingSocket
class SN_ListSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_ListSocket"
group = "DATA"
bl_label = "List"
socket_shape = "SQUARE"
default_python_value = "[]"
default_prop_value = []
def get_python_repr(self):
return f"[]"
default_value: bpy.props.StringProperty(name="Value",
default="",
description="Value of this socket",
get=ScriptingSocket._get_value,
set=ScriptingSocket._set_value)
subtypes = ["NONE"]
subtype_values = {"NONE": "default_value"}
def get_color(self, context, node):
return (0.85, 0.15, 1)
def draw_socket(self, context, layout, node, text, minimal=False):
layout.label(text=text)
@@ -0,0 +1,138 @@
import bpy
from .base_socket import ScriptingSocket
from ...settings.data_properties import (
bpy_to_path_sections,
bpy_to_indexed_sections,
join_sections,
)
blend_data_defaults = {
"Scenes": {"value": "bpy.context.scene", "name": "Using Active"},
"Scene": {"value": "bpy.context.scene", "name": "Using Active"},
"Objects": {
"value": "bpy.context.view_layer.objects.active",
"name": "Using Active",
},
"Object": {
"value": "bpy.context.view_layer.objects.active",
"name": "Using Active",
},
"Meshes": {
"value": "bpy.context.view_layer.objects.active.data",
"name": "Using Active",
},
"Mesh": {
"value": "bpy.context.view_layer.objects.active.data",
"name": "Using Active",
},
"Materials": {
"value": "bpy.context.view_layer.objects.active.active_material",
"name": "Using Active",
},
"Material": {
"value": "bpy.context.view_layer.objects.active.active_material",
"name": "Using Active",
},
"Areas": {"value": "bpy.context.area", "name": "Using Active"},
"Area": {"value": "bpy.context.area", "name": "Using Active"},
"Screens": {"value": "bpy.context.screen", "name": "Using Active"},
"Screen": {"value": "bpy.context.screen", "name": "Using Active"},
"View Layers": {"value": "bpy.context.view_layer", "name": "Using Active"},
"View Layer": {"value": "bpy.context.view_layer", "name": "Using Active"},
"Light": {"value": "bpy.context.view_layer.objects.active", "name": "Using Active"},
"Lights": {
"value": "bpy.context.view_layer.objects.active",
"name": "Using Active",
},
"Camera": {
"value": "bpy.context.view_layer.objects.active",
"name": "Using Active",
},
"Cameras": {
"value": "bpy.context.view_layer.objects.active",
"name": "Using Active",
},
"Preferences": {
"value": lambda: (
f"bpy.context.preferences.addons[__package__].preferences"
if bpy.context.scene.sn.is_exporting
else "bpy.context.scene.sna_addon_prefs_temp"
),
"name": "Using Self",
},
"Operator": {"value": "self", "name": "Using Self"},
"Modal Operator": {"value": "self", "name": "Using Self"},
}
class SN_PropertySocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_PropertySocket"
group = "DATA"
bl_label = "Property"
@property
def default_python_value(self):
if self.name in blend_data_defaults:
value = blend_data_defaults[self.name]["value"]
if type(value) == str:
return value
else:
return value()
return "None"
default_prop_value = ""
def get_python_repr(self):
return self.default_python_value
@property
def python_attr(self):
sections = bpy_to_path_sections(self.python_value)
if sections:
return sections[-1]
return self.python_value if self.python_value else "None"
@property
def python_is_attribute(self):
sections = bpy_to_path_sections(self.python_value)
if sections:
last_section = sections[-1].replace("'", '"')
if last_section[0] == "[" and last_section[-1] == "]":
return True
return False
@property
def python_source(self):
sections = bpy_to_path_sections(self.python_value)
if sections:
if self.python_value.startswith("bpy."):
sections.insert(0, "bpy")
path = join_sections(sections[:-1])
if path:
return path
return self.python_value if self.python_value else "None"
@property
def python_sections(self):
sections = bpy_to_path_sections(self.python_value)
if sections:
if self.python_value.startswith("bpy."):
sections.insert(0, "bpy")
return sections
return []
subtypes = ["NONE"]
subtype_values = {"NONE": "default_value"}
def get_color(self, context, node):
return (0, 0.87, 0.7)
def draw_socket(self, context, layout, node, text, minimal=False):
if not self.is_output and not self.is_linked:
if self.name in blend_data_defaults:
text += f" ({blend_data_defaults[self.name]['name']})"
else:
text += " (No Data)"
layout.label(text=text)
@@ -0,0 +1,18 @@
import bpy
class SN_RerouteSocket(bpy.types.NodeSocket):
bl_idname = "SN_RerouteSocket"
bl_label = "Reroute"
def draw_color(self, context, node):
if self.is_output:
return node.inputs[0].draw_color(context, node)
if self.is_linked:
return self.links[0].from_socket.draw_color(context, self.links[0].from_node)
return (0.3, 0.3, 0.3, 1)
def draw(self, context, layout, node, text):
layout.label(text=text)
@@ -0,0 +1,132 @@
import bpy
class SN_OT_AddDynamic(bpy.types.Operator):
bl_idname = "sn.add_dynamic"
bl_label = "Add Dynamic Socket"
bl_description = "Add another socket like this one"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
is_output: bpy.props.BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
index: bpy.props.IntProperty(options={"HIDDEN", "SKIP_SAVE"})
insert_above: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
@classmethod
def poll(cls, context):
return hasattr(context.space_data, "node_tree") and context.space_data.node_tree
def execute(self, context):
# find node in node tree
ntree = context.space_data.node_tree
if self.node in ntree.nodes:
node = ntree.nodes[self.node]
# find socket
if self.is_output:
socket = node.outputs[self.index]
else:
socket = node.inputs[self.index]
# trigger adding dynamic socket
socket.trigger_dynamic(self.insert_above)
# trigger reevaluation
node._evaluate(context)
return {"FINISHED"}
class SN_OT_RemoveSocket(bpy.types.Operator):
bl_idname = "sn.remove_socket"
bl_label = "Remove Socket"
bl_description = "Removes this socket"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
is_output: bpy.props.BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
index: bpy.props.IntProperty(options={"HIDDEN", "SKIP_SAVE"})
@classmethod
def poll(cls, context):
return hasattr(context.space_data, "node_tree") and context.space_data.node_tree
def execute(self, context):
# find node in node tree
ntree = context.space_data.node_tree
if self.node in ntree.nodes:
node = ntree.nodes[self.node]
# find socket
if self.is_output:
node.outputs.remove(node.outputs[self.index])
else:
node.inputs.remove(node.inputs[self.index])
# trigger reevaluation
node.on_dynamic_socket_remove(self.index, self.is_output)
node._evaluate(context)
return {"FINISHED"}
class SN_OT_SetIcon(bpy.types.Operator):
bl_idname = "sn.set_icon"
bl_label = "Set Icon"
bl_description = "Sets this icon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
icon_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
prop_name: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"}, default="icon")
icon: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
data = eval(self.icon_data_path)
setattr(data, self.prop_name, self.icon)
context.area.tag_redraw()
return {"FINISHED"}
class SN_OT_SelectIcon(bpy.types.Operator):
bl_idname = "sn.select_icon"
bl_label = "Select Icon"
bl_description = "Shows you a selection of all blender icons"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_property = "icon_search"
icon_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
prop_name: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"}, default="icon")
icon_search: bpy.props.StringProperty(name="Search", options={"SKIP_SAVE"})
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=800)
def draw(self, context):
layout = self.layout
icons = (
bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items
)
data = eval(self.icon_data_path)
prop = getattr(data, self.prop_name)
row = layout.row()
row.prop(self, "icon_search", text="", icon="VIEWZOOM")
grid = layout.grid_flow(align=True, even_columns=True, even_rows=True)
for icon in icons:
# NOTE filtering out icon 806 because it throws an error for some reason
if icon.value != 806 and (
self.icon_search.lower() in icon.name.lower() or not self.icon_search
):
op = grid.operator(
"sn.set_icon",
text="",
icon_value=icon.value,
emboss=prop == icon.value,
)
op.icon_data_path = self.icon_data_path
op.prop_name = self.prop_name
op.icon = icon.value
@@ -0,0 +1,80 @@
import bpy
from .base_socket import ScriptingSocket
class SN_StringSocket(bpy.types.NodeSocket, ScriptingSocket):
bl_idname = "SN_StringSocket"
group = "DATA"
bl_label = "String"
default_python_value = "\'\'"
default_prop_value = ""
string_repr_warning: bpy.props.BoolProperty(default=False,
name="Potential Error Warning!",
description="You're using two types of quotes in your string! Be aware that this will cause syntax errors if you don't change ' to \\'")
def get_python_repr(self):
self.string_repr_warning = False
value = getattr(self, self.subtype_attr)
if self.subtype == "DIR_PATH" and value and value[-1] == "\\":
value = value[:-1]
if "'" in value and not '"' in value:
value = f"\"{value}\""
elif '"' in value and not "'" in value:
value = f"\'{value}\'"
else:
if "'" in value and '"' in value:
self.string_repr_warning = True
value = f"\'{value}\'"
if self.subtype == "NONE":
return value
return f"r{value}"
default_value: bpy.props.StringProperty(name="Value",
description="Value of this socket",
get=ScriptingSocket._get_value,
set=ScriptingSocket._set_value)
def update_file_path(self, context):
new_path = bpy.path.abspath(self.value_file_path)
if new_path and new_path[-1] == "\\":
new_path = new_path[:-1]
self["value_file_path"] = new_path
self._update_value(context)
value_file_path: bpy.props.StringProperty(name="Value",
description="Value of this socket",
subtype="FILE_PATH",
update=update_file_path)
def update_dir_path(self, context):
new_path = bpy.path.abspath(self.value_dir_path)
if new_path and new_path[-1] == "\\":
new_path = new_path[:-1]
self["value_dir_path"] = new_path
self._update_value(context)
value_dir_path: bpy.props.StringProperty(name="Value",
description="Value of this socket",
subtype="DIR_PATH",
update=update_dir_path)
subtypes = ["NONE", "FILE_PATH", "DIR_PATH"]
subtype_values = {"NONE": "default_value", "FILE_PATH": "value_file_path", "DIR_PATH": "value_dir_path"}
def get_color(self, context, node):
return (0.44, 0.7, 1)
def draw_socket(self, context, layout, node, text, minimal=False):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
if self.string_repr_warning:
layout.prop(self, "string_repr_warning", text="", icon="ERROR", emboss=False)
layout.prop(self, self.subtype_attr, text=text)