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,99 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class PrintProperty(bpy.types.PropertyGroup):
text: bpy.props.StringProperty()
class SN_OT_ClearPrints(bpy.types.Operator):
bl_idname = "sn.clear_prints"
bl_label = "Clear Prints"
bl_description = "Clear this print nodes messages"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
bpy.data.node_groups[self.node_tree].nodes[self.node].messages.clear()
return {"FINISHED"}
class SN_PrintNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PrintNode"
bl_label = "Print"
bl_width_default = 200
node_color = "PROGRAM"
messages: bpy.props.CollectionProperty(type=PrintProperty)
print_on_node: bpy.props.BoolProperty(default=True,
name="Print On Node",
description="Show print results on this node",
update=SN_ScriptingBaseNode._evaluate)
limit_prints: bpy.props.IntProperty(default=0, min=0,
name="Amount of print messages that will be added to this node before a previous one is removed (0 is unlimited)",
update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_input()
self.add_string_input()
self.add_dynamic_string_input()
self.add_execute_output()
def evaluate(self, context):
self.messages.clear()
values = [inp.python_value for inp in self.inputs[1:-1]]
self.code_imperative = f"""
def sn_print(on_node, node, limit, *args):
print(*args)
if on_node:
try:
msg = node.messages.add()
msg.text = str(args)[1:-1]
if limit and len(node.messages) > limit:
node.messages.remove(0)
for area in bpy.context.screen.areas: area.tag_redraw()
except:
print("Can't add print outputs to the node when the print is run in an interface!")
"""
self.code = f"""
sn_print({self.print_on_node}, bpy.data.node_groups['{self.node_tree.name}'].nodes['{self.name}'], {self.limit_prints}, {", ".join(values)})
{self.indent(self.outputs[0].python_value, 5)}
"""
def evaluate_export(self, context):
self.messages.clear()
values = [inp.python_value for inp in self.inputs[1:-1]]
self.code = f"""
print({", ".join(values)})
{self.indent(self.outputs[0].python_value, 5)}
"""
def draw_node(self, context, layout):
layout.prop(self, "print_on_node", text="Print On Node")
if self.print_on_node:
layout.prop(self, "limit_prints", text="Limit")
if self.print_on_node:
if not self.messages:
box = layout.box()
box.label(text="Nothing printed!")
else:
row = layout.row()
row.label(text="Messages:")
op = row.operator("sn.clear_prints", text="", icon="TRASH", emboss=False)
op.node_tree = self.node_tree.name
op.node = self.name
col = layout.column(align=True)
col.scale_y = 0.9
for msg in self.messages:
box = col.box()
box.label(text=msg.text)
@@ -0,0 +1,23 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_SleepNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SleepNode"
bl_label = "Sleep"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_float_input("Seconds")
self.add_execute_output()
def evaluate(self, context):
self.code_import = "import time"
self.code = f"""
time.sleep({self.inputs[1].python_value})
{self.indent(self.outputs[0].python_value, 3)}
"""
@@ -0,0 +1,103 @@
import bpy
from random import uniform
from ..base_node import SN_ScriptingBaseNode
class SN_TimestampNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_TimestampNode"
bl_label = "Timestamp"
bl_width_default = 200
def update_connected_portals(self, context=None):
if self.timestamp_type == "START":
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.node_collection(self.bl_idname).nodes:
if node.timestamp_type == "END" and node.var_name == self.var_name:
node.var_name = self.var_name
timestamp_type: bpy.props.EnumProperty(name="Type",
description="Start the timestamp recording or print the total time since the start",
items=[("START", "Start", "Start", "MOD_TIME", 0),
("END", "End", "End", "TIME", 1)],
update=update_connected_portals)
timestamp: bpy.props.FloatProperty(name="Timestamp")
def update_var_name(self, context):
self.label = self.var_name
self._evaluate(context)
def get_var_name(self):
return self.get("var_name", "")
def set_var_name(self, value):
if self.timestamp_type == "INPUT":
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.node_collection(self.bl_idname).nodes:
if node.timestamp_type == "OUTPUT" and node.var_name == self.var_name:
node["var_name"] = value
self["var_name"] = value
var_name: bpy.props.StringProperty(name="Name",
description="The identifier that links this timestamp to other timestamps",
get=get_var_name, set=set_var_name,
update=update_var_name)
def update_custom_color(self, context):
# update own color
self.color = self.custom_color
# update connected color
if self.timestamp_type == "START":
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.node_collection(self.bl_idname).nodes:
if node.timestamp_type == "END" and node.var_name == self.var_name:
node.custom_color = self.custom_color
custom_color: bpy.props.FloatVectorProperty(name="Color",
size=3, min=0, max=1, subtype="COLOR",
description="The color of this node",
update=update_custom_color)
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.var_name = self.uuid
self.custom_color = (uniform(0,1), uniform(0,1), uniform(0,1))
def evaluate(self, context):
self.code_import = "import time"
if self.timestamp_type == "START":
self.code = f"""
t_{self.var_name} = time.time()
{self.indent(self.outputs[0].python_value, 3)}
"""
elif self.timestamp_type == "END":
self.code = f"""
if "t_{self.var_name}" in locals(): print(f"Time elapsed for {self.var_name}: {{(time.time() - t_{self.var_name}) * 1000}}ms")
try: bpy.data.node_groups['{self.node_tree.name}'].nodes['{self.name}'].timestamp = time.time() - t_{self.var_name}
except: print("Failed to set timestamp on node")
for a in bpy.context.screen.areas: a.tag_redraw()
{self.indent(self.outputs[0].python_value, 3)}
"""
def evaluate_export(self, context):
self.code = f"""
{self.indent(self.outputs[0].python_value, 3)}
"""
def draw_node(self, context, layout):
layout.prop(self, "timestamp_type", expand=True)
if self.timestamp_type == "START":
row = layout.row(align=True)
split = row.split(factor=0.6, align=True)
split.prop(self, "var_name", text="")
sub_split = split.split(factor=0.5, align=True)
sub_split.prop(self, "custom_color", text="")
sub_split.operator("sn.reset_portal", text="", icon="LOOP_BACK").node = self.name
else:
layout.label(text=f"Time elapsed: {self.timestamp*1000}ms")
@@ -0,0 +1,55 @@
import operator
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_OT_TriggerTriggerNode(bpy.types.Operator):
bl_idname = "sn.trigger_trigger_node"
bl_label = "Trigger Node"
bl_description = "Trigger this node"
bl_options = {"REGISTER", "INTERNAL"}
uid: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
if self.uid in context.scene.sn.function_store:
context.scene.sn.function_store[self.uid]()
else:
self.report({"WARNING"}, message="Couldn't find trigger function!")
return {"FINISHED"}
class SN_TriggerNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_TriggerNode"
bl_label = "Trigger"
is_trigger = True
bl_width_default = 200
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.trigger_trigger_node", text="Run Trigger", icon="PLAY")
op.uid = self.static_uid
@property
def handler_name(self):
return f"trigger_handler_{self.static_uid}"
def evaluate(self, context):
self.code = f"""
def {self.handler_name}():
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.context.scene.sn.function_store['{self.static_uid}'] = {self.handler_name}"
def evaluate_export(self, context):
self.code = ""
self.code_register = ""
@@ -0,0 +1,42 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_AfterRenderNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AfterRenderNode"
bl_label = "On Render Finish"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("render_cancel", "Cancel", "On canceling a render job"), ("render_post", "After", "On render (after)"), ("render_complete", "Complete", "On completion of render job")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.handler = self.action
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,34 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_BeforeExitNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BeforeExitNode"
bl_label = "On Blender Close"
is_trigger = True
bl_width_default = 200
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = "EXIT"
def evaluate(self, context):
self.code_import = f"import atexit"
self.code = f"""
def before_exit_handler_{self.static_uid}():
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"""atexit.register(before_exit_handler_{self.static_uid})"""
self.code_unregister = f"""atexit.unregister(before_exit_handler_{self.static_uid})"""
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_BeforeRenderNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BeforeRenderNode"
bl_label = "On Render Start"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("render_init", "Init", "On initialization of a render job"), ("render_pre", "Before", "On render (before)")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DepsgraphUpdateNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DepsgraphUpdateNode"
bl_label = "On Depsgraph Update"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("depsgraph_update_pre", "Before", "On depsgraph update (before)"), ("depsgraph_update_post", "After", "On depsgraph update (after)")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_ChangeFrameNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ChangeFrameNode"
bl_label = "On Frame Change"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("frame_change_pre", "Before", "Called after frame change for playback and rendering, before any data is evaluated for the new frame"), ("frame_change_post", "After", "Called after frame change for playback and rendering, after the data has been evaluated for the new frame")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,386 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ..Program.Run_Operator import on_operator_ref_update
space_names = {
"EMPTY": "Window",
"VIEW_3D": "3D View",
"IMAGE_EDITOR": "Image",
"NODE_EDITOR": "Node Editor",
"SEQUENCE_EDITOR": "Sequencer",
"CLIP_EDITOR": "Clip",
"DOPESHEET_EDITOR": "Dopesheet",
"GRAPH_EDITOR": "Graph Editor",
"NLA_EDITOR": "NLA Editor",
"TEXT_EDITOR": "Text",
"CONSOLE": "Console",
"INFO": "Info",
"OUTLINER": "Outliner",
"PROPERTIES": "Property Editor",
"FILE_BROWSER": "File Browser"
}
class SN_OnKeypressNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_OnKeypressNode"
bl_label = "On Keypress"
is_trigger = True
bl_width_default = 200
key: bpy.props.StringProperty(name="Key",default="Y", update=SN_ScriptingBaseNode._evaluate)
recording: bpy.props.BoolProperty(name="Recording",default=False)
ctrl: bpy.props.BoolProperty(name="Ctrl",
description="If the ctrl key has to be pressed",
default=False,
update=SN_ScriptingBaseNode._evaluate)
alt: bpy.props.BoolProperty(name="Alt",
description="If the alt key has to be pressed",
default=False,
update=SN_ScriptingBaseNode._evaluate)
shift: bpy.props.BoolProperty(name="Shift",
description="If the shift key has to be pressed",
default=False,
update=SN_ScriptingBaseNode._evaluate)
value: bpy.props.EnumProperty(name="Value",
description="The way the key has to be pressed",
items=[("PRESS","On Press","The action is run when the key is pressed once"),
("RELEASE","On Release","The action is run when the key is released"),
("CLICK","On Click","The action is run when the mouse button is clicked once"),
("DOUBLE_CLICK","On Double Click","The action is run when the mouse button is clicked twice"),
("ANY","Any","The action will run with any of the above ways"),
("CLICK_DRAG","On Click Drag","The action is run when you click and drag")],
update=SN_ScriptingBaseNode._evaluate)
direction: bpy.props.EnumProperty(name="Direction",
description="Direction to drag the mouse in to run the action",
items=[("ANY","Any Direction","The action will run when you click and drag in any direction"),
("NORTH","North","The action will run when you click and drag in this direction"),
("NORTH_EAST","North East","The action will run when you click and drag in this direction"),
("EAST","East","The action will run when you click and drag in this direction"),
("SOUTH_EAST","South East","The action will run when you click and drag in this direction"),
("SOUTH","South","The action will run when you click and drag in this direction"),
("SOUT_WEST","South West","The action will run when you click and drag in this direction"),
("WEST","West","The action will run when you click and drag in this direction"),
("NORTH_WEST","North West","The action will run when you click and drag in this direction")],
update=SN_ScriptingBaseNode._evaluate)
repeat: bpy.props.BoolProperty(name="Repeat Key",
description="Repeat the action when the key is held down",
default=False,
update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.ref_ntree = self.node_tree
self.order = 3
def reset_inputs(self, context=None):
self.inputs.clear()
self.pasted_operator = ""
self.ref_SN_OperatorNode = ""
self._evaluate(context)
def get_spaces(self, context):
items = [("EMPTY","Any Space","Run this shortcut in any space")]
for item in list(space_names.keys())[1:]:
items.append((item,item.replace("_"," ").title(),item.replace("_"," ").title()))
return items
space: bpy.props.EnumProperty(items=get_spaces,
name="Space",
description="The space which this action should be able to run in",
update=SN_ScriptingBaseNode._evaluate)
action: bpy.props.EnumProperty(items=[("OPERATOR","Operator","Operator"),
("PANEL","Panel","Panel"),
("MENU","Menu","Menu"),
("PIE_MENU","Pie Menu","Pie Menu")],
name="Shortcut Type",
description="The type of action to run from this shortcut",
update=reset_inputs)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=reset_inputs)
def update_picked(self,context):
if self.picked:
elem = eval("bpy.types." + self.picked)
if elem.bl_rna.base.identifier == "Menu":
if "pie" in self.picked.lower():
self.action = "PIE_MENU"
self.pie = self.picked
else:
self.action = "MENU"
self.menu = self.picked
elif elem.bl_rna.base.identifier == "Panel":
self.action = "PANEL"
self.panel = self.picked
self.picked = ""
picked: bpy.props.StringProperty(update=update_picked)
panel: bpy.props.StringProperty(name="Panel",
description="The panel to open when the key is pressed",
update=SN_ScriptingBaseNode._evaluate)
menu: bpy.props.StringProperty(name="Menu",
description="The menu to open when the key is pressed",
update=SN_ScriptingBaseNode._evaluate)
pie: bpy.props.StringProperty(name="Pie Menu",
description="The pie menu to open when the key is pressed",
update=SN_ScriptingBaseNode._evaluate)
def create_inputs(self, op_rna):
""" Create inputs for operator """
for prop in op_rna.properties:
if not prop.identifier in ["rna_type", "settings"]:
inp = self.add_input_from_property(prop)
if inp:
inp.can_be_disabled = not prop.is_required
inp.disabled = not prop.is_required
def update_pasted_operator(self, context):
self.inputs.clear()
if self.pasted_operator:
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
self.create_inputs(op_rna)
self._evaluate(context)
pasted_operator: bpy.props.StringProperty(name="Operator",
description="The operator to run when the key is pressed",
update=update_pasted_operator)
keep_open: bpy.props.BoolProperty(name="Keep Open",
description="Keep the panel open after a property is changed",
default=True,
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
ref_SN_PanelNode: bpy.props.StringProperty(name="Panel",
description="The panel to open with this shortcut",
update=SN_ScriptingBaseNode._evaluate)
ref_SN_MenuNode: bpy.props.StringProperty(name="Menu",
description="The menu to open with this shortcut",
update=SN_ScriptingBaseNode._evaluate)
ref_SN_PieMenuNode: bpy.props.StringProperty(name="Menu",
description="The menu to open with this shortcut",
update=SN_ScriptingBaseNode._evaluate)
def on_ref_update(self, node, data=None):
if node.bl_idname in ["SN_PanelNode", "SN_MenuNode", "SN_PieMenuNode"]:
self._evaluate(bpy.context)
elif node.bl_idname in ["SN_OperatorNode", "SN_ModalOperatorNode"]:
on_operator_ref_update(self, node, data, self.ref_ntree, self.ref_SN_OperatorNode, 0)
def update_custom_operator(self, context):
""" Updates the nodes settings when a new parent panel is selected """
self.inputs.clear()
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
parent = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
for prop in parent.properties:
if prop.property_type in ["Integer", "Float", "Boolean"] and prop.settings.is_vector:
socket = self._add_input(self.socket_names[prop.property_type + " Vector"], prop.name)
socket.size = prop.settings.size
socket.can_be_disabled = True
elif prop.property_type == "Enum":
if prop.stngs_enum.enum_flag:
socket = self._add_input(self.socket_names["Enum Set"], prop.name)
else:
socket = self._add_input(self.socket_names[prop.property_type], prop.name)
socket.items = str(list(map(lambda item: item.name, prop.stngs_enum.items)))
else:
self._add_input(self.socket_names[prop.property_type], prop.name).can_be_disabled = True
self._evaluate(context)
ref_SN_OperatorNode: bpy.props.StringProperty(name="Operator",
description="The operator to run with this shortcut",
update=update_custom_operator)
def evaluate(self, context):
if self.key:
input_code = ""
operator = ""
if self.action == "PANEL":
if self.parent_type == "BLENDER" and self.panel:
operator = "wm.call_panel"
input_code = f"kmi.properties.name = '{self.panel}'\n"
input_code += f"kmi.properties.keep_open = {self.keep_open}\n"
elif self.parent_type == "CUSTOM" and self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
operator = "wm.call_panel"
node = self.ref_ntree.nodes[self.ref_SN_PanelNode]
input_code = f"kmi.properties.name = '{node.last_idname}'\n"
input_code += f"kmi.properties.keep_open = {self.keep_open}\n"
elif self.action == "MENU":
if self.parent_type == "BLENDER" and self.menu:
operator = "wm.call_menu"
input_code = f"kmi.properties.name = '{self.menu}'\n"
elif self.parent_type == "CUSTOM" and self.ref_ntree and self.ref_SN_MenuNode in self.ref_ntree.nodes:
operator = "wm.call_menu"
node = self.ref_ntree.nodes[self.ref_SN_MenuNode]
input_code = f"kmi.properties.name = '{node.idname}'\n"
elif self.action == "PIE_MENU":
if self.parent_type == "BLENDER" and self.pie:
operator = "wm.call_menu_pie"
input_code = f"kmi.properties.name = '{self.pie}'\n"
elif self.parent_type == "CUSTOM" and self.ref_ntree and self.ref_SN_PieMenuNode in self.ref_ntree.nodes:
operator = "wm.call_menu_pie"
node = self.ref_ntree.nodes[self.ref_SN_PieMenuNode]
input_code = f"kmi.properties.name = '{node.idname}'\n"
elif self.action == "OPERATOR":
if self.parent_type == "BLENDER" and self.pasted_operator:
if not self.pasted_operator: return
operator = self.pasted_operator.split("(")[0].replace("bpy.ops.", "")
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
for inp in self.inputs:
if not inp.disabled:
for prop in op_rna.properties:
if (self.version == 0 and (prop.name and prop.name == inp.name) or (not prop.name and prop.identifier.replace("_", " ").title() == inp.name)) \
or (self.version == 1 and (inp.name.replace(" ", "_").lower() == prop.identifier)):
self.code += "\n" + f"op.{prop.identifier} = {inp.python_value}"
input_code += f"kmi.properties.{prop.identifier} = {inp.python_value}\n"
elif self.parent_type == "CUSTOM" and self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
operator = f"sna.{node.operator_python_name}"
for inp in self.inputs:
if not inp.disabled:
for prop in node.properties:
if prop.name == inp.name:
input_code += f"kmi.properties.{prop.python_name} = {inp.python_value}\n"
if operator:
self.code_register = f"""
kc = bpy.context.window_manager.keyconfigs.addon
km = kc.keymaps.new(name='{space_names[self.space]}', space_type='{self.space}')
kmi = km.keymap_items.new('{operator}', '{self.key}', '{self.value}',
ctrl={self.ctrl}, alt={self.alt}, shift={self.shift}, repeat={self.repeat})
{f'kmi.direction = "{self.direction}"' if self.value == "CLICK_DRAG" else ''}
{self.indent(input_code, 5)}
addon_keymaps['{self.static_uid}'] = (km, kmi)
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "space", text="")
row.prop(self, "value", text="")
if self.value == "CLICK_DRAG":
layout.prop(self, "direction", text="")
layout.operator("sn.record_key", text=self.key, depress=self.recording).node = self.name
row = layout.row(align=True)
row.prop(self,"ctrl", toggle=True)
row.prop(self,"shift", toggle=True)
row.prop(self,"alt", toggle=True)
layout.prop(self, "repeat")
layout.separator()
row = layout.row(align=True)
row.prop(self, "name")
row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM").node = self.name
layout.separator()
row = layout.row(align=True)
row.prop(self,"action", text="")
row.prop(self,"parent_type", text="", icon_only=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
if self.action == "PANEL":
if self.parent_type == "BLENDER":
name = self.panel.replace("_", " ").title() if self.panel else "Select Panel"
op = layout.operator("sn.pick_interface", text=name, icon="EYEDROPPER")
op.node = self.name
op.selection = "PANELS"
else:
row = layout.row(align=True)
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_PanelNode", parent_tree.node_collection("SN_PanelNode"), "refs", text="", icon="VIEWZOOM")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_PanelNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_PanelNode
layout.prop(self, "keep_open")
elif self.action == "MENU":
if self.parent_type == "BLENDER":
name = self.menu.replace("_", " ").title() if self.menu else "Select Menu"
op = layout.operator("sn.pick_interface", text=name, icon="EYEDROPPER")
op.node = self.name
op.selection = "MENUS"
else:
row = layout.row(align=True)
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_MenuNode", parent_tree.node_collection("SN_MenuNode"), "refs", text="", icon="VIEWZOOM")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_MenuNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_MenuNode
elif self.action == "PIE_MENU":
if self.parent_type == "BLENDER":
name = self.pie.replace("_"," ").title() if self.pie else "Select Pie Menu"
op = layout.operator("sn.pick_interface", text=name, icon="EYEDROPPER")
op.node = self.name
op.selection = "MENUS"
else:
row = layout.row(align=True)
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_PieMenuNode", parent_tree.node_collection("SN_PieMenuNode"), "refs", text="", icon="VIEWZOOM")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_PieMenuNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_PieMenuNode
elif self.action == "OPERATOR":
if self.parent_type == "BLENDER":
name = self.pasted_operator.split("(")[0].split(".")[-1].replace("_"," ").title() if self.pasted_operator else "Paste Operator"
op = layout.operator("sn.paste_operator", text=name, icon="PASTEDOWN")
op.node_tree = self.node_tree.name
op.node = self.name
else:
row = layout.row(align=True)
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OperatorNode", parent_tree.node_collection("SN_OperatorNode"), "refs", text="", icon="VIEWZOOM")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_OperatorNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_OperatorNode
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_OnLoadNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_OnLoadNode"
bl_label = "On Load"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("load_pre", "Before", "On loading a new blend file (before)"), ("load_post", "After", "On loading a new blend file (after)")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_OnSaveNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_OnSaveNode"
bl_label = "On Save"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("save_pre", "Before", "On blend file save (before)"), ("save_post", "After", "On blend file save (after)")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_RedoEventNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RedoEventNode"
bl_label = "On Redo"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("redo_pre", "Before", "On loading a redo step (before)"), ("redo_post", "After", "On loading a redo step (after)")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,41 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_UndoEventNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_UndoEventNode"
bl_label = "On Undo"
is_trigger = True
bl_width_default = 200
action: bpy.props.EnumProperty(items=[("undo_pre", "Before", "On loading an undo step (before)"), ("undo_post", "After", "On loading an undo step (after)")],name="Time of Action", description="When you want your event handler to run", update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_output()
def draw_node(self,context,layout):
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.run_event",text="Run Event",icon="PLAY")
op.uid = self.static_uid
op.handler = self.action
layout.prop(self, "action", expand=True)
@property
def handler_name(self):
return f"{self.action}_handler_{self.static_uid}"
def evaluate(self, context):
self.code_import = f"from bpy.app.handlers import persistent"
self.code = f"""
@persistent
def {self.handler_name}(dummy):
{self.indent(self.outputs[0].python_value, 7) if self.outputs[0].python_value.strip() else 'pass'}
"""
self.code_register = f"bpy.app.handlers.{self.action}.append({self.handler_name})"
self.code_unregister = f"bpy.app.handlers.{self.action}.remove({self.handler_name})"
@@ -0,0 +1,41 @@
import bpy
import atexit
class SN_OT_RunEventButton(bpy.types.Operator):
bl_idname = "sn.run_event"
bl_label = "Run Event"
bl_description = "Run the event code without triggering event"
bl_options = {"REGISTER", "INTERNAL"}
uid: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
handler: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def get_atexit_functions(self):
funs = []
class Capture:
def __eq__(self, other):
funs.append(other)
return False
c = Capture()
atexit.unregister(c)
return funs
def execute(self, context):
if self.handler == "EXIT":
for func in self.get_atexit_functions():
if self.uid in func.__name__:
func()
return {"FINISHED"}
else:
handlers = getattr(bpy.app.handlers, self.handler)
for handler in handlers:
if self.uid in handler.__name__:
handler(None)
return {"FINISHED"}
self.report({'WARNING'}, "Failed when trying to run event. Try recompiling!")
return {"CANCELLED"}
@@ -0,0 +1,123 @@
import bpy
class SN_OT_RecordKey(bpy.types.Operator):
bl_idname = "sn.record_key"
bl_label = "Record Key"
bl_description = "Records the next pressed key"
bl_options = {"REGISTER","UNDO","INTERNAL"}
node: bpy.props.StringProperty()
def invoke(self, context, event):
context.space_data.node_tree.nodes[self.node].recording = True
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
def assign_key(self,context,key_type):
if hasattr(context,"space_data") and context.space_data:
if hasattr(context.space_data,"node_tree"):
node = context.space_data.node_tree.nodes[self.node]
node.key = key_type
node.recording = False
context.area.tag_redraw()
def modal(self, context, event):
invalid = ["MOUSEMOVE","INBETWEEN_MOUSEMOVE","TRACKPADPAN","TRACKPADZOOM","MOUSEROTATE","MOUSESMARTZOOM",
"TEXTINPUT","WINDOW_DEACTIVATE","ACTIONZONE_AREA","ACTIONZONE_REGION","ACTIONZONE_FULLSCREEN"]
if not event.type in invalid and not "TIMER" in event.type:
self.assign_key(context,event.type)
return {"FINISHED"}
return {"RUNNING_MODAL"}
def sn_append_interface(self, context):
row = self.layout.row()
row.alert = True
op = row.operator("sn.select_interface_add",text="Select",icon="CHECKMARK")
op.idname = self.bl_idname
remove_interfaces = []
picker_node = None
class SN_OT_SelectInterfaceAdd(bpy.types.Operator):
bl_idname = "sn.select_interface_add"
bl_label = "Select"
bl_description = "Select this element in the interface"
bl_options = {"REGISTER","UNDO","INTERNAL"}
idname: bpy.props.StringProperty(options={"HIDDEN"})
def remove_registered_interfaces(self):
global remove_interfaces
for interface in remove_interfaces:
try:
interface.remove(sn_append_interface)
except:
pass
remove_interfaces.clear()
def execute(self, context):
global picker_node
if picker_node:
picker_node.picked = self.idname
picker_node = None
self.remove_registered_interfaces()
for area in context.screen.areas:
area.tag_redraw()
return {"FINISHED"}
class SN_OT_StartInterfacePicker(bpy.types.Operator):
bl_idname = "sn.pick_interface"
bl_label = "Select"
bl_description = "Select an element in the interface"
bl_options = {"REGISTER","UNDO","INTERNAL"}
node: bpy.props.StringProperty()
selection: bpy.props.EnumProperty(items=[("ALL","ALL","ALL"),
("PANELS","PANELS","PANELS"),
("MENUS","MENUS","MENUS")],
options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
global remove_interfaces
return len(remove_interfaces) == 0
def get_interfaces(self):
interfaces = []
for name in dir(bpy.types):
try:
interface = eval("bpy.types."+name)
if self.selection != "PANELS":
if hasattr(interface.bl_rna.base,"identifier") and interface.bl_rna.base.identifier == "Menu":
interfaces.append(name)
if self.selection != "MENUS":
if hasattr(interface.bl_rna.base,"identifier") and interface.bl_rna.base.identifier == "Panel":
interfaces.append(name)
except:
pass
return interfaces
def execute(self, context):
global picker_node
global remove_interfaces
picker_node = context.space_data.node_tree.nodes[self.node]
for interface in self.get_interfaces():
try:
eval("bpy.types."+interface+".append(sn_append_interface)")
remove_interfaces.append(eval("bpy.types."+interface))
except:
pass
for area in context.screen.areas:
area.tag_redraw()
return {"FINISHED"}
@@ -0,0 +1,46 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_AddonInfoNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddonInfoNode"
bl_label = "Addon Info"
node_color = "DEFAULT"
def on_create(self, context):
self.add_string_output("Name")
self.add_string_output("Description")
self.add_string_output("Author")
self.add_string_output("Location")
self.add_string_output("Warning")
self.add_string_output("Doc URL")
self.add_string_output("Tracker URL")
self.add_string_output("Category")
self.add_integer_vector_output("Version")
self.add_integer_vector_output("Blender Version")
def evaluate(self, context):
self.outputs[0].python_value = f"bpy.context.scene.sn.addon_name"
self.outputs[1].python_value = f"bpy.context.scene.sn.description"
self.outputs[2].python_value = f"bpy.context.scene.sn.author"
self.outputs[3].python_value = f"bpy.context.scene.sn.location"
self.outputs[4].python_value = f"bpy.context.scene.sn.warning"
self.outputs[5].python_value = f"bpy.context.scene.sn.doc_url"
self.outputs[6].python_value = f"bpy.context.scene.sn.tracker_url"
self.outputs[7].python_value = f"bpy.context.scene.sn.category"
self.outputs[8].python_value = f"tuple(bpy.context.scene.sn.version)"
self.outputs[9].python_value = f"tuple(bpy.context.scene.sn.blender)"
def evaluate_export(self, context):
self.outputs[0].python_value = f"'{bpy.context.scene.sn.addon_name}'"
self.outputs[1].python_value = f"'{bpy.context.scene.sn.description}'"
self.outputs[2].python_value = f"'{bpy.context.scene.sn.author}'"
self.outputs[3].python_value = f"'{bpy.context.scene.sn.location}'"
self.outputs[4].python_value = f"'{bpy.context.scene.sn.warning}'"
self.outputs[5].python_value = f"'{bpy.context.scene.sn.doc_url}'"
self.outputs[6].python_value = f"'{bpy.context.scene.sn.tracker_url}'"
self.outputs[7].python_value = f"'{bpy.context.scene.sn.category}'"
self.outputs[8].python_value = str(tuple(bpy.context.scene.sn.version))
self.outputs[9].python_value = str(tuple(bpy.context.scene.sn.blender))
@@ -0,0 +1,30 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_2DViewZoomNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_2DViewZoomNode"
bl_label = "2D View Zoom"
def on_create(self, context):
self.add_property_input("Area")
self.add_float_output("Zoom Level")
def evaluate(self, context):
self.code_imperative = """
def get_zoom_level(area):
ui_scale = bpy.context.preferences.system.ui_scale
for region in area.regions:
if region.type == "WINDOW":
test_length = 1000
x0, y0 = region.view2d.view_to_region(0, 0, clip=False)
x1, y1 = region.view2d.view_to_region(test_length, test_length, clip=False)
xl = x1 - x0
yl = y1 - y0
return (math.sqrt(xl**2 + yl**2) / test_length) * ui_scale
return 1 * ui_scale
"""
self.code_import = "import math"
self.outputs[0].python_value = f"get_zoom_level({self.inputs[0].python_value})"
@@ -0,0 +1,73 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AreaByTypeNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AreaByTypeNode"
bl_label = "Area By Type"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_input("Screen")
self.add_property_output("First Area")
self.add_property_output("Last Area")
self.add_property_output("Biggest Area")
self.add_list_output("All Areas")
self.add_boolean_output("Area Exists")
self.add_integer_output("Area Amount")
def area_items(self,context):
types = ["VIEW_3D", "IMAGE_EDITOR", "NODE_EDITOR", "SEQUENCE_EDITOR",
"CLIP_EDITOR", "DOPESHEET_EDITOR", "GRAPH_EDITOR", "NLA_EDITOR",
"TEXT_EDITOR", "CONSOLE", "INFO", "TOPBAR", "STATUSBAR", "OUTLINER",
"PROPERTIES", "FILE_BROWSER", "PREFERENCES"]
items = []
for a_type in types:
items.append((a_type,a_type.replace("_"," ").title(),a_type))
return items
area_type: bpy.props.EnumProperty(name="Area Type",
description="The type of area to find",
items=area_items,
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code_imperative = f"""
def find_areas_of_type(screen, area_type):
areas = []
for area in screen.areas:
if area.type == area_type:
areas.append(area)
return areas
def find_area_by_type(screen, area_type, index):
areas = find_areas_of_type(screen, area_type)
if areas:
return areas[index]
return None
def find_biggest_area_by_type(screen, area_type):
areas = find_areas_of_type(screen, area_type)
if not areas: return []
max_area = (areas[0], areas[0].width * areas[0].height)
for area in areas:
if area.width * area.height > max_area[1]:
max_area = (area, area.width * area.height)
return max_area[0]
"""
screen = "bpy.context.screen" if not "Screen" in self.inputs else self.inputs["Screen"].python_value
self.outputs["First Area"].python_value = f"find_area_by_type({screen}, '{self.area_type}', 0)"
self.outputs["Last Area"].python_value = f"find_area_by_type({screen}, '{self.area_type}', -1)"
self.outputs["Biggest Area"].python_value = f"find_biggest_area_by_type({screen}, '{self.area_type}')"
self.outputs["All Areas"].python_value = f"find_areas_of_type({screen}, '{self.area_type}')"
self.outputs["Area Exists"].python_value = f"bool(find_areas_of_type({screen}, '{self.area_type}'))"
self.outputs["Area Amount"].python_value = f"len(find_areas_of_type({screen}, '{self.area_type}'))"
def draw_node(self, context, layout):
layout.prop(self, "area_type", text="")
@@ -0,0 +1,41 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AreaLocationsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AreaLocationsNode"
bl_label = "Area Locations"
node_color = "DEFAULT"
def on_create(self, context):
self.add_property_input("Area")
self.add_integer_vector_output("Top Left").size = 2
self.add_integer_vector_output("Top Center").size = 2
self.add_integer_vector_output("Top Right").size = 2
self.add_integer_vector_output("Bottom Left").size = 2
self.add_integer_vector_output("Bottom Center").size = 2
self.add_integer_vector_output("Bottom Right").size = 2
self.add_integer_vector_output("Left Center").size = 2
self.add_integer_vector_output("Right Center").size = 2
self.add_integer_vector_output("Center").size = 2
def evaluate(self, context):
self.code_imperative = """
def region_by_type(area, region_type):
for region in area.regions:
if region.type == region_type:
return region
return area.regions[0]
"""
self.outputs["Top Left"].python_value = f"(0, region_by_type({self.inputs['Area'].python_value}, 'WINDOW').height)"
self.outputs["Top Center"].python_value = f"(region_by_type({self.inputs['Area'].python_value}, 'WINDOW').width/2, region_by_type({self.inputs['Area'].python_value}, 'WINDOW').height)"
self.outputs["Top Right"].python_value = f"(region_by_type({self.inputs['Area'].python_value}, 'WINDOW').width, region_by_type({self.inputs['Area'].python_value}, 'WINDOW').height)"
self.outputs["Bottom Left"].python_value = f"(0, 0)"
self.outputs["Bottom Center"].python_value = f"(region_by_type({self.inputs['Area'].python_value}, 'WINDOW').width/2, 0)"
self.outputs["Bottom Right"].python_value = f"(region_by_type({self.inputs['Area'].python_value}, 'WINDOW').width, 0)"
self.outputs["Left Center"].python_value = f"(0, region_by_type({self.inputs['Area'].python_value}, 'WINDOW').height/2)"
self.outputs["Right Center"].python_value = f"(region_by_type({self.inputs['Area'].python_value}, 'WINDOW').width, region_by_type({self.inputs['Area'].python_value}, 'WINDOW').height/2)"
self.outputs["Center"].python_value = f"(region_by_type({self.inputs['Area'].python_value}, 'WINDOW').width/2, region_by_type({self.inputs['Area'].python_value}, 'WINDOW').height/2)"
@@ -0,0 +1,38 @@
import bpy
import os
from ..base_node import SN_ScriptingBaseNode
class SN_AssetNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AssetNode"
bl_label = "Asset"
node_color = "STRING"
def on_create(self, context):
self.add_string_output("Path").subtype = "FILE_PATH"
asset: bpy.props.StringProperty(name="Asset",
description="Asset to get the path from",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.asset and self.asset in context.scene.sn.assets:
if context.scene.sn.assets[self.asset].path:
self.outputs["Path"].python_value = f"r\'{context.scene.sn.assets[self.asset].path}\'"
return
self.outputs["Path"].python_value = "\'\'"
def evaluate_export(self, context):
if self.asset and self.asset in context.scene.sn.assets:
if context.scene.sn.assets[self.asset].path:
self.code_import = "import os"
name = os.path.basename(context.scene.sn.assets[self.asset].path)
if not name: name = os.path.basename(os.path.dirname(context.scene.sn.assets[self.asset].path))
self.outputs["Path"].python_value = f"os.path.join(os.path.dirname(__file__), 'assets', '{name}')"
return
self.outputs["Path"].python_value = "\'\'"
def draw_node(self, context, layout):
layout.prop_search(self, "asset", context.scene.sn, "assets")
@@ -0,0 +1,274 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from .blend_data_base import BlendDataBaseNode
class SN_MaterialsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_MaterialsBlendDataNode"
bl_label = "Materials"
data_type = "Material"
data_type_plural = "Materials"
active_path = "bpy.context.object.active_material"
data_path = "bpy.data.materials"
class SN_MetaballsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_MetaballsBlendDataNode"
bl_label = "Metaballs"
data_type = "Metaball"
data_type_plural = "Metaballs"
data_path = "bpy.data.metaballs"
class SN_CurvesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_CurvesBlendDataNode"
bl_label = "Curves"
data_type = "Curve"
data_type_plural = "Curves"
data_path = "bpy.data.curves"
class SN_ActionsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_ActionsBlendDataNode"
bl_label = "Actions"
data_type = "Action"
data_type_plural = "Actions"
data_path = "bpy.data.actions"
class SN_ArmaturesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_ArmaturesBlendDataNode"
bl_label = "Armatures"
data_type = "Armature"
data_type_plural = "Armatures"
data_path = "bpy.data.armatures"
class SN_ImagesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_ImagesBlendDataNode"
bl_label = "Images"
data_type = "Image"
data_type_plural = "Images"
data_path = "bpy.data.images"
class SN_LightsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_LightsBlendDataNode"
bl_label = "Lights"
data_type = "Light"
data_type_plural = "Lights"
data_path = "bpy.data.lights"
class SN_NodeGroupsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_NodeGroupsBlendDataNode"
bl_label = "Node Groups"
data_type = "Node Group"
data_type_plural = "Node Groups"
data_path = "bpy.data.node_groups"
class SN_TextsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_TextsBlendDataNode"
bl_label = "Texts"
data_type = "Text"
data_type_plural = "Texts"
data_path = "bpy.data.texts"
class SN_TexturesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_TexturesBlendDataNode"
bl_label = "Textures"
data_type = "Texture"
data_type_plural = "Textures"
data_path = "bpy.data.textures"
class SN_WorkspacesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_WorkspacesBlendDataNode"
bl_label = "Workspaces"
data_type = "Workspace"
data_type_plural = "Workspaces"
data_path = "bpy.data.workspaces"
active_path = "bpy.context.workspace"
class SN_WorldsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_WorldsBlendDataNode"
bl_label = "Worlds"
data_type = "World"
data_type_plural = "Worlds"
data_path = "bpy.data.worlds"
active_path = "bpy.context.scene.world"
class SN_BrushesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_BrushesBlendDataNode"
bl_label = "Brushes"
data_type = "Brush"
data_type_plural = "Brushes"
data_path = "bpy.data.brushes"
class SN_CamerasBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_CamerasBlendDataNode"
bl_label = "Cameras"
data_type = "Camera"
data_type_plural = "Cameras"
data_path = "bpy.data.cameras"
active_path = "bpy.context.scene.camera.data"
class SN_FontsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_FontsBlendDataNode"
bl_label = "Fonts"
data_type = "Font"
data_type_plural = "Fonts"
data_path = "bpy.data.fonts"
class SN_GreasePencilsBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_GreasePencilsBlendDataNode"
bl_label = "Grease Pencils"
data_type = "Grease Pencil"
data_type_plural = "Grease Pencils"
data_path = "bpy.data.grease_pencils"
class SN_LatticesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_LatticesBlendDataNode"
bl_label = "Lattices"
data_type = "Lattice"
data_type_plural = "Lattices"
data_path = "bpy.data.lattices"
class SN_ScenesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_ScenesBlendDataNode"
bl_label = "Scenes"
data_type = "Scene"
data_type_plural = "Scenes"
data_path = "bpy.data.scenes"
active_path = "bpy.context.scene"
class SN_ScreensBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_ScreensBlendDataNode"
bl_label = "Screens"
data_type = "Screen"
data_type_plural = "Screens"
data_path = "bpy.data.screens"
active_path = "bpy.context.screen"
class SN_ShapeKeysBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_ShapeKeysBlendDataNode"
bl_label = "Shape Keys"
data_type = "Shape Key"
data_type_plural = "Shape Keys"
data_path = "bpy.data.shape_keys"
class SN_VolumesBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_VolumesBlendDataNode"
bl_label = "Volumes"
data_type = "Volume"
data_type_plural = "Volumes"
data_path = "bpy.data.volumes"
class SN_WindowManagersBlendDataNode(bpy.types.Node, BlendDataBaseNode, SN_ScriptingBaseNode):
bl_idname = "SN_WindowManagersBlendDataNode"
bl_label = "Window Managers"
data_type = "Window Manager"
data_type_plural = "Window Managers"
data_path = "bpy.data.window_managers"
active_path = "bpy.context.window_manager"
@@ -0,0 +1,22 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BlenderDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BlenderDataNode"
bl_label = "Blender Data"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_output("Current Context")
self.add_property_output("Blend Data")
self.add_property_output("App")
self.add_property_output("Path")
def evaluate(self, context):
self.outputs["Current Context"].python_value = "bpy.context"
self.outputs["Blend Data"].python_value = "bpy.data"
self.outputs["App"].python_value = "bpy.app"
self.outputs["Path"].python_value = "bpy.path"
@@ -0,0 +1,39 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CollectionsBlendDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CollectionsBlendDataNode"
bl_label = "Collections"
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_output("All Collections")
self.add_collection_property_output("Scene Collections")
self.add_property_output("Scene Collection")
self.add_property_output("Active Collection")
self.add_property_output("Indexed")
self.add_integer_input("Index")
def update_index_type(self, context):
inp = self.convert_socket(self.inputs[0], self.socket_names[self.index_type])
inp.name = "Index" if self.index_type == "Integer" else "Name"
self._evaluate(context)
index_type: bpy.props.EnumProperty(name="Index Type",
description="The type of index to use",
items=[("Integer", "Index", "Starts at 0. Negative indices go to the back of the list."),
("String", "Name", "Refers to the name property of the element.")],
update=update_index_type)
def evaluate(self, context):
self.outputs["All Collections"].python_value = f"bpy.data.collections"
self.outputs["Scene Collections"].python_value = f"bpy.context.scene.collection.children"
self.outputs["Scene Collection"].python_value = f"bpy.context.scene.collection"
self.outputs["Active Collection"].python_value = f"bpy.context.collection"
self.outputs["Indexed"].python_value = f"bpy.data.collections[{self.inputs[0].python_value}]"
def draw_node(self, context, layout):
layout.prop(self, "index_type", expand=True)
@@ -0,0 +1,37 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_MeshBlendDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_MeshBlendDataNode"
bl_label = "Meshes"
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_output("All Meshes")
self.add_list_output("Active Scene Meshes")
self.add_property_output("Active Object Mesh")
self.add_property_output("Indexed")
self.add_integer_input("Index")
def update_index_type(self, context):
inp = self.convert_socket(self.inputs[0], self.socket_names[self.index_type])
inp.name = "Index" if self.index_type == "Integer" else "Name"
self._evaluate(context)
index_type: bpy.props.EnumProperty(name="Index Type",
description="The type of index to use",
items=[("Integer", "Index", "Starts at 0. Negative indices go to the back of the list."),
("String", "Name", "Refers to the name property of the element.")],
update=update_index_type)
def evaluate(self, context):
self.outputs["All Meshes"].python_value = f"bpy.data.meshes"
self.outputs["Active Scene Meshes"].python_value = f"list(filter(lambda obj: obj, [obj.data if obj.type == 'MESH' else None for obj in bpy.context.scene.objects]))"
self.outputs["Active Object Mesh"].python_value = f"(bpy.context.active_object.data if bpy.context.active_object.type == 'MESH' else None)"
self.outputs["Indexed"].python_value = f"bpy.data.meshes[{self.inputs[0].python_value}]"
def draw_node(self, context, layout):
layout.prop(self, "index_type", expand=True)
@@ -0,0 +1,39 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ObjectBlendDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ObjectBlendDataNode"
bl_label = "Objects"
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_output("All Objects")
self.add_collection_property_output("Active Scene Objects")
self.add_collection_property_output("Selected Objects")
self.add_property_output("Active Object")
self.add_property_output("Indexed")
self.add_integer_input("Index")
def update_index_type(self, context):
inp = self.convert_socket(self.inputs[0], self.socket_names[self.index_type])
inp.name = "Index" if self.index_type == "Integer" else "Name"
self._evaluate(context)
index_type: bpy.props.EnumProperty(name="Index Type",
description="The type of index to use",
items=[("Integer", "Index", "Starts at 0. Negative indices go to the back of the list."),
("String", "Name", "Refers to the name property of the element.")],
update=update_index_type)
def evaluate(self, context):
self.outputs["All Objects"].python_value = f"bpy.data.objects"
self.outputs["Active Scene Objects"].python_value = f"bpy.context.scene.objects"
self.outputs["Selected Objects"].python_value = f"bpy.context.view_layer.objects.selected"
self.outputs["Active Object"].python_value = f"bpy.context.view_layer.objects.active"
self.outputs["Indexed"].python_value = f"bpy.data.objects[{self.inputs[0].python_value}]"
def draw_node(self, context, layout):
layout.prop(self, "index_type", expand=True)
@@ -0,0 +1,38 @@
import bpy
class BlendDataBaseNode():
data_type = ""
data_type_plural = ""
active_path = ""
data_path = ""
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_output(f"All {self.data_type_plural}")
if self.active_path: self.add_property_output(f"Active {self.data_type}")
self.add_property_output("Indexed")
self.add_integer_input("Index")
def update_index_type(self, context):
inp = self.convert_socket(self.inputs[0], self.socket_names[self.index_type])
inp.name = "Index" if self.index_type == "Integer" else "Name"
self._evaluate(context)
index_type: bpy.props.EnumProperty(name="Index Type",
description="The type of index to use",
items=[("Integer", "Index", "Starts at 0. Negative indices go to the back of the list."),
("String", "Name", "Refers to the name property of the element.")],
update=update_index_type)
def evaluate(self, context):
self.outputs[f"All {self.data_type_plural}"].python_value = self.data_path
if self.active_path: self.outputs[f"Active {self.data_type}"].python_value = self.active_path
self.outputs["Indexed"].python_value = f"{self.data_path}[{self.inputs[0].python_value}]"
def draw_node(self, context, layout):
layout.prop(self, "index_type", expand=True)
@@ -0,0 +1,21 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BooleanVectorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BooleanVectorNode"
bl_label = "Boolean Vector"
node_color = "VECTOR"
def on_create(self, context):
self.add_boolean_vector_input("Boolean").set_hide(True)
self.add_boolean_vector_output("Boolean")
def evaluate(self, context):
self.outputs["Boolean"].python_value = self.inputs["Boolean"].python_value
def draw_node(self, context, layout):
layout.prop(self.inputs["Boolean"], "size")
self.inputs["Boolean"].draw_socket(context, layout, self, "Value")
@@ -0,0 +1,20 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BooleanNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BooleanNode"
bl_label = "Boolean"
node_color = "BOOLEAN"
def on_create(self, context):
self.add_boolean_input("Boolean").set_hide(True)
self.add_boolean_output("Boolean")
def evaluate(self, context):
self.outputs["Boolean"].python_value = self.inputs["Boolean"].python_value
def draw_node(self, context, layout):
layout.prop(self.inputs["Boolean"], "default_value", text="Value")
@@ -0,0 +1,35 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ColorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ColorNode"
bl_label = "Color"
node_color = "VECTOR"
def update_size(self, context):
if self.use_four:
self.inputs[0].subtype = "COLOR_ALPHA"
self.outputs[0].subtype = "COLOR_ALPHA"
else:
self.inputs[0].subtype = "COLOR"
self.outputs[0].subtype = "COLOR"
use_four: bpy.props.BoolProperty(default=False,
name="Use Alpha",
update=update_size)
def on_create(self, context):
socket = self.add_float_vector_input("Color")
socket.set_hide(True)
socket.subtype = "COLOR"
self.add_float_vector_output("Color").subtype = "COLOR"
def evaluate(self, context):
self.outputs["Color"].python_value = self.inputs["Color"].python_value
def draw_node(self, context, layout):
layout.prop(self, "use_four")
self.inputs["Color"].draw_socket(context, layout, self, "")
@@ -0,0 +1,21 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_FloatVectorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_FloatVectorNode"
bl_label = "Float Vector"
node_color = "VECTOR"
def on_create(self, context):
self.add_float_vector_input("Float").set_hide(True)
self.add_float_vector_output("Float")
def evaluate(self, context):
self.outputs["Float"].python_value = self.inputs["Float"].python_value
def draw_node(self, context, layout):
layout.prop(self.inputs["Float"], "size")
self.inputs["Float"].draw_socket(context, layout, self, "Value")
@@ -0,0 +1,20 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_FloatNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_FloatNode"
bl_label = "Float"
node_color = "FLOAT"
def on_create(self, context):
self.add_float_input("Float").set_hide(True)
self.add_float_output("Float")
def evaluate(self, context):
self.outputs["Float"].python_value = self.inputs["Float"].python_value
def draw_node(self, context, layout):
layout.prop(self.inputs["Float"], "default_value", text="")
@@ -0,0 +1,21 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IntegerVectorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IntegerVectorNode"
bl_label = "Integer Vector"
node_color = "VECTOR"
def on_create(self, context):
self.add_integer_vector_input("Integer").set_hide(True)
self.add_integer_vector_output("Integer")
def evaluate(self, context):
self.outputs["Integer"].python_value = self.inputs["Integer"].python_value
def draw_node(self, context, layout):
layout.prop(self.inputs["Integer"], "size")
self.inputs["Integer"].draw_socket(context, layout, self, "Value")
@@ -0,0 +1,20 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IntegerNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IntegerNode"
bl_label = "Integer"
node_color = "INTEGER"
def on_create(self, context):
self.add_integer_input("Integer").set_hide(True)
self.add_integer_output("Integer")
def evaluate(self, context):
self.outputs["Integer"].python_value = self.inputs["Integer"].python_value
def draw_node(self, context, layout):
layout.prop(self.inputs["Integer"], "default_value", text="")
@@ -0,0 +1,19 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ListNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ListNode"
bl_label = "List"
node_color = "LIST"
def on_create(self, context):
self.add_dynamic_data_input("Data")
self.add_list_output("List")
def evaluate(self, context):
items = [inp.python_value if inp.is_linked else None for inp in self.inputs[:-1]]
items = filter(lambda item: item != None, items)
self.outputs["List"].python_value = f"[{', '.join(items)}]"
@@ -0,0 +1,12 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_NoneNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_NoneNode"
bl_label = "None"
def on_create(self, context):
self.add_data_output("None")
@@ -0,0 +1,20 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_StringNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_StringNode"
bl_label = "String"
node_color = "STRING"
def on_create(self, context):
self.add_string_input("String").set_hide(True)
self.add_string_output("String")
def evaluate(self, context):
self.outputs["String"].python_value = self.inputs["String"].python_value
def draw_node(self, context, layout):
layout.prop(self.inputs["String"], "default_value", text="")
@@ -0,0 +1,26 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BMeshDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BMeshDataNode"
bl_label = "BMesh Object Data"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_input("BMesh")
self.add_collection_property_output("Vertices")
self.add_collection_property_output("Faces")
self.add_collection_property_output("Edges")
def evaluate(self, context):
if self.inputs["BMesh"].is_linked:
self.outputs["Vertices"].python_value = f"{self.inputs['BMesh'].python_value}.verts"
self.outputs["Faces"].python_value = f"{self.inputs['BMesh'].python_value}.faces"
self.outputs["Edges"].python_value = f"{self.inputs['BMesh'].python_value}.edges"
else:
self.outputs["Vertices"].reset_value()
self.outputs["Faces"].reset_value()
self.outputs["Edges"].reset_value()
@@ -0,0 +1,58 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BMeshEdgeDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BMeshEdgeDataNode"
bl_label = "BMesh Edge Data"
node_color = "PROPERTY"
def update_with_transforms(self, context):
self.inputs["Object"].set_hide(not self.with_transforms)
self._evaluate(context)
def on_create(self, context):
self.add_property_input("BMesh Edge")
self.add_property_input("Object").set_hide(True)
self.add_collection_property_output("Vertices")
self.add_collection_property_output("Faces")
self.add_boolean_output("Selected")
self.add_boolean_output("Hidden")
self.add_boolean_output("Smooth")
self.add_integer_output("Index")
self.add_boolean_output("Is Manifold")
self.add_boolean_output("Is Boundary")
self.add_boolean_output("Is Contiguous")
self.add_boolean_output("Is Convex")
self.add_boolean_output("Is Wire")
self.add_boolean_output("Is Seam")
def evaluate(self, context):
if self.inputs["BMesh Edge"].is_linked:
self.outputs["Faces"].python_value = f"{self.inputs['BMesh Edge'].python_value}.link_faces"
self.outputs["Vertices"].python_value = f"{self.inputs['BMesh Edge'].python_value}.verts"
self.outputs["Is Boundary"].python_value = f"{self.inputs['BMesh Edge'].python_value}.is_boundary"
self.outputs["Is Contiguous"].python_value = f"{self.inputs['BMesh Edge'].python_value}.is_contiguous"
self.outputs["Is Convex"].python_value = f"{self.inputs['BMesh Edge'].python_value}.is_convex"
self.outputs["Is Wire"].python_value = f"{self.inputs['BMesh Edge'].python_value}.is_wire"
self.outputs["Is Seam"].python_value = f"{self.inputs['BMesh Edge'].python_value}.seam"
self.outputs["Selected"].python_value = f"{self.inputs['BMesh Edge'].python_value}.select"
self.outputs["Hidden"].python_value = f"{self.inputs['BMesh Edge'].python_value}.hide"
self.outputs["Smooth"].python_value = f"{self.inputs['BMesh Edge'].python_value}.smooth"
self.outputs["Index"].python_value = f"{self.inputs['BMesh Edge'].python_value}.index"
self.outputs["Is Manifold"].python_value = f"{self.inputs['BMesh Edge'].python_value}.is_manifold"
else:
self.outputs["Index"].reset_value()
self.outputs["Selected"].reset_value()
self.outputs["Hidden"].reset_value()
self.outputs["Smooth"].reset_value()
self.outputs["Is Boundary"].reset_value()
self.outputs["Is Contiguous"].reset_value()
self.outputs["Is Convex"].reset_value()
self.outputs["Is Wire"].reset_value()
self.outputs["Is Seam"].reset_value()
self.outputs["Vertices"].reset_value()
self.outputs["Faces"].reset_value()
self.outputs["Is Manifold"].reset_value()
@@ -0,0 +1,48 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BMeshFaceDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BMeshFaceDataNode"
bl_label = "BMesh Face Data"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_input("BMesh Face")
self.add_property_input("Object").set_hide(True)
self.add_collection_property_output("Edges")
self.add_collection_property_output("Vertices")
self.add_float_vector_output("Center")
self.add_float_vector_output("Center Weighted")
self.add_float_vector_output("Normal")
self.add_boolean_output("Selected")
self.add_boolean_output("Hidden")
self.add_boolean_output("Smooth")
self.add_integer_output("Index")
self.add_integer_output("Material Index")
def evaluate(self, context):
if self.inputs["BMesh Face"].is_linked:
self.outputs["Edges"].python_value = f"{self.inputs['BMesh Face'].python_value}.edges"
self.outputs["Vertices"].python_value = f"{self.inputs['BMesh Face'].python_value}.verts"
self.outputs["Center"].python_value = f"{self.inputs['BMesh Face'].python_value}.calc_center_median()"
self.outputs["Center Weighted"].python_value = f"{self.inputs['BMesh Face'].python_value}.calc_center_median_weighted()"
self.outputs["Normal"].python_value = f"{self.inputs['BMesh Face'].python_value}.normal"
self.outputs["Selected"].python_value = f"{self.inputs['BMesh Face'].python_value}.select"
self.outputs["Hidden"].python_value = f"{self.inputs['BMesh Face'].python_value}.hide"
self.outputs["Smooth"].python_value = f"{self.inputs['BMesh Face'].python_value}.smooth"
self.outputs["Index"].python_value = f"{self.inputs['BMesh Face'].python_value}.index"
self.outputs["Material Index"].python_value = f"{self.inputs['BMesh Face'].python_value}.material_index"
else:
self.outputs["Normal"].reset_value()
self.outputs["Index"].reset_value()
self.outputs["Selected"].reset_value()
self.outputs["Hidden"].reset_value()
self.outputs["Center"].reset_value()
self.outputs["Center Weighted"].reset_value()
self.outputs["Smooth"].reset_value()
self.outputs["Edges"].reset_value()
self.outputs["Vertices"].reset_value()
self.outputs["Material Index"].reset_value()
@@ -0,0 +1,47 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BMeshVertexDataNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BMeshVertexDataNode"
bl_label = "BMesh Vertex Data"
node_color = "PROPERTY"
def update_with_transforms(self, context):
self.inputs["Object"].set_hide(not self.with_transforms)
self._evaluate(context)
def on_create(self, context):
self.add_property_input("BMesh Vertex")
self.add_property_input("Object").set_hide(True)
self.add_float_vector_output("Location")
self.add_float_vector_output("Normal")
self.add_boolean_output("Selected")
self.add_boolean_output("Hidden")
self.add_integer_output("Index")
self.add_boolean_output("Is Boundary")
self.add_boolean_output("Is Manifold")
self.add_boolean_output("Is Wire")
def evaluate(self, context):
if self.inputs["BMesh Vertex"].is_linked:
self.outputs["Location"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.co"
self.outputs["Normal"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.normal"
self.outputs["Selected"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.select"
self.outputs["Hidden"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.hide"
self.outputs["Index"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.index"
self.outputs["Is Boundary"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.is_boundary"
self.outputs["Is Manifold"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.is_manifold"
self.outputs["Is Wire"].python_value = f"{self.inputs['BMesh Vertex'].python_value}.is_wire"
else:
self.outputs["Location"].reset_value()
self.outputs["Normal"].reset_value()
self.outputs["Index"].reset_value()
self.outputs["Selected"].reset_value()
self.outputs["Hidden"].reset_value()
self.outputs["Index"].reset_value()
self.outputs["Is Boundary"].reset_value()
self.outputs["Is Manifold"].reset_value()
self.outputs["Is Wire"].reset_value()
@@ -0,0 +1,45 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CreateLineLocationsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CreateLineLocationsNode"
bl_label = "Create Line"
node_color = "PROGRAM"
def update_use3d(self, context):
for input in self.inputs:
if input.bl_label == "Float Vector" and input.subtype == "NONE":
input.size = 3 if self.use_3d else 2
self._evaluate(context)
use_3d: bpy.props.BoolProperty(name="Use 3D",
description="Whether to use 3D or 2D coordinates",
default=False,
update=update_use3d)
def on_create(self, context):
inp = self.add_float_vector_input("Point 1")
inp.size = 2
inp.default_value[0] = 0
inp.default_value[1] = 0
inp.default_value[2] = 0
inp = self.add_float_vector_input("Point 2")
inp.size = 2
inp.default_value[0] = 0
inp.default_value[1] = 1
inp.default_value[2] = 1
self.add_list_output("Line")
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
def evaluate(self, context):
p1 = self.inputs["Point 1"].python_value
p2 = self.inputs["Point 2"].python_value
self.outputs[0].python_value = f"[{p1}, {p2}]"
@@ -0,0 +1,59 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CreateQuadLocationsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CreateQuadLocationsNode"
bl_label = "Create Quad"
node_color = "PROGRAM"
def update_use3d(self, context):
for input in self.inputs:
if input.bl_label == "Float Vector" and input.subtype == "NONE":
input.size = 3 if self.use_3d else 2
self._evaluate(context)
use_3d: bpy.props.BoolProperty(name="Use 3D",
description="Whether to use 3D or 2D coordinates",
default=False,
update=update_use3d)
def on_create(self, context):
inp = self.add_float_vector_input("Bottom Left")
inp.size = 2
inp.default_value[0] = 0
inp.default_value[1] = 0
inp.default_value[2] = 0
inp = self.add_float_vector_input("Bottom Right")
inp.size = 2
inp.default_value[0] = 1
inp.default_value[1] = 0
inp.default_value[2] = 0
inp = self.add_float_vector_input("Top Right")
inp.size = 2
inp.default_value[0] = 1
inp.default_value[1] = 1
inp.default_value[2] = 1
inp = self.add_float_vector_input("Top Left")
inp.size = 2
inp.default_value[0] = 0
inp.default_value[1] = 1
inp.default_value[2] = 1
self.add_list_output("Quad")
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
def evaluate(self, context):
bl = self.inputs["Bottom Left"].python_value
br = self.inputs["Bottom Right"].python_value
tl = self.inputs["Top Left"].python_value
tr = self.inputs["Top Right"].python_value
self.outputs[0].python_value = f"[{bl}, {br}, {tl}, {tr}]"
@@ -0,0 +1,52 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CreateTriangleLocationsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CreateTriangleLocationsNode"
bl_label = "Create Triangle"
node_color = "PROGRAM"
def update_use3d(self, context):
for input in self.inputs:
if input.bl_label == "Float Vector" and input.subtype == "NONE":
input.size = 3 if self.use_3d else 2
self._evaluate(context)
use_3d: bpy.props.BoolProperty(name="Use 3D",
description="Whether to use 3D or 2D coordinates",
default=False,
update=update_use3d)
def on_create(self, context):
inp = self.add_float_vector_input("Corner 1")
inp.size = 2
inp.default_value[0] = 0
inp.default_value[1] = 0
inp.default_value[2] = 0
inp = self.add_float_vector_input("Corner 2")
inp.size = 2
inp.default_value[0] = 1
inp.default_value[1] = 0
inp.default_value[2] = 0
inp = self.add_float_vector_input("Corner 3")
inp.size = 2
inp.default_value[0] = 1
inp.default_value[1] = 1
inp.default_value[2] = 1
self.add_list_output("Triangle")
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
def evaluate(self, context):
c1 = self.inputs["Corner 1"].python_value
c2 = self.inputs["Corner 2"].python_value
c3 = self.inputs["Corner 3"].python_value
self.outputs[0].python_value = f"[{c1}, {c2}, {c3}]"
@@ -0,0 +1,46 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_FacesToVertexLocationsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_FacesToVertexLocationsNode"
bl_label = "BMesh Faces To Vertex Locations"
node_color = "PROPERTY"
def on_create(self, context):
self.add_collection_property_input("Faces")
self.add_list_output("All Locations")
self.add_list_output("Quad Locations Only")
self.add_list_output("Triangle Locations Only")
self.add_list_output("Ngon Locations Only")
def evaluate(self, context):
self.code_imperative = f"""
def get_bmesh_vertex_locations(faces):
locations = []
for face in faces:
face_locations = []
for vert in face.verts:
face_locations.append(vert.co)
locations.append(face_locations)
return locations
def get_bmesh_vertex_locations_quads(faces):
locations = get_bmesh_vertex_locations(faces)
return [loc for loc in locations if len(loc) == 4]
def get_bmesh_vertex_locations_triangles(faces):
locations = get_bmesh_vertex_locations(faces)
return [loc for loc in locations if len(loc) == 3]
def get_bmesh_vertex_locations_ngons(faces):
locations = get_bmesh_vertex_locations(faces)
return [loc for loc in locations if len(loc) > 4]
"""
self.outputs["All Locations"].python_value = f"get_bmesh_vertex_locations({self.inputs['Faces'].python_value})"
self.outputs["Quad Locations Only"].python_value = f"get_bmesh_vertex_locations_quads({self.inputs['Faces'].python_value})"
self.outputs["Triangle Locations Only"].python_value = f"get_bmesh_vertex_locations_triangles({self.inputs['Faces'].python_value})"
self.outputs["Ngon Locations Only"].python_value = f"get_bmesh_vertex_locations_ngons({self.inputs['Faces'].python_value})"
@@ -0,0 +1,43 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ObjectToBMeshNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ObjectToBMeshNode"
bl_label = "Object To BMesh"
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_property_input("Object")
self.add_boolean_input("Use Edit Mode").default_value = True
self.add_boolean_input("Use Evaluated Mesh")
self.add_boolean_input("Use Object Transforms").default_value = True
self.add_execute_output()
self.add_property_output("BMesh")
def evaluate(self, context):
self.code_import = f"import bmesh"
self.code = f"""
bm_{self.static_uid} = bmesh.new()
if {self.inputs["Object"].python_value}:
if {self.inputs["Object"].python_value}.mode == 'EDIT' and {self.inputs["Use Edit Mode"].python_value}:
bm_{self.static_uid} = bmesh.from_edit_mesh({self.inputs["Object"].python_value}.data)
else:
if {self.inputs["Use Evaluated Mesh"].python_value}:
dg = bpy.context.evaluated_depsgraph_get()
bm_{self.static_uid}.from_mesh({self.inputs["Object"].python_value}.evaluated_get(dg).to_mesh())
else:
bm_{self.static_uid}.from_mesh({self.inputs["Object"].python_value}.data)
if {self.inputs["Use Object Transforms"].python_value}:
bm_{self.static_uid}.transform({self.inputs["Object"].python_value}.matrix_world)
bm_{self.static_uid}.verts.ensure_lookup_table()
bm_{self.static_uid}.faces.ensure_lookup_table()
bm_{self.static_uid}.edges.ensure_lookup_table()
{self.indent(self.outputs[0].python_value, 3)}
"""
self.outputs["BMesh"].python_value = f"bm_{self.static_uid}"
@@ -0,0 +1,34 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_NgonToTriangleLocationsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_NgonToTriangleLocationsNode"
bl_label = "Ngon To Triangle Locations"
node_color = "PROPERTY"
def on_create(self, context):
self.add_property_input("Ngon")
self.add_list_output("Triangle Locations")
def evaluate(self, context):
self.code_imperative = """
def ngon_to_triangle_locations(ngon):
bm = bmesh.new()
for v in ngon.verts:
bm.verts.new(v.co)
bm.faces.new(bm.verts)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.verts.ensure_lookup_table()
new_faces = []
for f in bm.faces:
vert_locations = []
for v in f.verts:
vert_locations.append(tuple(v.co))
new_faces.append(vert_locations)
return new_faces
"""
self.outputs["Triangle Locations"].python_value = f"ngon_to_triangle_locations({self.inputs['Ngon'].python_value})"
@@ -0,0 +1,28 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_TriangulateBmeshNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_TriangulateBmeshNode"
bl_label = "Triangulate BMesh"
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_property_input("BMesh")
self.add_execute_output()
self.add_property_output("BMesh")
def evaluate(self, context):
self.code = f"""
bm_{self.static_uid} = {self.inputs["BMesh"].python_value}.copy()
bmesh.ops.triangulate(bm_{self.static_uid}, faces=bm_{self.static_uid}.faces)
bm_{self.static_uid}.verts.ensure_lookup_table()
bm_{self.static_uid}.faces.ensure_lookup_table()
bm_{self.static_uid}.edges.ensure_lookup_table()
{self.indent(self.outputs[0].python_value, 3)}
"""
self.outputs["BMesh"].python_value = f"bm_{self.static_uid}"
@@ -0,0 +1,20 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_GetEditSelectNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_GetEditSelectNode"
bl_label = "Get Edit Select Mode"
node_color = "BOOLEAN"
def on_create(self, context):
self.add_boolean_output("Vertex")
self.add_boolean_output("Edge")
self.add_boolean_output("Face")
def evaluate(self, context):
self.outputs[0].python_value = f"bpy.context.tool_settings.mesh_select_mode[0]"
self.outputs[1].python_value = f"bpy.context.tool_settings.mesh_select_mode[1]"
self.outputs[2].python_value = f"bpy.context.tool_settings.mesh_select_mode[2]"
@@ -0,0 +1,111 @@
import bpy
import os
from ..base_node import SN_ScriptingBaseNode
class SN_IconNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IconNode"
bl_label = "Icon"
node_color = "ICON"
bl_width_default = 200
def on_create(self, context):
inp = self.add_string_input("Image Path")
inp.subtype = "FILE_PATH"
inp.set_hide(True)
self.add_icon_output("Icon")
def update_icon_source(self, context):
if self.icon_source == "PATH":
self.inputs[0].set_hide(False)
else:
self.inputs[0].set_hide(True)
self._evaluate(context)
icon_source: bpy.props.EnumProperty(name="Icon Source",
description="The source of the icons",
items=[("BLENDER","Blender","Blender",0),
("CUSTOM","Image","Image",1),
("PATH","Path","Path",2)],
update=update_icon_source)
icon: bpy.props.IntProperty(name="Value", description="Value of this socket", update=SN_ScriptingBaseNode._evaluate)
def update_icon_file(self, context):
if self.icon_file:
self.icon_file.use_fake_user = True
self.icon_file.preview_ensure()
self._evaluate(context)
icon_file: bpy.props.PointerProperty(type=bpy.types.Image,
name="Image File",
description="The image you want to use as an icon",
update=update_icon_file)
def evaluate(self, context):
if self.icon_source == "BLENDER":
self.outputs["Icon"].python_value = f"{self.icon}"
elif self.icon_source == "CUSTOM":
if self.icon_file:
self.outputs["Icon"].python_value = f"bpy.data.images['{self.icon_file.name}'].preview.icon_id"
else:
self.outputs["Icon"].python_value = "0"
elif self.icon_source == "PATH":
self.code_import = "import os"
self.code_imperative = f"""
def load_preview_icon(path):
global _icons
if not path in _icons:
if os.path.exists(path):
_icons.load(path, path, "IMAGE")
else:
return 0
return _icons[path].icon_id
"""
self.outputs["Icon"].python_value = f"load_preview_icon({self.inputs[0].python_value})"
def evaluate_export(self, context):
if self.icon_source == "BLENDER":
self.outputs["Icon"].python_value = f"{self.icon}"
elif self.icon_source == "CUSTOM":
if self.icon_file:
self.outputs["Icon"].python_value = f"_icons['{self.icon_file.name}'].icon_id"
self.code_import = "import os"
self.code_register = f"""
if not '{self.icon_file.name}' in _icons: _icons.load('{self.icon_file.name}', os.path.join(os.path.dirname(__file__), 'icons', '{self.icon_file.name}'), "IMAGE")
"""
else:
self.outputs["Icon"].python_value = "0"
elif self.icon_source == "PATH":
self.code_import = "import os"
self.code_imperative = f"""
def load_preview_icon(path):
global _icons
if not path in _icons:
if os.path.exists(path):
_icons.load(path, path, "IMAGE")
else:
return 0
return _icons[path].icon_id
"""
self.outputs["Icon"].python_value = f"load_preview_icon({self.inputs[0].python_value})"
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.2
row.prop(self,"icon_source",expand=True)
if self.icon_source == "BLENDER":
op = layout.operator("sn.select_icon", text="Choose Icon", icon_value=self.icon)
op.icon_data_path = f"bpy.data.node_groups['{self.node_tree.name}'].nodes['{self.name}']"
elif self.icon_source == "CUSTOM":
layout.template_ID(self, "icon_file", new="image.new", open="image.open", live_icon=True)
if self.icon_file and not self.icon_file.filepath:
layout.label(text="Image not saved!", icon="ERROR")
@@ -0,0 +1,19 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_IsExportNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IsExportNode"
bl_label = "Is Export"
node_color = "BOOLEAN"
def on_create(self, context):
self.add_boolean_output("Is Export")
def evaluate(self, context):
self.outputs[0].python_value = f"False"
def evaluate_export(self, context):
self.outputs[0].python_value = f"True"
@@ -0,0 +1,34 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_InModeNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_InModeNode"
bl_label = "In Mode"
node_color = "BOOLEAN"
def get_modes(self,context):
items = []
modes = ["EDIT_MESH", "EDIT_CURVE", "EDIT_SURFACE", "EDIT_TEXT", "EDIT_ARMATURE",
"EDIT_METABALL", "EDIT_LATTICE", "POSE", "SCULPT", "PAINT_WEIGHT", "PAINT_VERTEX",
"PAINT_TEXTURE", "PARTICLE", "OBJECT", "PAINT_GPENCIL", "EDIT_GPENCIL",
"SCULPT_GPENCIL" "WEIGHT_GPENCIL", "VERTEX_GPENCIL", "SCULPT_CURVES"]
for mode in modes:
items.append((mode,mode.replace("_"," ").title(),mode))
return items
modes: bpy.props.EnumProperty(items=get_modes,
update=SN_ScriptingBaseNode._evaluate,
name="Mode",
description="The mode which the active mode is compared to")
def draw_node(self, context, layout):
layout.prop(self, "modes", text="")
def on_create(self, context):
self.add_boolean_output("In Mode")
def evaluate(self, context):
self.outputs[0].python_value = f"'{self.modes}'==bpy.context.mode"
@@ -0,0 +1,46 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ..Input.Node_Idname import NodeType
class SN_NodeIsIdname(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_NodeIsIdname"
bl_label = "Node Is Idname"
node_color = "PROPERTY"
nodes: bpy.props.CollectionProperty(type=NodeType)
node: bpy.props.StringProperty(name="Node",
description="The node to get the type for",
update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_property_input("Node")
self.add_boolean_output("Is ID Name")
# load internal nodes
for name in dir(bpy.types):
cls = getattr(bpy.types, name)
if hasattr(cls, "bl_rna") and cls.bl_rna.base and "Node" in cls.bl_rna.base.bl_rna.name:
item = self.nodes.add()
item.name = f"{cls.bl_rna.name} ({cls.bl_rna.base.bl_rna.name})"
item.identifier = cls.bl_rna.identifier
# load python nodes
for cls in bpy.types.Node.__subclasses__():
item = self.nodes.add()
item.name = f"{cls.bl_rna.name} (Python)"
item.identifier = cls.bl_rna.identifier
def evaluate(self, context):
if self.node:
self.outputs[0].python_value = f"{self.inputs[0].python_value}.bl_rna.identifier == '{self.nodes[self.node].identifier}'"
else:
self.outputs[0].python_value = "False"
def draw_node(self, context, layout):
layout.prop_search(self, "node", self, "nodes", text="")
@@ -0,0 +1,26 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_IsObjectType(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IsObjectType"
bl_label = "Is Object Type"
node_color = "PROPERTY"
types: bpy.props.EnumProperty(items=[('MESH', 'Mesh', ''), ('CURVE', 'Curve', ''), ('SURFACE', 'Surface', ''), ('META', 'Metaball', ''), ('FONT', 'Text', ''), ('HAIR', 'Hair', ''), ('POINTCLOUD', 'Point Cloud', ''), ('VOLUME', 'Volume', ''), ('GPENCIL', 'Grease Pencil', ''), ('ARMATURE', 'Armature', ''), ('LATTICE', 'Lattice', ''), ('EMPTY', 'Empty', ''), ('LIGHT', 'Light', ''), ('LIGHT_PROBE', 'Light Probe', ''), ('CAMERA', 'Camera', ''), ('SPEAKER', 'Speaker', '')],
update=SN_ScriptingBaseNode._evaluate,
name="Type",
description="The type which the object is compared to")
def draw_node(self, context, layout):
layout.prop(self, "types", text="")
def on_create(self, context):
self.add_property_input("Object")
self.add_boolean_output("Is Type")
def evaluate(self, context):
self.outputs[0].python_value = f"{self.inputs[0].python_value}.type == '{self.types}'"
@@ -0,0 +1,33 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_NamedIconNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_NamedIconNode"
bl_label = "Named Icon"
node_color = "ICON"
def on_create(self, context):
self.add_string_output("Icon")
def update_selected(self, context):
for icon in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items:
if icon.value == self.icon:
self.icon_name = icon.name
return
self.icon_name = "NONE"
self["icon"] = 0
icon: bpy.props.IntProperty(name="Value", description="Value of this socket", update=update_selected)
icon_name: bpy.props.StringProperty(default="NONE" ,update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.outputs[0].python_value = f"'{self.icon_name}'"
def draw_node(self, context, layout):
op = layout.operator("sn.select_icon", text="Choose Icon", icon_value=self.icon)
op.icon_data_path = f"bpy.data.node_groups['{self.node_tree.name}'].nodes['{self.name}']"
@@ -0,0 +1,53 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class NodeType(bpy.types.PropertyGroup):
name: bpy.props.StringProperty()
identifier: bpy.props.StringProperty()
class SN_NodeIdnameNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_NodeIdnameNode"
bl_label = "Node Idname"
node_color = "STRING"
def on_create(self, context):
self.add_string_output("Idname")
# load internal nodes
for name in dir(bpy.types):
cls = getattr(bpy.types, name)
if hasattr(cls, "bl_rna") and cls.bl_rna.base and "Node" in cls.bl_rna.base.bl_rna.name:
item = self.nodes.add()
item.name = f"{cls.bl_rna.name} ({cls.bl_rna.base.bl_rna.name})"
item.identifier = cls.bl_rna.identifier
# load python nodes
for cls in bpy.types.Node.__subclasses__():
item = self.nodes.add()
item.name = f"{cls.bl_rna.name} (Python)"
item.identifier = cls.bl_rna.identifier
nodes: bpy.props.CollectionProperty(type=NodeType)
node: bpy.props.StringProperty(name="Node",
description="The node to get the type for",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.node:
self.outputs["Idname"].python_value = f"'{self.nodes[self.node].identifier}'"
else:
self.outputs["Idname"].python_value = "''"
def draw_node(self, context, layout):
layout.prop_search(self, "node", self, "nodes", text="")
@@ -0,0 +1,33 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_RandomColorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RandomColorNode"
bl_label = "Random Color"
node_color = "VECTOR"
def on_create(self, context):
self.add_boolean_input("Use Alpha")
inp = self.add_float_input("Fixed Alpha")
inp.can_be_disabled = True
inp.disabled = True
inp.default_value = 1.0
inp = self.add_integer_input("Seed")
inp.can_be_disabled = True
inp.disabled = True
self.add_float_vector_output("Random Color").subtype = "COLOR"
def evaluate(self, context):
self.code_import = "import random"
self.code_imperative = """
def random_color(use_alpha, fixed_alpha, seed):
random.seed(seed)
if use_alpha:
return (random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1), fixed_alpha if fixed_alpha != None else random.uniform(0, 1))
else:
return (random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1))
"""
self.outputs[0].python_value = f"random_color({self.inputs['Use Alpha'].python_value}, {'None' if self.inputs['Fixed Alpha'].disabled else self.inputs['Fixed Alpha'].python_value}, {'None' if self.inputs['Seed'].disabled else self.inputs['Seed'].python_value})"
@@ -0,0 +1,47 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_RandomNumberNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RandomNumberNode"
bl_label = "Random Number"
node_color = "FLOAT"
def on_create(self, context):
self.add_float_input("Minimum")
self.add_float_input("Maximum")
inp = self.add_integer_input("Seed")
inp.can_be_disabled = True
inp.disabled = True
self.add_integer_output("Random Number")
def update_num_type(self, context):
self.convert_socket(self.outputs[0], self.socket_names[self.number_type])
self._evaluate(context)
number_type: bpy.props.EnumProperty(name="Type",
description="Type of number",
items=[("Integer", "Integer", "Integer"),
("Float", "Float", "Float")],
update=update_num_type)
def evaluate(self, context):
self.code_import = "import random"
self.code_imperative = """
def random_integer(min, max, seed):
random.seed(seed)
return random.randint(int(min), int(max))
def random_float(min, max, seed):
random.seed(seed)
return random.uniform(min, max)
"""
if self.number_type == "Integer":
self.outputs[0].python_value = f"random_integer({self.inputs['Minimum'].python_value}, {self.inputs['Maximum'].python_value}, {'None' if self.inputs['Seed'].disabled else self.inputs['Seed'].python_value})"
else:
self.outputs[0].python_value = f"random_float({self.inputs['Minimum'].python_value}, {self.inputs['Maximum'].python_value}, {'None' if self.inputs['Seed'].disabled else self.inputs['Seed'].python_value})"
def draw_node(self, context, layout):
layout.prop(self, "number_type")
@@ -0,0 +1,46 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_SceneContextNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SceneContextNode"
bl_label = "Scene Context"
node_color = "DEFAULT"
def on_create(self, context):
self.add_property_output("Active Scene")
self.add_property_output("Active Area")
self.add_property_output("Active View Layer")
self.add_property_output("Active Camera")
self.add_property_output("Active Bone")
self.add_property_output("Active Pose Bone")
self.add_property_output("Preferences")
self.add_property_output("Window Manager")
self.add_property_output("Window")
self.add_property_output("Screen")
self.add_string_output("Engine")
self.add_string_output("Mode")
self.add_string_output("Blend Filepath")
self.add_boolean_output("File Is Saved")
self.add_boolean_output("File Has Changes")
self.add_integer_output("Current Frame")
def evaluate(self, context):
self.outputs["Active Scene"].python_value = f"bpy.context.scene"
self.outputs["Active Area"].python_value = f"bpy.context.area"
self.outputs["Active View Layer"].python_value = f"bpy.context.view_layer"
self.outputs["Active Camera"].python_value = f"bpy.context.scene.camera"
self.outputs["Active Bone"].python_value = f"bpy.context.active_bone"
self.outputs["Active Pose Bone"].python_value = f"bpy.context.active_pose_bone"
self.outputs["Preferences"].python_value = f"bpy.context.preferences"
self.outputs["Window Manager"].python_value = f"bpy.context.window_manager"
self.outputs["Window"].python_value = f"bpy.context.window"
self.outputs["Screen"].python_value = f"bpy.context.screen"
self.outputs["Engine"].python_value = f"bpy.context.engine"
self.outputs["Mode"].python_value = f"bpy.context.mode"
self.outputs["Blend Filepath"].python_value = f"bpy.data.filepath"
self.outputs["File Is Saved"].python_value = f"bpy.data.is_saved"
self.outputs["File Has Changes"].python_value = f"bpy.data.is_dirty"
self.outputs["Current Frame"].python_value = f"bpy.context.scene.frame_current"
@@ -0,0 +1,33 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_TimeNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_TimeNode"
bl_label = "Time and Date"
node_color = "INTEGER"
def on_create(self, context):
self.add_string_output("Time")
self.add_integer_output("Hours")
self.add_integer_output("Minutes")
self.add_integer_output("Seconds")
self.add_integer_output("Milliseconds")
self.add_string_output("Date")
self.add_integer_output("Year")
self.add_integer_output("Month")
self.add_integer_output("Day")
def evaluate(self, context):
self.code_import = f"from datetime import datetime"
self.outputs[0].python_value = "str(datetime.now().time()).split(\".\")[0]"
self.outputs[1].python_value = "datetime.now().time().hour"
self.outputs[2].python_value = "datetime.now().time().minute"
self.outputs[3].python_value = "datetime.now().time().second"
self.outputs[4].python_value = "datetime.now().time().microsecond//1000"
self.outputs[5].python_value = "str(datetime.now().date())"
self.outputs[6].python_value = "datetime.now().date().year"
self.outputs[7].python_value = "datetime.now().date().month"
self.outputs[8].python_value = "datetime.now().date().day"
@@ -0,0 +1,187 @@
import bpy
from ..Program.Run_Operator import on_operator_ref_update
from ..base_node import SN_ScriptingBaseNode
class SN_ButtonNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ButtonNodeNew"
bl_label = "Button"
node_color = "INTERFACE"
bl_width_default = 240
def on_create(self, context):
self.version = 1
self.add_interface_input()
self.add_string_input("Label").default_value = "My Button"
self.add_boolean_input("Emboss").default_value = True
self.add_boolean_input("Depress")
self.add_icon_input("Icon")
self.add_interface_output().passthrough_layout_type = True
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
on_operator_ref_update(self, node, data, self.ref_ntree, self.ref_SN_OperatorNode, 5)
def reset_inputs(self):
""" Remove all operator inputs """
for i in range(len(self.inputs)-1, -1, -1):
inp = self.inputs[i]
if inp.can_be_disabled:
self.inputs.remove(inp)
def create_inputs(self, op_rna):
""" Create inputs for operator """
for prop in op_rna.properties:
if not prop.identifier in ["rna_type", "settings"]:
inp = self.add_input_from_property(prop)
if inp:
inp.can_be_disabled = True
inp.disabled = not prop.is_required
def update_custom_operator(self, context):
""" Updates the nodes settings when a new parent panel is selected """
self.reset_inputs()
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
parent = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
for prop in parent.properties:
if prop.property_type in ["Integer", "Float", "Boolean"] and prop.settings.is_vector:
socket = self._add_input(self.socket_names[prop.property_type + " Vector"], prop.name)
socket.size = prop.settings.size
socket.can_be_disabled = True
elif prop.property_type == "Enum":
if prop.stngs_enum.enum_flag:
socket = self._add_input(self.socket_names["Enum Set"], prop.name)
else:
socket = self._add_input(self.socket_names[prop.property_type], prop.name)
socket.items = str(list(map(lambda item: item.name, prop.stngs_enum.items)))
else:
self._add_input(self.socket_names[prop.property_type], prop.name).can_be_disabled = True
self._evaluate(context)
def update_source_type(self, context):
self.hide_disabled_inputs = False
if self.source_type == "BLENDER":
self.pasted_operator = self.pasted_operator
elif self.source_type == "CUSTOM":
self.ref_SN_OperatorNode = self.ref_SN_OperatorNode
self._evaluate(context)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the operator from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
source_type: bpy.props.EnumProperty(name="Source Type",
description="Use a custom operator or a blender internal",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=update_source_type)
ref_SN_OperatorNode: bpy.props.StringProperty(name="Custom Operator",
description="The operator ran by this button",
update=update_custom_operator)
def update_pasted_operator(self, context):
self.reset_inputs()
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
self.pasted_name = op_rna.name
self.create_inputs(op_rna)
self._evaluate(context)
pasted_operator: bpy.props.StringProperty(default="bpy.ops.sn.dummy_button_operator()",
update=update_pasted_operator)
pasted_name: bpy.props.StringProperty(default="Paste Operator")
def update_hide_disabled_inputs(self, context):
for inp in self.inputs:
if inp.can_be_disabled and inp.disabled:
inp.set_hide(self.hide_disabled_inputs)
hide_disabled_inputs: bpy.props.BoolProperty(default=False,
name="Hide Disabled Inputs",
description="Hides the disabled inputs of this node",
update=update_hide_disabled_inputs)
def evaluate(self, context):
if self.source_type == "BLENDER":
op_name = self.pasted_operator[8:].split("(")[0]
code = f"op = {self.active_layout}.operator('{op_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in op_rna.properties:
if (self.version == 0 and (prop.name and prop.name == inp.name) or (not prop.name and prop.identifier.replace("_", " ").title() == inp.name)) \
or (self.version == 1 and inp.name.replace(" ", "_").lower() == prop.identifier):
code += "\n" + f"op.{prop.identifier} = {inp.python_value}"
self.code = f"""
{self.indent(code, 6)}
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
code = f"op = {self.active_layout}.operator('sna.{node.operator_python_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in node.properties:
if prop.name == inp.name:
code += "\n" + f"op.{prop.python_name} = {inp.python_value}"
else:
code = f"op = {self.active_layout}.operator('sn.dummy_button_operator', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
self.code = f"""
{self.indent(code, 6)}
{self.indent(self.outputs[0].python_value, 6)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "source_type", text="", icon_only=True)
if self.source_type == "BLENDER":
name = "Paste Operator"
if self.pasted_operator:
if self.pasted_name:
name = self.pasted_name
elif len(self.pasted_operator.split(".")) > 2:
name = self.pasted_operator.split(".")[3].split("(")[0].replace("_", " ").title()
else:
name = self.pasted_operator
op = row.operator("sn.paste_operator", text=name, icon="PASTEDOWN")
op.node_tree = self.node_tree.name
op.node = self.name
elif self.source_type == "CUSTOM":
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OperatorNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_OperatorNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_OperatorNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_OperatorNode
row.prop(self, "hide_disabled_inputs", text="", icon="HIDE_ON" if self.hide_disabled_inputs else "HIDE_OFF", emboss=False)
@@ -0,0 +1,100 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...utils import get_python_name
class SN_DisplayCollectionListNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayCollectionListNodeNew"
bl_label = "Display Collection List"
node_color = "INTERFACE"
bl_width_default = 200
def layout_type(self, _): return "layout"
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input()
self.add_property_input("Index Property")
self.add_integer_input("Rows")
self.add_dynamic_interface_output("Item Row")
self.add_dynamic_interface_output(
"Interface").passthrough_layout_type = True
self.add_property_output("Item")
self.add_integer_output("Item Index")
def update_function_node(self, context):
self._evaluate(context)
ref_SN_FunctionNode: bpy.props.StringProperty(name="Function",
description="Filter function which should have a single property input and return a boolean (True keeps the item, False removes it)",
update=update_function_node)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Function Node Tree",
description="The node tree to select the function from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
def draw_node(self, context, layout):
row = layout.row(align=True)
row.label(text="Filter:")
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_FunctionNode", bpy.data.node_groups[parent_tree.name].node_collection(
"SN_FunctionNode"), "refs", text="")
def evaluate(self, context):
if self.inputs["Index Property"].is_linked and self.inputs["Collection Property"].is_linked:
ui_list_idname = f"SNA_UL_{get_python_name(self.name, 'List')}_{self.static_uid}"
func = None
if self.ref_ntree and self.ref_SN_FunctionNode in self.ref_ntree.nodes:
func = self.ref_ntree.nodes[self.ref_SN_FunctionNode]
self.code_imperative = f"""
def display_collection_id(uid, vars):
id = f"coll_{{uid}}"
for var in vars.keys():
if var.startswith("i_"):
id += f"_{{var}}_{{vars[var]}}"
return id
class {ui_list_idname}(bpy.types.UIList):
def draw_item(self, context, layout, data, item_{self.static_uid}, icon, active_data, active_propname, index_{self.static_uid}):
row = layout
{self.indent([out.python_value if out.name == 'Item Row' else '' for out in self.outputs], 11)}
def filter_items(self, context, data, propname):
flt_flags = []
for item in getattr(data, propname):
if not self.filter_name or self.filter_name.lower() in item.name.lower():
if {f'{func.func_name}(item)' if func else 'True'}:
flt_flags.append(self.bitflag_filter_item)
else:
flt_flags.append(0)
else:
flt_flags.append(0)
return flt_flags, []
"""
self.code_register = f"""
bpy.utils.register_class({ui_list_idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({ui_list_idname})
"""
self.code = f"""
coll_id = display_collection_id('{self.static_uid}', locals())
{self.active_layout}.template_list('{ui_list_idname}', coll_id, {self.inputs['Collection Property'].python_source}, '{self.inputs['Collection Property'].python_attr}', {self.inputs['Index Property'].python_source}, '{self.inputs['Index Property'].python_attr}', rows={self.inputs['Rows'].python_value})
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 4)}
"""
self.outputs["Item"].python_value = f"item_{self.static_uid}"
if "Item Index" in self.outputs:
self.outputs["Item Index"].python_value = f"index_{self.static_uid}"
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
self.outputs["Item"].reset_value()
if "Item Index" in self.outputs:
self.outputs["Item Index"].reset_value()
@@ -0,0 +1,35 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayEnumItemNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayEnumItemNode"
bl_label = "Display Enum Item"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_string_input("Item")
self.add_string_input("Label")
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Property"].is_linked and (self.inputs["Item"].is_linked or self.inputs["Item"].default_value):
self.code = f"""
{self.active_layout}.prop_enum({self.inputs['Property'].python_source}, '{self.inputs['Property'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, value={self.inputs['Item'].python_value})
{self.indent(self.outputs[0].python_value, 6)}
"""
elif not self.inputs["Property"].is_linked:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Enum Item specified!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 6)}
"""
@@ -0,0 +1,25 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayIconNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayIconNodeNew"
bl_label = "Display Icon"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_icon_input("Icon")
self.add_float_input("Scale").default_value = 1
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
self.code = f"""
{self.active_layout}.template_icon(icon_value={self.inputs['Icon'].python_value}, scale={self.inputs['Scale'].python_value})
{self.indent(self.outputs[0].python_value, 3)}
"""
def draw_node(self, context, layout):
layout.label(text="Use this node with care! This might slow down your UI", icon="INFO")
@@ -0,0 +1,34 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayPreviewNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPreviewNodeNew"
bl_label = "Display Preview"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_boolean_input("Show Buttons")
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Property"].is_linked:
self.code = f"""
{self.active_layout}.template_preview({self.inputs['Property'].python_value}, show_buttons={self.inputs['Show Buttons'].python_value})
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
box = layout.box()
box.label(text="Careful! This element is prone to crashes!", icon="INFO")
box.label(text="Only materials, textures, lights, worlds and line styles can be displayed")
@@ -0,0 +1,60 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplayPropertyNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPropertyNodeNew"
bl_label = "Display Property"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_string_input("Label")
self.add_icon_input("Icon")
self.add_boolean_input("Emboss").default_value = True
inp = self.add_boolean_input("Expand")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Slider")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Toggle")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Invert Checkbox")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Full Shortcut")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_integer_input("Index")
inp.can_be_disabled = True
inp.disabled = True
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Property"].is_linked:
attributes = ""
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
attributes += f", {inp.name.lower().replace(' ', '_')}={inp.python_value}"
attribute = f"attr_{self.static_uid} = '[\"' + str({self.inputs['Property'].python_attr[1:-1]} + '\"]') " if self.inputs["Property"].python_is_attribute else ""
self.code = f"""
{attribute}
{self.active_layout}.prop({self.inputs['Property'].python_source}, {f"attr_{self.static_uid}" if self.inputs["Property"].python_is_attribute else "'" + self.inputs['Property'].python_attr.replace("'", '"') + "'"}, text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}{attributes})
{self.indent(self.outputs[0].python_value, 7)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 7)}
"""
@@ -0,0 +1,31 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplaySearchNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySearchNodeNew"
bl_label = "Display Search"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input("Collection")
self.add_property_input("String/Pointer")
self.add_string_input("Label")
self.add_icon_input("Blender Icon").subtype = "BLENDER_ONLY"
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if len(self.inputs["String/Pointer"].links) and len(self.inputs["Collection"].links):
self.code = f"""
{self.active_layout}.prop_search({self.inputs['String/Pointer'].python_source}, '{self.inputs['String/Pointer'].python_attr.replace("'",'"')}', {self.inputs['Collection'].python_source}, '{self.inputs['Collection'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, icon={self.inputs['Blender Icon'].python_value})
{self.indent(self.outputs[0].python_value, 4)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 4)}
"""
@@ -0,0 +1,70 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_DisplaySerpensShortcutNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySerpensShortcutNodeNew"
bl_label = "Display Serpens Shortcut"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
self.add_interface_output().passthrough_layout_type = True
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
ref_SN_OnKeypressNode: bpy.props.StringProperty(name="Shortcut",
description="The shortcut to display",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.ref_ntree and self.ref_SN_OnKeypressNode in self.ref_ntree.nodes:
self.code_imperative = """
def find_user_keyconfig(key):
km, kmi = addon_keymaps[key]
for item in bpy.context.window_manager.keyconfigs.user.keymaps[km.name].keymap_items:
found_item = False
if kmi.idname == item.idname:
found_item = True
for name in dir(kmi.properties):
if not name in ["bl_rna", "rna_type"] and not name[0] == "_":
if name in kmi.properties and name in item.properties and not kmi.properties[name] == item.properties[name]:
found_item = False
if found_item:
return item
print(f"Couldn't find keymap item for {key}, using addon keymap instead. This won't be saved across sessions!")
return kmi
"""
node = self.ref_ntree.nodes[self.ref_SN_OnKeypressNode]
src = f"find_user_keyconfig('{node.static_uid}')"
self.code = f"""
{self.active_layout}.prop({src}, 'type', text={self.inputs['Label'].python_value}, full_event=True)
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OnKeypressNode", parent_tree.node_collection(
"SN_OnKeypressNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_OnKeypressNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="",
icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_OnKeypressNode
layout.label(
text="Copy blender shortcuts from the blend data browser in display property", icon="INFO")
@@ -0,0 +1,45 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...utils import normalize_code
class SN_EnumMapInterfaceNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_EnumMapInterfaceNode"
bl_label = "Enum Map (Interface)"
bl_width_default = 200
node_color = "INTERFACE"
passthrough_layout_type = True
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Enum Value")
self.add_interface_output("Interface")
self.add_interface_output("Other Option")
out = self.add_dynamic_interface_output("Enum Option")
out.is_variable = True
def on_socket_name_change(self, socket):
self._evaluate(bpy.context)
def evaluate(self, context):
options = ""
for out in self.outputs:
if out.is_variable and not out.dynamic:
option_code = self.indent(out.python_value, 7)
option = f"""
{"el" if options else ""}if {self.inputs["Enum Value"].python_value} == "{out.name}":
{option_code}
"""
if option_code.strip():
options += normalize_code(option) + "\n"
other_opt_code = self.indent(self.outputs['Other Option'].python_value, 6)
self.code = f"""
{self.indent(options, 5)}
{"else:" if options.strip() else "if True:"}
{other_opt_code if other_opt_code.strip() else "pass"}
{self.indent(self.outputs[0].python_value, 5)}
"""
@@ -0,0 +1,30 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_IconGalleryNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IconGalleryNodeNew"
bl_label = "Icon Gallery"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input("Enum")
self.add_boolean_input("Show Labels")
self.add_float_input("Scale").default_value = 5
self.add_float_input("Scale Popup").default_value = 5
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
if self.inputs["Enum"].is_linked:
self.code = f"""
{self.active_layout}.template_icon_view({self.inputs['Enum'].python_source}, '{self.inputs['Enum'].python_attr}', show_labels={self.inputs['Show Labels'].python_value}, scale={self.inputs['Scale'].python_value}, scale_popup={self.inputs['Scale Popup'].python_value})
{self.indent(self.outputs[0].python_value, 4)}
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
{self.indent(self.outputs[0].python_value, 4)}
"""
@@ -0,0 +1,26 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
class SN_LabelNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_LabelNodeNew"
bl_label = "Label"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label").default_value = "My Label"
self.add_icon_input("Icon")
self.add_interface_output().passthrough_layout_type = True
def evaluate(self, context):
self.code = f"""
{self.active_layout}.label(text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value})
{self.indent(self.outputs[0].python_value, 3)}
"""
def draw_node(self, context, layout):
pass
@@ -0,0 +1,73 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToMenuNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToMenuNodeNew"
bl_label = "Add To Menu"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Menu")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the menu"),
("APPEND", "Append", "Append this to the end of the menu")],
name="Position",
description="Position of this interface to the selected menu",
update=SN_ScriptingBaseNode._evaluate)
menu_parent: bpy.props.StringProperty(default="VIEW3D_MT_add",
name="Parent",
description="The menu id this interface should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.menu_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value if out.name == 'Menu' else '' for out in self.outputs], 5)}
"""
if self.menu_parent == "WM_MT_button_context":
self.code_imperative = """
class WM_MT_button_context(bpy.types.Menu):
bl_label = ""
def draw(self, context):
pass
"""
self.code_register = f"""
{"if getattr(bpy.types, 'WM_MT_button_context', None) == None: bpy.utils.register_class(WM_MT_button_context)" if self.menu_parent == "WM_MT_button_context" else ""}
bpy.types.{self.menu_parent}.{self.append.lower()}({func_name})
"""
self.code_unregister = f"""
bpy.types.{self.menu_parent}.remove({func_name})
"""
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_menu_picker", text=f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,60 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToPanelNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToPanelNodeNew"
bl_label = "Add To Panel"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Panel")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the panel"),
("APPEND", "Append", "Append this to the end of the panel")],
name="Position",
description="Position of this interface to the selected panel",
update=SN_ScriptingBaseNode._evaluate)
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.panel_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value if out.name == 'Panel' else '' for out in self.outputs], 7)}
"""
self.code_register = f"bpy.types.{self.panel_parent}.{self.append.lower()}({func_name})"
self.code_unregister = f"bpy.types.{self.panel_parent}.remove({func_name})"
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,111 @@
import bpy
from ....utils import get_python_name
from ...base_node import SN_ScriptingBaseNode
class SN_RunInterfaceFunctionNodeNew(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunInterfaceFunctionNodeNew"
bl_label = "Function Run (Interface)"
bl_width_default = 240
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_dynamic_interface_output().passthrough_layout_type = True
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
if node.bl_idname == "SN_InterfaceFunctionNode" and data:
index = 0
for i, out in enumerate(node.outputs):
if out.bl_idname != "SN_InterfaceSocket":
index = i-1
break
# inputs has been added
if "added" in data:
socket_index = list(data["added"].node.outputs).index(data["added"])
self.add_input_from_socket(data["added"])
self.inputs.move(len(self.inputs)-1, socket_index-index)
# input has been removed
elif "removed" in data:
self.inputs.remove(self.inputs[data["removed"]])
# input has changed
elif "changed" in data:
self.convert_socket(self.inputs[data["changed"].index-index], data["changed"].bl_idname)
# input has updated
elif "updated" in data:
if data["updated"].index-index < len(self.inputs):
self.inputs[data["updated"].index-index].name = data["updated"].name
self._evaluate(bpy.context)
def update_function_reference(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
# remember connections
links = []
for inp in self.inputs[1:]:
links.append(None)
if inp.is_linked:
links[-1] = inp.from_socket()
# remove current data inputs
for i in range(len(self.inputs)-1, 0, -1):
self.inputs.remove(self.inputs[i])
# add new data inputs
if self.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
for out in parent_tree.nodes[self.ref_SN_InterfaceFunctionNode].outputs[1:-1]:
if out.bl_idname != "SN_InterfaceSocket":
self.add_input_from_socket(out)
# restore connections
if len(links) == len(self.inputs)-1:
for i, from_socket in enumerate(links):
if from_socket:
self.node_tree.links.new(from_socket, self.inputs[i+1])
self._evaluate(context)
ref_SN_InterfaceFunctionNode: bpy.props.StringProperty(name="Function",
description="The function to run",
update=update_function_reference)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
if self.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
# get input values
inp_values = []
for inp in self.inputs[1:]:
inp_values.append(inp.python_value)
inp_values = ", ".join(inp_values)
self.code = f"""
layout_function = {self.active_layout}
{parent_tree.nodes[self.ref_SN_InterfaceFunctionNode].func_name}(layout_function, {inp_values})
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 6)}
"""
else:
self.code = f"""
{self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_InterfaceFunctionNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_InterfaceFunctionNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_InterfaceFunctionNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_InterfaceFunctionNode
@@ -0,0 +1,94 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....utils import get_python_name, unique_collection_name
class SN_InterfaceFunctionNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_InterfaceFunctionNode"
bl_label = "Function (Interface)"
def layout_type(self, _): return "layout_function"
is_trigger = True
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_dynamic_interface_output()
out = self.add_dynamic_data_output("Input")
out.is_variable = True
out.changeable = True
def on_dynamic_socket_add(self, socket):
if not socket.bl_idname == "SN_InterfaceSocket":
sockets = []
for out in self.outputs:
if not out.bl_idname in ["SN_InterfaceSocket", "SN_DynamicInterfaceSocket"]:
sockets.append(out.name)
socket["name"] = get_python_name(socket.name, "Input", lower=False)
socket["name"] = unique_collection_name(socket.name, "Input", sockets[:-1], "_", includes_name=True)
self.trigger_ref_update({ "added": socket })
self._evaluate(bpy.context)
def on_dynamic_socket_remove(self, index, is_output):
first_index = 0
for out in self.outputs:
if out.bl_idname in ["SN_InterfaceSocket", "SN_DynamicInterfaceSocket"]:
first_index += 1
if index >= first_index:
self.trigger_ref_update({ "removed": index - first_index + 1 })
def on_socket_type_change(self, socket):
if not socket.bl_idname == "SN_InterfaceSocket":
self.trigger_ref_update({ "changed": socket })
def on_socket_name_change(self, socket):
if not socket.bl_idname == "SN_InterfaceSocket":
sockets = []
for out in self.outputs:
if not out.bl_idname in ["SN_InterfaceSocket", "SN_DynamicInterfaceSocket"]:
sockets.append(out.name)
socket["name"] = get_python_name(socket.name, "Input", lower=False)
socket["name"] = unique_collection_name(socket.name, "Input", sockets[:-1], "_", includes_name=True)
self.trigger_ref_update({ "updated": socket })
self._evaluate(bpy.context)
@property
def func_name(self):
return f"sna_{get_python_name(self.name, 'func')}_{self.static_uid}"
def evaluate(self, context):
out_values = []
index = 0
for i, out in enumerate(self.outputs):
if out.bl_idname != "SN_InterfaceSocket":
index = i
break
for i, out in enumerate(self.outputs[index:-1]):
out_values.append(get_python_name(out.name, f"parameter_{i}"))
out_names = ", ".join(out_values)
if index > 1:
code = self.indent([out.python_value for out in self.outputs[:index-1]], 6)
else:
code = self.indent([out.python_value if out.name == 'Interface' else '' for out in self.outputs], 0)
self.code = f"""
def {self.func_name}(layout_function, {out_names}):
{code if code.strip() else "pass"}
"""
for i, out in enumerate(self.outputs[index:-1]):
out.python_value = out_values[i]
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "name")
op = row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM")
op.node = self.name
op.add_node = "SN_RunInterfaceFunctionNodeNew"
row.operator("sn.copy_python_name", text="", icon="COPYDOWN").name = self.func_name
@@ -0,0 +1,68 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_MenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_MenuNode"
bl_label = "Menu"
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_integer_input("Columns")["default_value"] = 1
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Menu")
idname_override: bpy.props.StringProperty(default="",
name="Idname Override",
description="Use this if you want to define the idname of this panel yourself",
update=SN_ScriptingBaseNode._evaluate)
title: bpy.props.StringProperty(default="",
name="Label",
description="The label shown at the top of this menu",
update=SN_ScriptingBaseNode._evaluate)
@property
def idname(self):
if self.idname_override:
return self.idname_override
return f"SNA_MT_{self.static_uid}"
def evaluate(self, context):
self.trigger_ref_update()
self.code = f"""
class {self.idname}(bpy.types.Menu):
bl_idname = "{self.idname}"
bl_label = "{self.title}"
@classmethod
def poll(cls, context):
return not ({self.inputs["Hide"].python_value})
def draw(self, context):
layout = self.layout.column_flow(columns={self.inputs["Columns"].python_value})
layout.operator_context = "INVOKE_DEFAULT"
{self.indent([out.python_value if out.name == 'Menu' else '' for out in self.outputs], 5)}
"""
self.code_register = f"""
bpy.utils.register_class({self.idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({self.idname})
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "name")
row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM").node = self.name
layout.prop(self, "title")
def draw_node_panel(self, context, layout):
layout.prop(self, "idname_override")
@@ -0,0 +1,295 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....utils import get_python_name
class SN_PanelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PanelNode"
bl_label = "Panel"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Panel")
self.add_dynamic_interface_output("Header")
self.ref_ntree = self.node_tree
def update_from_parent(self, parent):
""" Update this subpanels nodes values from the parent node """
self["space"] = parent.space
self["region"] = parent.region
self["context"] = parent.context
def update_custom_parent(self, context):
""" Updates the nodes settings when a new parent panel is selected """
if self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
parent = self.ref_ntree.nodes[self.ref_SN_PanelNode]
self.update_from_parent(parent)
self._evaluate(context)
def on_ref_update(self, node, data=None):
""" Called when a parent panel is updated, updates this nodes settings """
if self.is_subpanel:
if node.node_tree == self.ref_ntree and node.name == self.ref_SN_PanelNode:
self.update_from_parent(node)
self._evaluate(bpy.context)
def update_is_subpanel(self, context):
""" Updates the compile order when turned into a subpanel """
if self.is_subpanel:
self["parent_type"] = "BLENDER"
self["ref_SN_PanelNode"] = ""
self["space"] = "PROPERTIES"
self["region"] = "WINDOW"
self["context"] = "render"
self.order = 1
else:
self["space"] = "VIEW_3D"
self["region"] = "UI"
self["context"] = ""
self.order = 0
self._evaluate(context)
panel_label: bpy.props.StringProperty(default="New Panel",
name="Label",
description="The label of your panel",
update=SN_ScriptingBaseNode._evaluate)
space: bpy.props.StringProperty(default="VIEW_3D",
name="Space",
description="The space your panel is in",
update=SN_ScriptingBaseNode._evaluate)
region: bpy.props.StringProperty(default="UI",
name="Region",
description="The region your panel is in",
update=SN_ScriptingBaseNode._evaluate)
context: bpy.props.StringProperty(default="",
name="Context",
description="The context your panel is in",
update=SN_ScriptingBaseNode._evaluate)
category: bpy.props.StringProperty(default="New Category",
name="Category",
description="The category your panel is in (Only relevant if in an N-Panel)",
update=SN_ScriptingBaseNode._evaluate)
panel_order: bpy.props.IntProperty(default=0, min=0,
name="Order",
description="The order of your panel compared to other custom panels",
update=SN_ScriptingBaseNode._evaluate)
hide_header: bpy.props.BoolProperty(default=False,
name="Hide Header",
description="Hide the panels header",
update=SN_ScriptingBaseNode._evaluate)
expand_header: bpy.props.BoolProperty(default=False,
name="Expand Header",
description="Expands the header to fill the full panel width",
update=SN_ScriptingBaseNode._evaluate)
default_closed: bpy.props.BoolProperty(default=False,
name="Default Closed",
description="Closes the panel by default",
update=SN_ScriptingBaseNode._evaluate)
def update_shortcut_only(self, context):
if self.shortcut_only:
self.space = "VIEW_3D"
self.region = "WINDOW"
self.context = ""
self.category = ""
self._evaluate(context)
shortcut_only: bpy.props.BoolProperty(default=False,
name="Shortcut Only",
description="Only displays this panel when opened with a shortcut and not in the interface",
update=update_shortcut_only)
is_subpanel: bpy.props.BoolProperty(default=False,
name="Is Subpanel",
description="If this panel should be a subpanel",
update=update_is_subpanel)
panel_parent: bpy.props.StringProperty(default="RENDER_PT_context",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
ref_SN_PanelNode: bpy.props.StringProperty(name="Custom Parent",
description="The panel used as a custom parent panel for this subpanel",
update=update_custom_parent)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
idname_override: bpy.props.StringProperty(default="",
name="Idname Override",
description="Use this if you want to define the idname of this panel yourself",
update=SN_ScriptingBaseNode._evaluate)
panel_width: bpy.props.IntProperty(name="Panel Width", default=0,
description="The panel width for popovers and popups",
update=SN_ScriptingBaseNode._evaluate)
last_idname: bpy.props.StringProperty(name="Last Idname",
description="The last idname that this panel had when it was compiled",
default="")
def update_idname(self):
py_name = get_python_name(self.panel_label)
alt_py_name = get_python_name(self.name)
idname = f"SNA_PT_{py_name.upper() if py_name else alt_py_name}_{self.uuid}"
if self.idname_override:
idname = self.idname_override
self.last_idname = idname
self.trigger_ref_update()
def evaluate(self, context):
self.update_idname()
options = []
if self.hide_header: options.append("'HIDE_HEADER'")
if self.expand_header: options.append("'HEADER_LAYOUT_EXPAND'")
if self.default_closed: options.append("'DEFAULT_CLOSED'")
parent = ""
if self.is_subpanel:
if self.parent_type == "BLENDER":
parent = self.panel_parent
elif self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
parent_node = self.ref_ntree.nodes[self.ref_SN_PanelNode]
parent = parent_node.last_idname
self.code = f"""
class {self.last_idname}(bpy.types.Panel):
bl_label = '{self.panel_label}'
bl_idname = '{self.last_idname}'
bl_space_type = '{self.space}'
bl_region_type = '{self.region}'
bl_context = '{self.context}'
{f"bl_category = '{self.category}'" if self.category and not parent else ""}
bl_order = {self.panel_order}
{f"bl_options = {{{', '.join(options)}}}" if options else ""}
{f"bl_parent_id = '{parent}'" if self.is_subpanel and parent else ""}
bl_ui_units_x={self.panel_width}
@classmethod
def poll(cls, context):
return not ({self.inputs["Hide"].python_value})
def draw_header(self, context):
layout = self.layout
{self.indent([out.python_value if out.name == 'Header' else '' for out in self.outputs], 7)}
def draw(self, context):
layout = self.layout
{self.indent([out.python_value if out.name == 'Panel' else '' for out in self.outputs], 7)}
"""
if self.is_subpanel and parent and not context.scene.sn.is_exporting and self.parent_type == "CUSTOM":
self.code_register = f"if '{parent}' in globals(): bpy.utils.register_class({self.last_idname})"
self.code_unregister = f"if '{parent}' in globals(): bpy.utils.unregister_class({self.last_idname})"
else:
if bpy.context.scene.sn.is_exporting:
self.code_register = f"bpy.utils.register_class({self.last_idname})"
self.code_unregister = f"bpy.utils.unregister_class({self.last_idname})"
else:
self.code_register = f"""
try: bpy.utils.register_class({self.last_idname})
except: pass
"""
self.code_unregister = f"""
try: bpy.utils.unregister_class({self.last_idname})
except: pass
"""
def draw_node(self, context, layout):
if not self.shortcut_only:
if not self.is_subpanel:
row = layout.row(align=True)
row.scale_y = 1.4
op = row.operator("sn.activate_panel_picker", text=f"{self.space.replace('_', ' ').title()} {self.region.replace('_', ' ').title()} {self.context.replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
else:
row = layout.row(align=True)
if self.parent_type == "BLENDER":
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
row.prop(self, "parent_type", text="", icon_only=True)
else:
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_PanelNode", bpy.data.node_groups[parent_tree.name].node_collection(self.bl_idname), "refs", text="")
row.prop(self, "parent_type", text="", icon_only=True)
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_PanelNode in self.ref_ntree.nodes
op = subrow.operator("sn.find_node", text="", icon="RESTRICT_SELECT_OFF", emboss=False)
op.node_tree = self.ref_ntree.name if self.ref_ntree else ""
op.node = self.ref_SN_PanelNode
if self.ref_SN_PanelNode == self.name and self.ref_ntree == self.node_tree:
layout.label(text="Can't use self as panel parent!", icon="ERROR")
layout.prop(self, "is_subpanel")
row = layout.row(align=True)
row.prop(self, "name")
row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM").node = self.name
layout.prop(self, "panel_label")
if not self.shortcut_only:
if not self.is_subpanel:
layout.prop(self, "category")
layout.prop(self, "panel_order")
layout.prop(self, "hide_header")
layout.prop(self, "expand_header")
layout.prop(self, "default_closed")
layout.prop(self, "shortcut_only")
def draw_node_panel(self, context, layout):
col = layout.column()
col.enabled = not self.is_subpanel
col.prop(self, "space")
col.prop(self, "region")
col.prop(self, "context")
row = layout.row()
row.enabled = self.is_subpanel
row.prop(self, "panel_parent")
layout.prop(self, "idname_override")
layout.prop(self, "panel_width")
@@ -0,0 +1,66 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_PieMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_PieMenuNode"
bl_label = "Pie Menu"
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_dynamic_interface_output("Menu")
idname_override: bpy.props.StringProperty(default="",
name="Idname Override",
description="Use this if you want to define the idname of this panel yourself",
update=SN_ScriptingBaseNode._evaluate)
title: bpy.props.StringProperty(default="",
name="Label",
description="The label shown at the top of this menu",
update=SN_ScriptingBaseNode._evaluate)
@property
def idname(self):
if self.idname_override:
return self.idname_override
return f"SNA_MT_{self.static_uid}"
def evaluate(self, context):
self.trigger_ref_update()
self.code = f"""
class {self.idname}(bpy.types.Menu):
bl_idname = "{self.idname}"
bl_label = "{self.title}"
@classmethod
def poll(cls, context):
return not ({self.inputs["Hide"].python_value})
def draw(self, context):
layout = self.layout.menu_pie()
{self.indent([out.python_value if out.name == 'Menu' else '' for out in self.outputs], 5)}
"""
self.code_register = f"""
bpy.utils.register_class({self.idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({self.idname})
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "name")
row.operator("sn.find_referencing_nodes", text="", icon="VIEWZOOM").node = self.name
layout.prop(self, "title")
def draw_node_panel(self, context, layout):
layout.prop(self, "idname_override")
@@ -0,0 +1,103 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ...templates.PropertyNode import PropertyNode
class SN_PreferencesNode(SN_ScriptingBaseNode, bpy.types.Node, PropertyNode):
bl_idname = "SN_PreferencesNode"
bl_label = "Preferences"
bl_width_default = 200
def layout_type(self, _):
return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide").default_value = False
self.add_dynamic_interface_output("Preferences")
def evaluate(self, context):
props_imperative_list = self.props_imperative(context).split("\n")
props_code_list = self.props_code(context).split("\n")
props_register_list = self.props_register(context).split("\n")
props_unregister_list = self.props_unregister(context).split("\n")
idname = f"SNA_TempAddonPreferences_{self.static_uid}"
prop_name = f"sna_addon_prefs_temp"
self.code = f"""
{self.indent(props_imperative_list, 5)}
class {idname}(bpy.types.PropertyGroup):
{self.indent(props_code_list, 6) if self.indent(props_code_list, 6).strip() else "pass"}
def sna_prefs(layout):
if not ({self.inputs["Hide"].python_value}):
self = bpy.context.scene.{prop_name}
{self.indent([out.python_value for out in self.outputs[:-1]], 7)}
"""
self.code_register = f"""
{self.indent(props_register_list, 7)}
bpy.utils.register_class({idname})
bpy.types.Scene.{prop_name} = bpy.props.PointerProperty(type={idname})
bpy.context.scene.sn.preferences.append(sna_prefs)
for a in bpy.context.screen.areas: a.tag_redraw()
"""
self.code_unregister = f"""
bpy.context.scene.sn.preferences.clear()
for a in bpy.context.screen.areas: a.tag_redraw()
del bpy.types.Scene.{prop_name}
bpy.utils.unregister_class({idname})
{self.indent(props_unregister_list, 7)}
"""
def evaluate_export(self, context):
props_imperative_list = self.props_imperative(context).split("\n")
props_code_list = self.props_code(context).split("\n")
props_register_list = self.props_register(context).split("\n")
props_unregister_list = self.props_unregister(context).split("\n")
idname = f"SNA_AddonPreferences_{self.static_uid}"
self.code = f"""
{self.indent(props_imperative_list, 5)}
class {idname}(bpy.types.AddonPreferences):
bl_idname = {"__package__" if bpy.context.scene.sn.is_exporting else "'"+bpy.context.scene.sn.module_name+"'"}
{self.indent(props_code_list, 6)}
def draw(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value if out.name == 'Preferences' else '' for out in self.outputs], 8)}
"""
self.code_register = f"""
{self.indent(props_register_list, 7)}
bpy.utils.register_class({idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({idname})
{self.indent(props_unregister_list, 7)}
"""
def draw_node(self, context, layout):
layout.operator("sn.open_preferences", icon="PREFERENCES").navigation = "CUSTOM"
amount = 0
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
amount += len(ntree.node_collection(self.bl_idname).refs)
if amount > 1:
layout.label(
text="Multiple preferences nodes! Only one will be used", icon="ERROR"
)
self.draw_list(layout)
@@ -0,0 +1,73 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToMenuNode"
bl_label = "Add To Menu (Legacy)"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_interface_output("Menu").prev_dynamic = True
self.add_dynamic_interface_output("Menu")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the menu"),
("APPEND", "Append", "Append this to the end of the menu")],
name="Position",
description="Position of this interface to the selected menu",
update=SN_ScriptingBaseNode._evaluate)
menu_parent: bpy.props.StringProperty(default="VIEW3D_MT_add",
name="Parent",
description="The menu id this interface should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.menu_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value for out in self.outputs[:-1]], 5)}
"""
if self.menu_parent == "WM_MT_button_context":
self.code_imperative = """
class WM_MT_button_context(bpy.types.Menu):
bl_label = "Unused"
def draw(self, context):
pass
"""
self.code_register = f"""
{"if not hasattr(bpy.types, 'WM_MT_button_context'): bpy.utils.register_class(WM_MT_button_context)" if self.menu_parent == "WM_MT_button_context" else ""}
bpy.types.{self.menu_parent}.{self.append.lower()}({func_name})
"""
self.code_unregister = f"""
bpy.types.{self.menu_parent}.remove({func_name})
"""
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_menu_picker", text=f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,61 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AddToPanelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AddToPanelNode"
bl_label = "Add To Panel (Legacy)"
bl_width_default = 200
def layout_type(self, _): return "layout"
is_trigger = True
node_color = "INTERFACE"
def on_create(self, context):
self.add_boolean_input("Hide")
self.add_interface_output("Panel").prev_dynamic = True
self.add_dynamic_interface_output("Panel")
append: bpy.props.EnumProperty(default="APPEND", items=[
("PREPEND", "Prepend", "Prepend this to the start of the panel"),
("APPEND", "Append", "Append this to the end of the panel")],
name="Position",
description="Position of this interface to the selected panel",
update=SN_ScriptingBaseNode._evaluate)
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
uid = self.uuid
func_name = f"sna_add_to_{self.panel_parent.lower()}_{uid}"
self.code = f"""
def {func_name}(self, context):
if not ({self.inputs["Hide"].python_value}):
layout = self.layout
{self.indent([out.python_value for out in self.outputs[:-1]], 7)}
"""
self.code_register = f"bpy.types.{self.panel_parent}.{self.append.lower()}({func_name})"
self.code_unregister = f"bpy.types.{self.panel_parent}.remove({func_name})"
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.3
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
layout.prop(self, "append", expand=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,164 @@
import bpy
from ...Program.Run_Operator import on_operator_ref_update
from ...base_node import SN_ScriptingBaseNode
class SN_ButtonNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ButtonNode"
bl_label = "Button (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.version = 1
self.add_interface_input()
self.add_string_input("Label").default_value = "My Button"
self.add_boolean_input("Emboss").default_value = True
self.add_boolean_input("Depress")
self.add_icon_input("Icon")
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
on_operator_ref_update(self, node, data, self.ref_ntree, self.ref_SN_OperatorNode, 5)
def reset_inputs(self):
""" Remove all operator inputs """
for i in range(len(self.inputs)-1, -1, -1):
inp = self.inputs[i]
if inp.can_be_disabled:
self.inputs.remove(inp)
def create_inputs(self, op_rna):
""" Create inputs for operator """
for prop in op_rna.properties:
if not prop.identifier in ["rna_type", "settings"]:
inp = self.add_input_from_property(prop)
if inp:
inp.can_be_disabled = True
inp.disabled = not prop.is_required
def update_custom_operator(self, context):
""" Updates the nodes settings when a new parent panel is selected """
self.reset_inputs()
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
parent = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
for prop in parent.properties:
if prop.property_type in ["Integer", "Float", "Boolean"] and prop.settings.is_vector:
socket = self._add_input(self.socket_names[prop.property_type + " Vector"], prop.name)
socket.size = prop.settings.size
socket.can_be_disabled = True
else:
self._add_input(self.socket_names[prop.property_type], prop.name).can_be_disabled = True
self._evaluate(context)
def update_source_type(self, context):
self.hide_disabled_inputs = False
if self.source_type == "BLENDER":
self.pasted_operator = self.pasted_operator
elif self.source_type == "CUSTOM":
self.ref_SN_OperatorNode = self.ref_SN_OperatorNode
self._evaluate(context)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the operator from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
source_type: bpy.props.EnumProperty(name="Source Type",
description="Use a custom operator or a blender internal",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=update_source_type)
ref_SN_OperatorNode: bpy.props.StringProperty(name="Custom Operator",
description="The operator ran by this button",
update=update_custom_operator)
def update_pasted_operator(self, context):
self.reset_inputs()
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
self.pasted_name = op_rna.name
self.create_inputs(op_rna)
self._evaluate(context)
pasted_operator: bpy.props.StringProperty(default="bpy.ops.sn.dummy_button_operator()",
update=update_pasted_operator)
pasted_name: bpy.props.StringProperty(default="Paste Operator")
def update_hide_disabled_inputs(self, context):
for inp in self.inputs:
if inp.can_be_disabled and inp.disabled:
inp.set_hide(self.hide_disabled_inputs)
hide_disabled_inputs: bpy.props.BoolProperty(default=False,
name="Hide Disabled Inputs",
description="Hides the disabled inputs of this node",
update=update_hide_disabled_inputs)
def evaluate(self, context):
if self.source_type == "BLENDER":
op_name = self.pasted_operator[8:].split("(")[0]
self.code = f"op = {self.active_layout}.operator('{op_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in op_rna.properties:
if (self.version == 0 and (prop.name and prop.name == inp.name) or (not prop.name and prop.identifier.replace("_", " ").title() == inp.name)) \
or (self.version == 1 and inp.name.replace(" ", "_").lower() == prop.identifier):
self.code += "\n" + f"op.{prop.identifier} = {inp.python_value}"
else:
if self.ref_ntree and self.ref_SN_OperatorNode in self.ref_ntree.nodes:
node = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
self.code = f"op = {self.active_layout}.operator('sna.{node.operator_python_name}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
for prop in node.properties:
if prop.name == inp.name:
self.code += "\n" + f"op.{prop.python_name} = {inp.python_value}"
else:
self.code = f"op = {self.active_layout}.operator('sn.dummy_button_operator', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}, depress={self.inputs['Depress'].python_value})"
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "source_type", text="", icon_only=True)
if self.source_type == "BLENDER":
name = "Paste Operator"
if self.pasted_operator:
if self.pasted_name:
name = self.pasted_name
elif len(self.pasted_operator.split(".")) > 2:
name = self.pasted_operator.split(".")[3].split("(")[0].replace("_", " ").title()
else:
name = self.pasted_operator
op = row.operator("sn.paste_operator", text=name, icon="PASTEDOWN")
op.node_tree = self.node_tree.name
op.node = self.name
elif self.source_type == "CUSTOM":
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OperatorNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_OperatorNode"), "refs", text="")
row.prop(self, "hide_disabled_inputs", text="", icon="HIDE_ON" if self.hide_disabled_inputs else "HIDE_OFF", emboss=False)
@@ -0,0 +1,40 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CopyMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CopyMenuNode"
bl_label = "Copy Menu (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
menu_parent: bpy.props.StringProperty(name="Menu",
default="VIEW3D_MT_add",
description="The menu that should be displayed",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code = f"""
if hasattr(bpy.types,"{self.menu_parent}"):
if not hasattr(bpy.types.{self.menu_parent}, "poll") or bpy.types.{self.menu_parent}.poll(context):
bpy.types.{self.menu_parent}.draw(self, context)
else:
{self.active_layout}.label(text="Can't display this menu here!", icon="ERROR")
else:
{self.active_layout}.label(text="Can't display this menu!", icon="ERROR")
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
name = f"{self.menu_parent.replace('_MT_', ' ').replace('_', ' ').title()}"
op = row.operator("sn.activate_menu_picker", icon="EYEDROPPER", text=name)
op.node_tree = self.node_tree.name
op.node = self.name
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,40 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CopyPanelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CopyPanelNode"
bl_label = "Copy Panel (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
panel_parent: bpy.props.StringProperty(default="EEVEE_MATERIAL_PT_surface",
name="Parent",
description="The panel id this subpanel should be shown in",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code = f"""
if hasattr(bpy.types,"{self.panel_parent}"):
if not hasattr(bpy.types.{self.panel_parent}, "poll") or bpy.types.{self.panel_parent}.poll(context):
bpy.types.{self.panel_parent}.draw(self, context)
else:
{self.active_layout}.label(text="Can't display this panel here!", icon="ERROR")
else:
{self.active_layout}.label(text="Can't display this panel!", icon="ERROR")
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
op = row.operator("sn.activate_subpanel_picker", text=f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}", icon="EYEDROPPER")
op.node_tree = self.node_tree.name
op.node = self.name
op.allow_subpanels = True
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,48 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....utils import get_python_name
class SN_DisplayCollectionListNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayCollectionListNode"
bl_label = "Display Collection List (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def layout_type(self, _): return "layout"
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input()
self.add_property_input("Index Property")
self.add_integer_input("Rows")
self.add_interface_output("Item Row").prev_dynamic = True
self.add_dynamic_interface_output("Item Row")
self.add_property_output("Item")
self.add_integer_output("Item Index")
def evaluate(self, context):
if self.inputs["Index Property"].is_linked and self.inputs["Collection Property"].is_linked:
ui_list_idname = f"SNA_UL_{get_python_name(self.name, 'List')}_{self.static_uid}"
self.code_imperative = f"""
class {ui_list_idname}(bpy.types.UIList):
def draw_item(self, context, layout, data, item_{self.static_uid}, icon, active_data, active_propname, index_{self.static_uid}):
row = layout
{self.indent([out.python_value for out in self.outputs[:-2]], 11)}
"""
self.code_register = f"""
bpy.utils.register_class({ui_list_idname})
"""
self.code_unregister = f"""
bpy.utils.unregister_class({ui_list_idname})
"""
self.code = f"{self.active_layout}.template_list('{ui_list_idname}', '{self.static_uid}', {self.inputs['Collection Property'].python_source}, '{self.inputs['Collection Property'].python_attr}', {self.inputs['Index Property'].python_source}, '{self.inputs['Index Property'].python_attr}', rows={self.inputs['Rows'].python_value})"
self.outputs["Item"].python_value = f"item_{self.static_uid}"
if "Item Index" in self.outputs:
self.outputs["Item Index"].python_value = f"index_{self.static_uid}"
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
self.outputs["Item"].reset_value()
if "Item Index" in self.outputs:
self.outputs["Item Index"].reset_value()
@@ -0,0 +1,21 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplayIconNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayIconNode"
bl_label = "Display Icon (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_icon_input("Icon")
self.add_float_input("Scale").default_value = 1
def evaluate(self, context):
self.code = f"{self.active_layout}.template_icon(icon_value={self.inputs['Icon'].python_value}, scale={self.inputs['Scale'].python_value})"
def draw_node(self, context, layout):
layout.label(text="Use this node with care! This might slow down your UI", icon="INFO")
@@ -0,0 +1,29 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplayPreviewNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPreviewNode"
bl_label = "Display Preview (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_boolean_input("Show Buttons")
def evaluate(self, context):
if self.inputs["Property"].is_linked:
self.code = f"""
{self.active_layout}.template_preview({self.inputs['Property'].python_value}, show_buttons={self.inputs['Show Buttons'].python_value})
"""
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
def draw_node(self, context, layout):
box = layout.box()
box.label(text="Careful! This element is prone to crashes!", icon="INFO")
box.label(text="Only materials, textures, lights, worlds and line styles can be displayed")
@@ -0,0 +1,55 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplayPropertyNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplayPropertyNode"
bl_label = "Display Property (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input()
self.add_string_input("Label")
self.add_icon_input("Icon")
self.add_boolean_input("Emboss").default_value = True
inp = self.add_boolean_input("Expand")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Slider")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Toggle")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Invert Checkbox")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_boolean_input("Full Shortcut")
inp.default_value = False
inp.can_be_disabled = True
inp.disabled = True
inp = self.add_integer_input("Index")
inp.can_be_disabled = True
inp.disabled = True
def evaluate(self, context):
if self.inputs["Property"].is_linked:
attributes = ""
for inp in self.inputs:
if inp.can_be_disabled and not inp.disabled:
attributes += f", {inp.name.lower().replace(' ', '_')}={inp.python_value}"
self.code = f"""
{self.active_layout}.prop({self.inputs['Property'].python_source}, '{self.inputs['Property'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, icon_value={self.inputs['Icon'].python_value}, emboss={self.inputs['Emboss'].python_value}{attributes})
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
"""
@@ -0,0 +1,24 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplaySearchNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySearchNode"
bl_label = "Display Search (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_collection_property_input("Collection")
self.add_property_input("String/Pointer")
self.add_string_input("Label")
self.add_icon_input("Blender Icon").subtype = "BLENDER_ONLY"
def evaluate(self, context):
if len(self.inputs["String/Pointer"].links) and len(self.inputs["Collection"].links):
self.code = f"""{self.active_layout}.prop_search({self.inputs['String/Pointer'].python_source}, '{self.inputs['String/Pointer'].python_attr.replace("'",'"')}', {self.inputs['Collection'].python_source}, '{self.inputs['Collection'].python_attr.replace("'",'"')}', text={self.inputs['Label'].python_value}, icon={self.inputs['Blender Icon'].python_value})"""
else:
self.code = f"{self.active_layout}.label(text='No Property connected!', icon='ERROR')"
@@ -0,0 +1,58 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DisplaySerpensShortcutNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DisplaySerpensShortcutNode"
bl_label = "Display Serpens Shortcut (Legacy)"
node_color = "INTERFACE"
bl_width_default = 200
def on_create(self, context):
self.add_interface_input()
self.add_string_input("Label")
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Panel Node Tree",
description="The node tree to select the panel from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
ref_SN_OnKeypressNode: bpy.props.StringProperty(name="Shortcut",
description="The shortcut to display",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.ref_ntree and self.ref_SN_OnKeypressNode in self.ref_ntree.nodes:
self.code_imperative = """
def find_user_keyconfig(key):
km, kmi = addon_keymaps[key]
for item in bpy.context.window_manager.keyconfigs.user.keymaps[km.name].keymap_items:
found_item = False
if kmi.idname == item.idname:
found_item = True
for name in dir(kmi.properties):
if not name in ["bl_rna", "rna_type"] and not name[0] == "_":
if not kmi.properties[name] == item.properties[name]:
found_item = False
if found_item:
return item
print(f"Couldn't find keymap item for {key}, using addon keymap instead. This won't be saved across sessions!")
return kmi
"""
node = self.ref_ntree.nodes[self.ref_SN_OnKeypressNode]
src = f"find_user_keyconfig('{node.static_uid}')"
self.code = f"{self.active_layout}.prop({src}, 'type', text={self.inputs['Label'].python_value}, full_event=True)"
def draw_node(self, context, layout):
row = layout.row(align=True)
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
row.prop_search(self, "ref_ntree", bpy.data, "node_groups", text="")
subrow = row.row(align=True)
subrow.enabled = self.ref_ntree != None
subrow.prop_search(self, "ref_SN_OnKeypressNode", parent_tree.node_collection("SN_OnKeypressNode"), "refs", text="")
layout.label(text="Copy blender shortcuts from the blend data browser in display property", icon="INFO")
@@ -0,0 +1,27 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IconGalleryNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IconGalleryNode"
bl_label = "Icon Gallery (Legacy)"
node_color = "INTERFACE"
def on_create(self, context):
self.add_interface_input()
self.add_property_input("Enum")
self.add_boolean_input("Show Labels")
self.add_float_input("Scale").default_value = 5
self.add_float_input("Scale Popup").default_value = 5
def evaluate(self, context):
if self.inputs["Enum"].is_linked:
self.code = f"""
{self.active_layout}.template_icon_view({self.inputs['Enum'].python_source}, '{self.inputs['Enum'].python_attr}', show_labels={self.inputs['Show Labels'].python_value}, scale={self.inputs['Scale'].python_value}, scale_popup={self.inputs['Scale Popup'].python_value})
"""
else:
self.code = f"""
{self.active_layout}.label(text='No Property connected!', icon='ERROR')
"""
@@ -0,0 +1,29 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IfElseInterfaceNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IfElseInterfaceNode"
bl_label = "If/Else (Interface) (Legacy)"
bl_width_default = 200
node_color = "INTERFACE"
passthrough_layout_type = True
def on_create(self, context):
self.add_interface_input()
self.add_boolean_input("Condition").default_value = True
self.add_interface_output("True")
self.add_interface_output("False")
self.add_interface_output("Continue")
def evaluate(self, context):
self.code = f"""
if {self.inputs['Condition'].python_value}:
{self.indent(self.outputs['True'].python_value, 6) if self.outputs['True'].python_value.strip() else 'pass'}
{"else:" if self.outputs['False'].python_value.strip() else ""}
{self.indent(self.outputs['False'].python_value, 6) if self.outputs['False'].python_value.strip() else ''}
{self.indent(self.outputs['Continue'].python_value, 5)}
"""

Some files were not shown because too many files have changed in this diff Show More