2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -0,0 +1,154 @@
import json
import bpy
from ...nodes.base_node import SN_ScriptingBaseNode
class SN_OT_RemovePreset(bpy.types.Operator):
bl_idname = "sn.remove_preset"
bl_label = "Remove Preset"
bl_description = "Removes this preset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
prefs.presets.remove(self.index)
bpy.ops.wm.save_userpref()
return {"FINISHED"}
class SN_OT_RemovePresets(bpy.types.Operator):
bl_idname = "sn.remove_presets"
bl_label = "Remove Presets"
bl_description = "Remove presets"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
layout.label(text="Presets")
for i, preset in enumerate(prefs.presets):
layout.operator("sn.remove_preset", text=f"Remove '{preset.name}'", icon="REMOVE").index = i
if not len(prefs.presets):
layout.label(text="No presets", icon="INFO")
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=300)
class SN_OT_AddPreset(bpy.types.Operator):
bl_idname = "sn.add_preset"
bl_label = "Add Preset"
bl_description = "Adds the active node as a preset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
return context.space_data.node_tree.nodes.active
def get_save_value(self, data, attr):
value = getattr(data, attr)
if "bpy_prop_array" in str(type(value)) or "Color" in str(type(value)):
return tuple(value)
return value
def execute(self, context):
node = context.space_data.node_tree.nodes.active
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
item = prefs.presets.add()
item.name = node.label if node.label else node.name
item.idname = node.bl_idname
data = { "node": {}, "inputs": [], "outputs": [] }
# get node attributes
attributes = [a for a in dir(node) if not callable(getattr(node, a))]
data_attributes = ["width", "color", "use_custom_color"]
for attr in attributes:
if not attr.startswith("__") and not attr.startswith("bl_")\
and not attr == "code" and not attr.startswith("code_") and not attr.startswith("ref_")\
and not hasattr(SN_ScriptingBaseNode, attr) and not attr in bpy.types.Node.bl_rna.properties.keys()\
and not attr in ["active_layout", "disable_evaluation", "skip_export", "static_uid",]:
data_attributes.append(attr)
# save node attributes
for attr in data_attributes:
data["node"][attr] = self.get_save_value(node, attr)
socket_save_attributes = ["name", "disabled", "index_type", "data_type", "default_value"]
# get input attributes
for inp in node.inputs:
input_data = {}
if not inp.is_program:
for attr in socket_save_attributes:
if hasattr(inp, attr):
input_data[attr] = self.get_save_value(inp, attr)
data["inputs"].append(input_data)
# get output attributes
for out in node.outputs:
output_data = {}
if not out.is_program:
for attr in socket_save_attributes:
if hasattr(out, attr):
output_data[attr] = self.get_save_value(out, attr)
data["outputs"].append(output_data)
item.data = json.dumps(data)
bpy.ops.wm.save_userpref()
return {"FINISHED"}
class SN_OT_LoadPreset(bpy.types.Operator):
bl_idname = "sn.load_preset"
bl_label = "Load Preset"
bl_description = "Loads this preset node"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def get_write_value(self, value):
return list(value) if type(value) == list else value
def execute(self, context):
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
preset = prefs.presets[self.index]
bpy.ops.node.add_node("INVOKE_DEFAULT", type=preset.idname, use_transform=True)
node = context.space_data.node_tree.nodes.active
node.label = preset.name
data = json.loads(preset.data)
# load node data
node.disable_evaluation = True
for attr in data["node"].keys():
setattr(node, attr, self.get_write_value(data["node"][attr]))
# load input data
for i, inp_data in enumerate(data["inputs"]):
node.disable_evaluation = True
for attr in inp_data.keys():
setattr(node.inputs[i], attr, self.get_write_value(inp_data[attr]))
# load output data
for i, out_data in enumerate(data["outputs"]):
node.disable_evaluation = True
for attr in out_data.keys():
setattr(node.outputs[i], attr, self.get_write_value(out_data[attr]))
node.disable_evaluation = False
node._evaluate(context)
return {"FINISHED"}
@@ -0,0 +1,28 @@
import bpy
class SN_MT_PresetMenu(bpy.types.Menu):
bl_idname = "SN_MT_PresetMenu"
bl_label = "Presets"
def draw(self, context):
layout = self.layout
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
for i, preset in enumerate(prefs.presets):
layout.operator("sn.load_preset", text=preset.name).index = i
if not len(prefs.presets):
layout.label(text="No presets", icon="INFO")
layout.separator()
node = context.space_data.node_tree.nodes.active
if node:
layout.operator("sn.add_preset", icon="ADD", text=f"Add '{node.label if node.label else node.name}'")
else:
layout.operator("sn.add_preset", icon="ADD")
row = layout.row()
row.enabled = len(prefs.presets) > 0
row.operator("sn.remove_presets", text="Remove Preset", icon="REMOVE")
@@ -0,0 +1,30 @@
import bpy
class WM_MT_button_context(bpy.types.Menu):
bl_label = ""
def draw(self, context):
pass
def serpens_right_click(self, context):
layout = self.layout
property_pointer = getattr(context, "button_pointer", None)
property_value = getattr(context, "button_prop", None)
button_value = getattr(context, "button_operator", None)
if property_value or button_value:
layout.separator()
if property_value and property_pointer:
layout.operator("sn.copy_property", text="Get Serpens Property", icon="FILE_SCRIPT")
if button_value:
layout.operator("sn.copy_operator", text="Get Serpens Operator", icon="FILE_SCRIPT")
if context:
layout.operator("sn.copy_context", text="Copy Context", icon="COPYDOWN")
@@ -0,0 +1,147 @@
import bpy
REPLACE_NAMES = {
"ObjectBase": "bpy.data.objects['Object']", # outliner object hide
"LayerCollection": "bpy.context.view_layer.active_layer_collection", # outliner collection hide
"SpaceView3D": "bpy.context.screen.areas[0].spaces[0]", # 3d space data
"ToolSettings": "bpy.context.scene.tool_settings", # any space tool settings
}
class SN_OT_CopyProperty(bpy.types.Operator):
bl_idname = "sn.copy_property"
bl_label = "Copy Property"
bl_description = "Copy the path of this property"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
# get property details
property_pointer = getattr(context, "button_pointer", None)
property_value = getattr(context, "button_prop", None)
# copy data path if available
if bpy.ops.ui.copy_data_path_button.poll():
bpy.ops.ui.copy_data_path_button("INVOKE_DEFAULT", full_path=True)
context.scene.sn.last_copied_datatype = property_value.type.title()
path = context.window_manager.clipboard.replace('"', "'")
if path and path[-1] == "]" and path[:-1].split("[")[-1].isdigit():
path = "[".join(path.split("[")[:-1])
context.scene.sn.last_copied_datatype += " Vector"
elif getattr(property_value, "subtype", None) == "COLOR":
context.scene.sn.last_copied_datatype += " Vector"
context.window_manager.clipboard = path
if property_value.type == "ENUM" and property_value.is_enum_flag:
context.scene.sn.last_copied_datatype += " Set"
context.scene.sn.last_copied_datapath = context.window_manager.clipboard
self.report({"INFO"}, message="Copied!")
return {"FINISHED"}
# check if replacement is available
if property_pointer and property_value:
if property_pointer.bl_rna.identifier in REPLACE_NAMES:
context.window_manager.clipboard = f"{REPLACE_NAMES[property_pointer.bl_rna.identifier]}.{property_value.identifier}"
context.window_manager.clipboard = (
context.window_manager.clipboard.replace('"', "'")
)
context.scene.sn.last_copied_datatype = property_value.type.title()
if property_value.type == "ENUM" and property_value.is_enum_flag:
context.scene.sn.last_copied_datatype += " Set"
context.scene.sn.last_copied_datapath = context.window_manager.clipboard
self.report({"INFO"}, message="Copied!")
return {"FINISHED"}
# error when property not available
self.report(
{"ERROR"},
message="We can't copy this property yet! Please use the Blend Data browser to find it!",
)
print("Serpens Log: ", property_pointer, property_value)
return {"CANCELLED"}
class SN_OT_CopyOperator(bpy.types.Operator):
bl_idname = "sn.copy_operator"
bl_label = "Copy Operator"
bl_description = "Copy the path of this operator"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def find_ops_path_from_rna(self, rna_identifier):
for cat_name in dir(bpy.ops):
if cat_name[0].isalpha() and not cat_name == "class":
try:
cat = eval(f"bpy.ops.{cat_name}")
except:
cat = None
if cat:
for op_name in dir(cat):
if op_name[0].isalpha():
try:
op = eval(f"bpy.ops.{cat_name}.{op_name}")
except:
op = None
if op and op.get_rna_type().identifier == rna_identifier:
return f"bpy.ops.{cat_name}.{op_name}()"
return None
def execute(self, context):
# copy operator if available
if bpy.ops.ui.copy_python_command_button.poll():
bpy.ops.ui.copy_python_command_button("INVOKE_DEFAULT")
self.report({"INFO"}, message="Copied!")
return {"FINISHED"}
# get button details
button_value = getattr(context, "button_operator", None)
# check if value exists
if button_value:
op_path = self.find_ops_path_from_rna(button_value.bl_rna.identifier)
if op_path:
context.window_manager.clipboard = op_path
self.report({"INFO"}, message="Copied!")
return {"FINISHED"}
# error when button not available
self.report(
{"ERROR"},
message="We can't copy this operator yet! Please report this to the developers!",
)
print("Serpens Log: ", button_value)
return {"CANCELLED"}
def copy_context():
context = {}
for attribute in dir(bpy.context):
if (
attribute[0].isalpha()
and not attribute in ["property"]
and hasattr(bpy.context, attribute)
and not callable(getattr(bpy.context, attribute))
):
context[attribute] = getattr(bpy.context, attribute)
return context
class SN_OT_CopyContext(bpy.types.Operator):
bl_idname = "sn.copy_context"
bl_label = "Copy Context"
bl_description = "Copy the context from this area"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
copied = copy_context()
context.scene.sn.copied_context.clear()
context.scene.sn.copied_context.append(copied)
context.scene.sn.hide_preferences = True
for screen in bpy.data.screens:
for area in screen.areas:
area.tag_redraw()
self.report({"INFO"}, message="Copied and reloaded!")
return {"FINISHED"}
@@ -0,0 +1,45 @@
import bpy
import os
from ...extensions import snippet_ops
class SN_MT_SnippetsMenu(bpy.types.Menu):
bl_idname = "SN_MT_SnippetsMenu"
bl_label = "Snippets"
def draw(self, context):
layout = self.layout
op = layout.operator("node.add_node", text="Snippet")
op.type = "SN_SnippetNode"
op.use_transform = True
no_cat_snippets = False
for snippet in snippet_ops.loaded_snippets:
if type(snippet) != str:
row = layout.row()
row.context_pointer_set("snippet", context.scene.sn.snippet_categories[snippet["name"]])
row.menu("SN_MT_SnippetMenu", text=snippet["name"])
else:
no_cat_snippets = True
if no_cat_snippets:
layout.menu("SN_MT_SnippetMenu", text="Others")
class SN_MT_SnippetMenu(bpy.types.Menu):
bl_idname = "SN_MT_SnippetMenu"
bl_label = "Snippets"
def draw(self, context):
layout = self.layout
if hasattr(context, "snippet"):
for data in snippet_ops.loaded_snippets:
if not type(data) == str and data["name"] == context.snippet.name:
for snippet in data["snippets"]:
layout.operator("sn.add_snippet", text=snippet.split(".")[0]).path = os.path.join(context.snippet.path, snippet)
else:
path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "extensions", "snippets")
for name in snippet_ops.loaded_snippets:
if type(name) == str:
layout.operator("sn.add_snippet", text=name.split(".")[0]).path = os.path.join(path, name)