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