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