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,61 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DrawCircleNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DrawCircleNode"
bl_label = "Draw Circle"
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):
self.add_execute_input()
inp = self.add_float_vector_input("Color")
inp.subtype = "COLOR_ALPHA"
self.add_float_input("Radius").default_value = 1
self.add_float_input("Width").default_value = 1
self.add_integer_input("Segments").default_value = 32
self.add_enum_input("On Top")["items"] = str(["NONE", "ALWAYS", "LESS", "LESS_EQUAL", "EQUAL", "GREATER", "GREATER_EQUAL"])
inp = self.add_float_vector_input("Location")
inp.size = 2
inp.default_value[0] = 0
inp.default_value[1] = 0
inp.default_value[2] = 0
self.add_execute_output()
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
def evaluate(self, context):
self.code_import = f"""
import gpu
import gpu_extras.presets as gpu_extras_presets
"""
self.code = f"""
gpu.state.line_width_set({self.inputs["Width"].python_value})
gpu.state.depth_test_set({self.inputs["On Top"].python_value})
gpu.state.depth_mask_set(True)
gpu.state.blend_set('ALPHA')
gpu_extras_presets.draw_circle_2d({self.inputs["Location"].python_value}, {self.inputs["Color"].python_value}, {self.inputs["Radius"].python_value}, segments={self.inputs["Segments"].python_value})
{self.indent(self.outputs[0].python_value, 3)}
"""
@@ -0,0 +1,120 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DrawLineNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DrawLineNode"
bl_label = "Draw 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 update_use_loc_list(self, context):
self.inputs["Line Locations"].set_hide(not self.use_loc_list)
for inp in self.inputs:
if inp.bl_label == "Float Vector" and inp.subtype == "NONE":
inp.set_hide(self.use_loc_list)
self._evaluate(context)
use_loc_list: bpy.props.BoolProperty(
name="Draw Multiple",
description="Whether to draw multiple points (this is more efficient than separate nodes)",
default=False,
update=update_use_loc_list,
)
def on_create(self, context):
self.add_execute_input()
inp = self.add_float_vector_input("Color")
inp.subtype = "COLOR_ALPHA"
self.add_float_input("Width").default_value = 1
self.add_enum_input("On Top")["items"] = str(
[
"NONE",
"ALWAYS",
"LESS",
"LESS_EQUAL",
"EQUAL",
"GREATER",
"GREATER_EQUAL",
]
)
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
inp = self.add_list_input("Line Locations")
inp.set_hide(True)
self.add_execute_output()
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
layout.prop(self, "use_loc_list", text="Draw Multiple")
def evaluate(self, context):
self.code_import = f"""
import gpu
import gpu_extras
"""
p1 = self.inputs["Point 1"].python_value
p2 = self.inputs["Point 2"].python_value
lines = f"""
lines = [({p1}, {p2})]
"""
if self.use_loc_list:
lines = f"lines = {self.inputs['Line Locations'].python_value}"
coords = f"""
{lines}
coords = []
indices = []
for i, line in enumerate(lines):
coords.extend(line)
indices.append((i*2, i*2+1))
"""
self.code = f"""
{coords}
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = gpu_extras.batch.batch_for_shader(shader, 'LINES', {{"pos": coords}}, indices=tuple(indices))
shader.bind()
shader.uniform_float("color", {self.inputs["Color"].python_value})
gpu.state.line_width_set({self.inputs["Width"].python_value})
{f"gpu.state.depth_test_set({self.inputs['On Top'].python_value})" if self.use_3d else ""}
{"gpu.state.depth_mask_set(True)" if self.use_3d else ""}
gpu.state.blend_set('ALPHA')
batch.draw(shader)
{self.indent(self.outputs[0].python_value, 3)}
"""
@@ -0,0 +1,101 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DrawPointNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DrawPointNode"
bl_label = "Draw Point"
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 update_use_loc_list(self, context):
if self.use_loc_list:
self.convert_socket(self.inputs["Location"], self.socket_names["List"])
else:
self.convert_socket(
self.inputs["Location"], self.socket_names["Float Vector"]
)
self._evaluate(context)
use_loc_list: bpy.props.BoolProperty(
name="Draw Multiple",
description="Whether to draw multiple points (this is more efficient than separate nodes)",
default=False,
update=update_use_loc_list,
)
def on_create(self, context):
self.add_execute_input()
inp = self.add_float_vector_input("Color")
inp.subtype = "COLOR_ALPHA"
self.add_float_input("Size").default_value = 1
self.add_enum_input("On Top")["items"] = str(
[
"NONE",
"ALWAYS",
"LESS",
"LESS_EQUAL",
"EQUAL",
"GREATER",
"GREATER_EQUAL",
]
)
inp = self.add_float_vector_input("Location")
inp.size = 2
inp.default_value[0] = 0
inp.default_value[1] = 0
inp.default_value[2] = 0
self.add_execute_output()
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
layout.prop(self, "use_loc_list", text="Draw Multiple")
def evaluate(self, context):
self.code_import = f"""
import gpu
import gpu_extras
"""
coords = f"coords = ()"
loc_inp = self.inputs["Location"]
if loc_inp.bl_label == "Float Vector":
coords = f"coords = ({loc_inp.python_value}, )"
else:
coords = f"coords = tuple({loc_inp.python_value})"
self.code = f"""
{coords}
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = gpu_extras.batch.batch_for_shader(shader, 'POINTS', {{"pos": coords}})
shader.bind()
shader.uniform_float("color", {self.inputs["Color"].python_value})
gpu.state.point_size_set({self.inputs["Size"].python_value})
gpu.state.depth_test_set({self.inputs["On Top"].python_value})
gpu.state.depth_mask_set(True)
gpu.state.blend_set('ALPHA')
batch.draw(shader)
{self.indent(self.outputs[0].python_value, 3)}
"""
@@ -0,0 +1,136 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DrawQuadNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DrawQuadNode"
bl_label = "Draw 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.inputs["On Top"].set_hide(not self.use_3d)
self.inputs["Backface Culling"].set_hide(not self.use_3d)
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 update_use_loc_list(self, context):
self.inputs["Quad Locations"].set_hide(not self.use_loc_list)
for inp in self.inputs:
if inp.bl_label == "Float Vector" and inp.subtype == "NONE":
inp.set_hide(self.use_loc_list)
self._evaluate(context)
use_loc_list: bpy.props.BoolProperty(
name="Draw Multiple",
description="Whether to draw multiple points (this is more efficient than separate nodes)",
default=False,
update=update_use_loc_list,
)
def on_create(self, context):
self.add_execute_input()
inp = self.add_float_vector_input("Color")
inp.subtype = "COLOR_ALPHA"
self.add_enum_input("On Top")["items"] = str(
[
"NONE",
"ALWAYS",
"LESS",
"LESS_EQUAL",
"EQUAL",
"GREATER",
"GREATER_EQUAL",
]
)
self.add_boolean_input("Backface Culling").default_value = True
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
inp = self.add_list_input("Quad Locations")
inp.set_hide(True)
self.add_execute_output()
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
layout.prop(self, "use_loc_list", text="Draw Multiple")
def evaluate(self, context):
self.code_import = f"""
import gpu
import gpu_extras
"""
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
quad_locations = (
f"quads = [[tuple({bl}), tuple({br}), tuple({tl}), tuple({tr})]]"
)
if self.use_loc_list:
quad_locations = f"quads = {self.inputs['Quad Locations'].python_value}"
verts = f"""
{quad_locations}
vertices = []
indices = []
for i_{self.static_uid}, quad in enumerate(quads):
vertices.extend(quad)
indices.extend([(i_{self.static_uid} * 4, i_{self.static_uid} * 4 + 1, i_{self.static_uid} * 4 + 2), (i_{self.static_uid} * 4 + 2, i_{self.static_uid} * 4 + 1, i_{self.static_uid} * 4 + 3)])
"""
self.code = f"""
{verts}
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = gpu_extras.batch.batch_for_shader(shader, 'TRIS', {{"pos": tuple(vertices)}}, indices=tuple(indices))
shader.bind()
shader.uniform_float("color", {self.inputs["Color"].python_value})
{f"gpu.state.depth_test_set({self.inputs['On Top'].python_value})" if self.use_3d else ""}
{"gpu.state.depth_mask_set(True)" if self.use_3d else ""}
{f"gpu.state.face_culling_set('BACK' if {self.inputs['Backface Culling'].python_value} else 'NONE')" if self.use_3d else ""}
gpu.state.blend_set('ALPHA')
batch.draw(shader)
{self.indent(self.outputs[0].python_value, 3)}
"""
@@ -0,0 +1,83 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DrawModalTextNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DrawModalTextNode"
bl_label = "Draw Text"
node_color = "PROGRAM"
def update_use3d(self, context):
if self.use_3d:
self.inputs["Position"].size = 3
else:
self.inputs["Position"].size = 2
self._evaluate(context)
use_3d: bpy.props.BoolProperty(name="Use 3D", default=False, description="Use 3D coordinates", update=update_use3d)
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_string_input("Text").default_value = "My Text"
self.add_string_input("Font").subtype = "FILE_PATH"
inp = self.add_float_vector_input("Text Color")
inp.subtype = "COLOR_ALPHA"
inp.default_value = tuple([1]*32)
self.add_float_input("Size").default_value = 20
self.add_integer_input("DPI").default_value = 72
self.add_integer_input("Wrap Width").default_value = 0
inp = self.add_float_vector_input("Position")
inp.size = 2
inp.default_value = [1]*32
self.add_float_input("Rotation")
def draw_node(self, context, layout):
layout.prop(self, "use_3d")
def evaluate(self, context):
position_code = f"x_{self.static_uid}, y_{self.static_uid} = {self.inputs['Position'].python_value}"
if self.use_3d:
position_code = f"x_{self.static_uid}, y_{self.static_uid} = location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, tuple({self.inputs['Position'].python_value}))"
self.code = f"""
font_id = 0
if {self.inputs["Font"].python_value} and os.path.exists({self.inputs["Font"].python_value}):
font_id = blf.load({self.inputs["Font"].python_value})
if font_id == -1:
print("Couldn't load font!")
else:
{self.indent(position_code, 4)}
blf.position(font_id, x_{self.static_uid}, y_{self.static_uid}, 0)
if bpy.app.version >= (3, 4, 0):
blf.size(font_id, {self.inputs["Size"].python_value})
else:
blf.size(font_id, {self.inputs["Size"].python_value}, {self.inputs["DPI"].python_value})
clr = {self.inputs["Text Color"].python_value}
blf.color(font_id, clr[0], clr[1], clr[2], clr[3])
if {self.inputs["Wrap Width"].python_value if "Wrap Width" in self.inputs else "False"}:
blf.enable(font_id, blf.WORD_WRAP)
blf.word_wrap(font_id, {self.inputs["Wrap Width"].python_value if "Wrap Width" in self.inputs else "0"})
if {self.inputs["Rotation"].python_value if "Rotation" in self.inputs else "False"}:
blf.enable(font_id, blf.ROTATION)
blf.rotation(font_id, {self.inputs["Rotation"].python_value if "Rotation" in self.inputs else "0"})
blf.enable(font_id, blf.WORD_WRAP)
blf.draw(font_id, {self.inputs["Text"].python_value})
blf.disable(font_id, blf.ROTATION)
blf.disable(font_id, blf.WORD_WRAP)
{self.indent(self.outputs[0].python_value, 3)}
"""
self.code_import = f"""
import blf
import os
{"from bpy_extras.view3d_utils import location_3d_to_region_2d" if self.use_3d else ""}
"""
@@ -0,0 +1,126 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_DrawTriangleNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_DrawTriangleNode"
bl_label = "Draw 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.inputs["On Top"].set_hide(not self.use_3d)
self.inputs["Backface Culling"].set_hide(not self.use_3d)
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 update_use_loc_list(self, context):
self.inputs["Triangle Locations"].set_hide(not self.use_loc_list)
for inp in self.inputs:
if inp.bl_label == "Float Vector" and inp.subtype == "NONE":
inp.set_hide(self.use_loc_list)
self._evaluate(context)
use_loc_list: bpy.props.BoolProperty(
name="Draw Multiple",
description="Whether to draw multiple points (this is more efficient than separate nodes)",
default=False,
update=update_use_loc_list,
)
def on_create(self, context):
self.add_execute_input()
inp = self.add_float_vector_input("Color")
inp.subtype = "COLOR_ALPHA"
self.add_enum_input("On Top")["items"] = str(
[
"NONE",
"ALWAYS",
"LESS",
"LESS_EQUAL",
"EQUAL",
"GREATER",
"GREATER_EQUAL",
]
)
self.add_boolean_input("Backface Culling").default_value = True
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
inp = self.add_list_input("Triangle Locations")
inp.set_hide(True)
self.add_execute_output()
def draw_node(self, context, layout):
layout.prop(self, "use_3d", text="Use 3D")
layout.prop(self, "use_loc_list", text="Draw Multiple")
def evaluate(self, context):
self.code_import = f"""
import gpu
import gpu_extras
"""
c1 = self.inputs["Corner 1"].python_value
c2 = self.inputs["Corner 2"].python_value
c3 = self.inputs["Corner 3"].python_value
tria_locations = f"trias = [[tuple({c1}), tuple({c2}), tuple({c3})]]"
if self.use_loc_list:
tria_locations = f"trias = {self.inputs['Triangle Locations'].python_value}"
verts = f"""
{tria_locations}
vertices = []
indices = []
for i_{self.static_uid}, tria in enumerate(trias):
vertices.extend(tria)
indices.extend([(i_{self.static_uid} * 3, i_{self.static_uid} * 3 + 1, i_{self.static_uid} * 3 + 2)])
"""
self.code = f"""
{verts}
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = gpu_extras.batch.batch_for_shader(shader, 'TRIS', {{"pos": tuple(vertices)}}, indices=tuple(indices))
shader.bind()
shader.uniform_float("color", {self.inputs["Color"].python_value})
{f"gpu.state.depth_test_set({self.inputs['On Top'].python_value})" if self.use_3d else ""}
{"gpu.state.depth_mask_set(True)" if self.use_3d else ""}
{f"gpu.state.face_culling_set('BACK' if {self.inputs['Backface Culling'].python_value} else 'NONE')" if self.use_3d else ""}
gpu.state.blend_set('ALPHA')
batch.draw(shader)
{self.indent(self.outputs[0].python_value, 3)}
"""
@@ -0,0 +1,56 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_EndDrawingNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_EndDrawingNode"
bl_label = "End Drawing"
node_color = "PROGRAM"
bl_width_default = 200
ref_SN_StartDrawingNode: bpy.props.StringProperty(name="Handler",
description="The handler to stop",
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Handler Node Tree",
description="The node tree to select the handler from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
def on_ref_update(self, node, data=None):
self._evaluate(bpy.context)
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.ref_ntree = self.node_tree
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_StartDrawingNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_StartDrawingNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_StartDrawingNode 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_StartDrawingNode
def evaluate(self, context):
handler = None
if self.ref_ntree and self.ref_SN_StartDrawingNode in self.ref_ntree.nodes:
handler = self.ref_ntree.nodes[self.ref_SN_StartDrawingNode]
self.code = f"""
if handler_{handler.static_uid}:
bpy.types.{handler.draw_space}.draw_handler_remove(handler_{handler.static_uid}[0], 'WINDOW')
handler_{handler.static_uid}.pop(0)
for a in bpy.context.screen.areas: a.tag_redraw()
{self.indent(self.outputs[0].python_value, 4)}
"""
@@ -0,0 +1,181 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_StartDrawingNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_StartDrawingNode"
bl_label = "Start Drawing"
node_color = "PROGRAM"
bl_width_default = 200
draw_type: bpy.props.EnumProperty(
name="Draw Type",
description="The type of drawing that should be started",
items=[("POST_PIXEL", "2D", "Post Pixel"), ("POST_VIEW", "3D", "Post View"), ("BACKDROP", "Backdrop", "Backdrop for node editors")],
default="POST_PIXEL",
update=SN_ScriptingBaseNode._evaluate)
def update_enum_socket(self, from_socket, to_socket):
to_socket.subtype = "CUSTOM_ITEMS"
to_socket.custom_items_editable = False
to_socket.custom_items.clear()
for item in from_socket.custom_items:
new = to_socket.custom_items.add()
new.name = item.name
def update_vector_socket(self, from_socket, to_socket):
to_socket.size = from_socket.size
to_socket.subtype = from_socket.subtype
def on_ref_update(self, node, data=None):
if node.bl_idname == "SN_FunctionNode" and data:
# 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)
# 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], data["changed"].bl_idname)
# update enum items
if data["changed"].bl_label == "Enum" or data["changed"].bl_label == "Enum Set":
self.update_enum_socket(data["changed"], self.inputs[data["changed"].index])
elif "Vector" in data["changed"].bl_label:
self.update_vector_socket(data["changed"], self.inputs[data["changed"].index])
# input has updated
elif "updated" in data:
self.inputs[data["updated"].index].name = data["updated"].name
self._evaluate(bpy.context)
elif node.bl_idname == "SN_FunctionReturnNode" and data:
# output has been added
if "added" in data:
socket_index = list(data["added"].node.inputs).index(data["added"])
self.add_output_from_socket(data["added"])
self.outputs.move(len(self.outputs)-1, socket_index)
# output has been removed
elif "removed" in data:
self.outputs.remove(self.outputs[data["removed"]])
# output has changed
elif "changed" in data:
self.convert_socket(self.outputs[data["changed"].index], data["changed"].bl_idname)
# output has updated
elif "updated" in data:
self.outputs[data["updated"].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_FunctionNode in parent_tree.nodes:
for out in parent_tree.nodes[self.ref_SN_FunctionNode].outputs[1:-1]:
inp = self.add_input_from_socket(out)
# update enum items
if out.bl_label == "Enum" or out.bl_label == "Enum Set":
self.update_enum_socket(out, inp)
elif "Vector" in out.bl_label:
self.update_vector_socket(out, inp)
# 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)
def update_references(self, context):
self.update_function_reference(context)
self.trigger_ref_update(self)
self._evaluate(context)
ref_SN_FunctionNode: bpy.props.StringProperty(name="Function",
description="The function to run",
update=update_references)
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_space_items(self, context):
items = []
names = ["SpaceView3D", "SpaceNodeEditor", "SpaceClipEditor", "SpaceConsole", "SpaceDopeSheetEditor", "SpaceFileBrowser",
"SpaceGraphEditor", "SpaceImageEditor", "SpaceInfo", "SpaceNLA", "SpaceOutliner", "SpacePreferences",
"SpaceProperties", "SpaceSequenceEditor", "SpaceSpreadsheet", "SpaceTextEditor"]
for name in names:
items.append((name, name, name))
return items
draw_space: bpy.props.EnumProperty(name="Draw Space",
description="The space this operator can run in and the text is drawn in",
update=update_references,
items=draw_space_items)
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.ref_ntree = self.node_tree
def draw_node(self, context, layout):
layout.prop(self, "name")
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_FunctionNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_FunctionNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_FunctionNode 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_FunctionNode
layout.prop(self, "draw_type")
layout.prop(self, "draw_space", text="Space")
def evaluate(self, context):
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]
# get input values
inp_values = []
for inp in self.inputs[1:]:
inp_values.append(inp.python_value)
inp_values = ", ".join(inp_values)
if inp_values:
inp_values += ", "
self.code_imperative = f"""
handler_{self.static_uid} = []
"""
self.code = f"""
handler_{self.static_uid}.append(bpy.types.{self.draw_space}.draw_handler_add({func.func_name}, ({inp_values}), 'WINDOW', '{self.draw_type}'))
for a in bpy.context.screen.areas: a.tag_redraw()
{self.indent(self.outputs[0].python_value, 4)}
"""
self.code_unregister = f"""
if handler_{self.static_uid}:
bpy.types.{self.draw_space}.draw_handler_remove(handler_{self.static_uid}[0], 'WINDOW')
handler_{self.static_uid}.pop(0)
"""
@@ -0,0 +1,52 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_AppendFromFileNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_AppendFromFileNode"
bl_label = "Append From File"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_string_input("Path").subtype = "FILE_PATH"
self.add_string_input("Name")
self.add_boolean_input("Linked")
self.add_property_output("Appended")
append_names = ["Action", "Brush", "Camera", "Collection", "FreestyleLineStyle", "Image", "Light", "Material",
"Mesh", "NodeTree", "Object", "Palette", "Scene", "Text", "Texture", "WorkSpace", "World"]
data_names = ["actions", "brushes", "cameras", "collections", "linestyles", "images", "lights", "materials",
"meshes", "node_groups", "objects", "palettes", "scenes", "texts", "textures", "workspaces", "worlds"]
def get_append_types(self, context):
items = self.append_names
tuple_items = []
for item in items:
tuple_items.append((item, item.replace("_", " ").title(), item))
return tuple_items
append_type: bpy.props.EnumProperty(items=get_append_types, name="Type", description="Type of the Append object", update=SN_ScriptingBaseNode._evaluate)
def draw_node(self, context, layout):
layout.prop(self, "append_type")
def evaluate(self, context):
filepath=f"{self.inputs[1].python_value} + r'\{self.append_type}'"
data_name = self.data_names[self.append_names.index(self.append_type)]
self.code = f"""
before_data = list(bpy.data.{data_name})
bpy.ops.wm.append(directory={filepath}, filename={self.inputs[2].python_value}, link={self.inputs[3].python_value})
new_data = list(filter(lambda d: not d in before_data, list(bpy.data.{data_name})))
appended_{self.static_uid} = None if not new_data else new_data[0]
{self.indent(self.outputs[0].python_value, 5)}
"""
if "Appended" in self.outputs:
self.outputs["Appended"].python_value = f"appended_{self.static_uid}"
@@ -0,0 +1,36 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_CreateFolderNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_CreateFolderNode"
bl_label = "Create Folder"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_string_input("Path").subtype = "DIR_PATH"
self.add_string_input("Name").default_value = "New Folder"
self.add_execute_output()
self.add_string_output("New Path").subtype = "DIR_PATH"
def evaluate(self, context):
path = self.inputs["Path"].python_value
name = self.inputs["Name"].python_value
if path and name:
self.code_import = "import os"
self.outputs["New Path"].python_value = f"os.path.join({path}, {name})" if path and name else ""
self.code = f"""
if not os.path.exists(os.path.join({path}, {name})):
os.mkdir(os.path.join({path}, {name}))
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.outputs["New Path"].python_value = ""
self.code = f"""
{self.indent(self.outputs[0].python_value, 6)}
"""
@@ -0,0 +1,35 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ReadTextFileNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ReadTextFileNode"
bl_label = "Read Text File"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_string_input("Path").subtype = "FILE_PATH"
self.add_string_output("Text")
self.add_list_output("Lines")
def evaluate(self, context):
path = self.inputs["Path"].python_value
self.code_import = "import os"
self.code = f"""
text_{self.static_uid} = ""
lines_{self.static_uid} = []
if os.path.exists({path}):
with open({path}, "r") as file_{self.static_uid}:
lines_{self.static_uid} = list(map(lambda l: l.strip(), file_{self.static_uid}.readlines()))
text_{self.static_uid} = "\\n".join(lines_{self.static_uid})
{self.indent(self.outputs[0].python_value, 5)}
"""
self.outputs["Text"].python_value = f"text_{self.static_uid}"
self.outputs["Lines"].python_value = f"lines_{self.static_uid}"
@@ -0,0 +1,49 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_WriteTextFileNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_WriteTextFileNode"
bl_label = "Write Text File"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_string_input("Path").subtype = "FILE_PATH"
self.add_string_input("Text")
write_type: bpy.props.EnumProperty(name="Type",
description="Where to write to in the text file",
items=[("APPEND", "Append", "Append text to the end of the file"),
("OVERWRITE", "Overwrite", "Overwrite the content of the file")],
default="APPEND",
update=SN_ScriptingBaseNode._evaluate)
def draw_node(self, context, layout):
layout.prop(self, "write_type", expand=True)
def evaluate(self, context):
path = self.inputs["Path"].python_value
self.code_import = "import os"
if self.write_type == "APPEND":
self.code = f"""
with open({path}, mode='a') as file_{self.static_uid}:
{f"file_{self.static_uid}.seek(0)" if self.write_type == "OVERWRITE" else ""}
file_{self.static_uid}.write({self.inputs["Text"].python_value})
{f"file_{self.static_uid}.truncate()" if self.write_type == "OVERWRITE" else ""}
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
with open({path}, mode='w') as file_{self.static_uid}:
file_{self.static_uid}.seek(0)
file_{self.static_uid}.write({self.inputs["Text"].python_value})
file_{self.static_uid}.truncate()
{self.indent(self.outputs[0].python_value, 6)}
"""
@@ -0,0 +1,58 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...utils import get_python_name, unique_collection_name
class SN_FunctionReturnNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_FunctionReturnNode"
bl_label = "Function Return (Execute)"
bl_width_default = 200
def on_create(self, context):
self.add_execute_input()
out = self.add_dynamic_data_input("Output")
out.is_variable = True
out.changeable = True
def on_dynamic_socket_add(self, socket):
socket["name"] = get_python_name(socket.name, "Output", lower=False)
socket["name"] = unique_collection_name(socket.name, "Output", [inp.name for inp in self.inputs[1:-1]], "_", includes_name=True)
self.trigger_ref_update({ "added": socket })
self._evaluate(bpy.context)
def on_dynamic_socket_remove(self, index, is_output):
self.trigger_ref_update({ "removed": index })
self._evaluate(bpy.context)
def on_socket_type_change(self, socket):
self.trigger_ref_update({ "changed": socket })
self._evaluate(bpy.context)
def on_socket_name_change(self, socket):
socket["name"] = get_python_name(socket.name, "Output", lower=False)
socket["name"] = unique_collection_name(socket.name, "Output", [inp.name for inp in self.inputs[1:-1]], "_", includes_name=True)
self.trigger_ref_update({ "updated": socket })
self._evaluate(bpy.context)
def evaluate(self, context):
if len(self.inputs) > 2:
returns = []
for inp in self.inputs[1:-1]:
returns.append(inp.python_value)
returns = ", ".join(returns)
if len(self.inputs) == 3:
self.code = f"return {returns}"
else:
self.code = f"return [{returns}]"
else:
self.code = f"return"
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
@@ -0,0 +1,214 @@
import bpy
from ...utils import get_python_name
from ..base_node import SN_ScriptingBaseNode
class SN_RunFunctionNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunFunctionNode"
bl_label = "Function Run (Execute)"
bl_width_default = 240
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.ref_ntree = self.node_tree
def update_enum_socket(self, from_socket, to_socket):
to_socket.subtype = "CUSTOM_ITEMS"
to_socket.custom_items_editable = False
to_socket.custom_items.clear()
for item in from_socket.custom_items:
new = to_socket.custom_items.add()
new.name = item.name
def update_vector_socket(self, from_socket, to_socket):
to_socket.size = from_socket.size
def on_ref_update(self, node, data=None):
if node.bl_idname == "SN_FunctionNode" and data:
# 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)
# 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], data["changed"].bl_idname)
# update enum items
if data["changed"].bl_label == "Enum" or data["changed"].bl_label == "Enum Set":
self.update_enum_socket(data["changed"], self.inputs[data["changed"].index])
elif "Vector" in data["changed"].bl_label:
self.update_vector_socket(data["changed"], self.inputs[data["changed"].index])
# input has updated
elif "updated" in data:
self.inputs[data["updated"].index].name = data["updated"].name
self._evaluate(bpy.context)
elif node.bl_idname == "SN_FunctionReturnNode" and data:
# output has been added
if "added" in data:
socket_index = list(data["added"].node.inputs).index(data["added"])
self.add_output_from_socket(data["added"])
self.outputs.move(len(self.outputs)-1, socket_index)
# output has been removed
elif "removed" in data:
self.outputs.remove(self.outputs[data["removed"]])
# output has changed
elif "changed" in data:
self.convert_socket(self.outputs[data["changed"].index], data["changed"].bl_idname)
# output has updated
elif "updated" in data:
self.outputs[data["updated"].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_FunctionNode in parent_tree.nodes:
for out in parent_tree.nodes[self.ref_SN_FunctionNode].outputs[1:-1]:
inp = self.add_input_from_socket(out)
# update enum items
if out.bl_label == "Enum" or out.bl_label == "Enum Set":
self.update_enum_socket(out, inp)
elif "Vector" in out.bl_label:
self.update_vector_socket(out, inp)
# 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)
def update_function_return_reference(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
# remember connections
links = []
for out in self.outputs[1:]:
links.append([])
if out.is_linked:
links[-1] = out.to_sockets()
# remove current data outputs
for i in range(len(self.outputs)-1, 0, -1):
self.outputs.remove(self.outputs[i])
# add new data outputs
if self.ref_SN_FunctionReturnNode in parent_tree.nodes:
for out in parent_tree.nodes[self.ref_SN_FunctionReturnNode].inputs[1:-1]:
self.add_output_from_socket(out)
# restore connections
if len(links) == len(self.outputs)-1:
for i, to_sockets in enumerate(links):
for to_socket in to_sockets:
self.node_tree.links.new(self.outputs[i+1], to_socket)
self._evaluate(context)
ref_SN_FunctionNode: bpy.props.StringProperty(name="Function",
description="The function to run",
update=update_function_reference)
ref_SN_FunctionReturnNode: bpy.props.StringProperty(name="Return",
description="The return node to get values from",
update=update_function_return_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 update_require_execute(self, context):
self.inputs[0].set_hide(not self.require_execute)
self.outputs[0].set_hide(not self.require_execute)
self._evaluate(context)
require_execute: bpy.props.BoolProperty(name="Require Execute", default=True,
description="Removes the execute inputs and only gives you the return value",
update=update_require_execute)
def evaluate(self, context):
parent_tree = self.ref_ntree if self.ref_ntree else self.node_tree
if self.ref_SN_FunctionNode 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)
if self.require_execute:
# get return variable names
return_values = []
for i, out in enumerate(self.outputs[1:]):
return_values.append(get_python_name(f"{out.name}_{i}_{self.static_uid}", f"parameter_{i}_{self.static_uid}"))
return_names = ", ".join(return_values)
# set values with execute
if return_names:
self.code = f"{return_names} = {parent_tree.nodes[self.ref_SN_FunctionNode].func_name}({inp_values})"
else:
self.code = f"{parent_tree.nodes[self.ref_SN_FunctionNode].func_name}({inp_values})"
for i, out in enumerate(self.outputs[1:]):
out.python_value = return_values[i]
else:
# set values without execute
if len(self.outputs) > 2:
for i, out in enumerate(self.outputs[1:]):
out.python_value = f"{parent_tree.nodes[self.ref_SN_FunctionNode].func_name}({inp_values})[{i}]"
elif len(self.outputs) == 2:
self.outputs[-1].python_value = f"{parent_tree.nodes[self.ref_SN_FunctionNode].func_name}({inp_values})"
else:
for out in self.outputs[1:]:
out.reset_value()
if self.require_execute:
self.code += f"\n{self.outputs[0].python_value}"
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_FunctionNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_FunctionNode"), "refs", text="")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_FunctionNode 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_FunctionNode
row = layout.row()
row.enabled = self.ref_ntree != None
row.prop_search(self, "ref_SN_FunctionReturnNode", bpy.data.node_groups[parent_tree.name].node_collection("SN_FunctionReturnNode"), "refs", text="Return")
subrow = row.row()
subrow.enabled = self.ref_ntree != None and self.ref_SN_FunctionReturnNode 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_FunctionReturnNode
layout.prop(self, "require_execute")
if self.ref_ntree and self.ref_SN_FunctionNode and self.ref_SN_FunctionReturnNode:
return_node = self.ref_ntree.nodes[self.ref_SN_FunctionReturnNode]
if not self.ref_ntree.nodes[self.ref_SN_FunctionNode] in return_node.root_nodes:
row = layout.row()
row.alert = True
row.label(text="Return node not connected to function node!", icon="ERROR")
@@ -0,0 +1,93 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ...utils import get_python_name, unique_collection_name
class SN_FunctionNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_FunctionNode"
bl_label = "Function (Execute)"
is_trigger = True
bl_width_default = 200
def on_create(self, context):
self.add_execute_output()
out = self.add_dynamic_data_output("Input")
out.is_variable = True
out.changeable = True
def on_dynamic_socket_add(self, socket):
socket["name"] = get_python_name(socket.name, "Input", lower=False)
socket["name"] = unique_collection_name(socket.name, "Input", [out.name for out in self.outputs[1:-1]], "_", includes_name=True)
socket.python_value = socket.name
if hasattr(socket, "size_editable"):
socket.size_editable = True
self.trigger_ref_update({ "added": socket })
self._evaluate(bpy.context)
def on_dynamic_socket_remove(self, index, is_output):
self.trigger_ref_update({ "removed": index })
self._evaluate(bpy.context)
def on_socket_type_change(self, socket):
if socket.bl_label == "Enum" or socket.bl_label == "Enum Set":
socket.subtype = "CUSTOM_ITEMS"
elif hasattr(socket, "size_editable"):
socket.size_editable = True
self.trigger_ref_update({ "changed": socket })
self._evaluate(bpy.context)
def on_socket_name_change(self, socket):
socket["name"] = get_python_name(socket.name, "Input", lower=False)
socket["name"] = unique_collection_name(socket.name, "Input", [out.name for out in self.outputs[1:-1]], "_", includes_name=True)
socket.python_value = socket.name
self.trigger_ref_update({ "updated": socket })
self._evaluate(bpy.context)
def update_fixed_name(self, context):
self.trigger_ref_update()
self._evaluate(context)
fixed_func_name: bpy.props.StringProperty(default="",
name="Fixed Name",
description="A fixed python name that will be used for this function",
update=update_fixed_name)
@property
def func_name(self):
if self.fixed_func_name:
return self.fixed_func_name
return f"sna_{get_python_name(self.name, 'func')}_{self.static_uid}"
def evaluate(self, context):
out_values = []
for i, out in enumerate(self.outputs[1:-1]):
out_values.append(get_python_name(out.name, f"parameter_{i}", lower=False))
out_names = ", ".join(out_values)
self.code = f"""
def {self.func_name}({out_names}):
{self.indent(self.outputs[0].python_value, 6) if self.outputs[0].python_value else 'pass'}
"""
for i, out in enumerate(self.outputs[1:-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_RunFunctionNode"
row.operator("sn.copy_python_name", text="", icon="COPYDOWN").name = self.func_name
def draw_node_panel(self, context, layout):
layout.prop(self, "fixed_func_name")
@@ -0,0 +1,41 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ModalEventNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ModalEventNode"
bl_label = "Modal Event"
node_color = "DEFAULT"
def on_create(self, context):
self.add_string_output("Type")
self.add_string_output("Type Previous")
self.add_string_output("Value")
self.add_string_output("Value Previous")
self.add_boolean_output("Alt")
self.add_boolean_output("Shift")
self.add_boolean_output("Ctrl")
self.add_boolean_output("Os Key")
self.add_integer_vector_output("Mouse Region").size = 2
self.add_integer_vector_output("Mouse Window").size = 2
self.add_integer_vector_output("Mouse Offset").size = 2
self.add_float_output("Pressure")
self.add_float_output("Tilt")
# When adding options here, also add them to the modal call where it's saved
def evaluate(self, context):
self.outputs["Type"].python_value = f"event.type"
if "Type Previous" in self.outputs: self.outputs["Type Previous"].python_value = f"event.type_prev"
self.outputs["Value"].python_value = f"event.value"
if "Value Previous" in self.outputs: self.outputs["Value Previous"].python_value = f"event.value_prev"
self.outputs["Alt"].python_value = f"event.alt"
self.outputs["Shift"].python_value = f"event.shift"
self.outputs["Ctrl"].python_value = f"event.ctrl"
self.outputs["Os Key"].python_value = f"event.oskey"
self.outputs["Mouse Region"].python_value = f"(event.mouse_region_x, event.mouse_region_y)"
self.outputs["Mouse Window"].python_value = f"(event.mouse_x, event.mouse_y)"
self.outputs["Mouse Offset"].python_value = f"((event.mouse_x - self.start_pos[0]), (event.mouse_y - self.start_pos[1]))"
self.outputs["Pressure"].python_value = f"event.pressure"
self.outputs["Tilt"].python_value = f"event.tilt"
@@ -0,0 +1,259 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ...templates.PropertyNode import PropertyNode
from ....utils import get_python_name, normalize_code, unique_collection_name
class SN_ModalOperatorNode(SN_ScriptingBaseNode, bpy.types.Node, PropertyNode):
bl_idname = "SN_ModalOperatorNode"
bl_label = "Modal Operator"
is_trigger = True
bl_width_default = 200
node_color = "PROGRAM"
collection_key_overwrite = "SN_OperatorNode"
def on_node_property_change(self, property):
self.trigger_ref_update({ "property_change": property })
def on_node_property_add(self, property):
property.allow_pointers = False
self.trigger_ref_update({ "property_add": property })
def on_node_property_remove(self, index):
self.trigger_ref_update({ "property_remove": index })
def on_node_property_move(self, from_index, to_index):
self.trigger_ref_update({ "property_move": (from_index, to_index) })
def on_node_name_change(self):
new_name = self.name.replace("\"", "'")
if not self.name == new_name:
self.name = new_name
names = []
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for ref in ntree.node_collection("SN_OperatorNode").refs:
names.append(ref.node.name)
new_name = unique_collection_name(self.name, "My Operator", names, " ", includes_name=True)
if not self.name == new_name:
self.name = new_name
self.trigger_ref_update()
self._evaluate(bpy.context)
def on_create(self, context):
self.add_boolean_input("Disable")
self.add_string_input("Disabled Warning")
self.add_execute_output("Before Modal")
self.add_execute_output("Modal")
self.add_execute_output("Draw").set_hide(True)
self.add_execute_output("After Modal")
def update_description(self, context):
self["operator_description"] = self.operator_description.replace("\"", "'")
self._evaluate(context)
operator_description: bpy.props.StringProperty(name="Description",
description="Description of the operator",
update=update_description)
def cursor_items(self, context):
items = []
types = ['CROSSHAIR', 'DEFAULT', 'NONE', 'WAIT', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER', 'PICK_AREA', 'STOP', 'COPY', 'CROSS', 'MUTE', 'ZOOM_IN', 'ZOOM_OUT']
for cursor in types:
items.append((cursor, cursor.replace("_", " ").title(), cursor))
return items
cursor: bpy.props.EnumProperty(items=cursor_items,
name="Cursor",
description="The cursor to use while the modal is running",
update=SN_ScriptingBaseNode._evaluate)
keep_interactive: bpy.props.BoolProperty(default=True,
name="Keep Interactive",
description="If this is enabled, the ui is still interactive when the modal is running",
update=SN_ScriptingBaseNode._evaluate)
enable_escape: bpy.props.BoolProperty(default=True,
name="Default Escape",
description="Finish the modal automatically when pressing escape or rightclicking. If this is turned off you need to add a way to finish a modal yourself",
update=SN_ScriptingBaseNode._evaluate)
def update_draw_text(self, context):
if "Draw" in self.outputs:
self.outputs["Draw"].set_hide(not self.draw_text)
else:
self.outputs["Draw Text"].set_hide(not self.draw_text)
self._evaluate(context)
draw_text: bpy.props.BoolProperty(default=False,
name="Draw",
description="Lets you draw to the interface while the modal is running",
update=update_draw_text)
def draw_space_items(self, context):
items = []
names = ["SpaceNodeEditor", "SpaceView3D", "SpaceClipEditor", "SpaceConsole", "SpaceDopeSheetEditor", "SpaceFileBrowser",
"SpaceGraphEditor", "SpaceImageEditor", "SpaceInfo", "SpaceNLA", "SpaceOutliner", "SpacePreferences",
"SpaceProperties", "SpaceSequenceEditor", "SpaceSpreadsheet", "SpaceTextEditor"]
for name in names:
items.append((name, name, name))
return items
draw_space: bpy.props.EnumProperty(name="Draw Space",
description="The space this operator can run in and elements are drawn in",
update=SN_ScriptingBaseNode._evaluate,
items=draw_space_items)
hide_properties: bpy.props.BoolProperty(default=False, name="Hide Properties",
description="Hide the properties section of this operator")
@property
def operator_python_name(self):
return get_python_name(self.name, replacement="my_generic_operator") + f"_{self.static_uid.lower()}"
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
python_name = get_python_name(self.name, replacement="my_generic_operator")
row.operator("sn.copy_python_name", text="", icon="COPYDOWN").name = "sna." + python_name
layout.label(text="Description: ")
layout.prop(self, "operator_description", text="")
layout.prop(self, "cursor")
layout.prop(self, "keep_interactive")
layout.prop(self, "draw_text")
if self.draw_text:
layout.prop(self, "draw_space", text="Space")
layout.prop(self, "enable_escape")
if self.enable_escape:
layout.label(text="ESC or Rightclick to cancel the modal", icon="INFO")
if not self.hide_properties:
self.draw_list(layout)
def draw_node_panel(self, context, layout):
layout.prop(self, "hide_properties")
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")
if self.draw_text:
self.code_imperative = f"""
class dotdict(dict):
__getattr__ = dict.get
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
"""
self.code_import = "import blf"
escape = """
if event.type in ['RIGHTMOUSE', 'ESC']:
self.execute(context)
return {'CANCELLED'}
"""
modal_code = self.outputs['Modal'].python_value
if "Draw" in self.outputs:
draw_code = self.outputs["Draw"].python_value
else:
draw_code = self.outputs["Draw Text"].python_value
self.code = f"""
{self.indent(props_imperative_list, 3)}
_{self.static_uid}_running = False
class SNA_OT_{self.operator_python_name.title()}(bpy.types.Operator):
bl_idname = "sna.{self.operator_python_name}"
bl_label = "{self.name}"
bl_description = "{self.operator_description}"
bl_options = {"{" + '"REGISTER", "UNDO"' + "}"}
{self.indent(props_code_list, 4)}
cursor = "{self.cursor}"
_handle = None
_event = {{}}
@classmethod
def poll(cls, context):
if bpy.app.version >= (3, 0, 0) and {'Disabled Warning' in self.inputs}:
cls.poll_message_set({self.inputs['Disabled Warning'].python_value if 'Disabled Warning' in self.inputs else ""})
if not {self.draw_text} or context.area.spaces[0].bl_rna.identifier == '{self.draw_space}':
return not {self.inputs[0].python_value}
return False
def save_event(self, event):
event_options = ["type", "value", "alt", "shift", "ctrl", "oskey", "mouse_region_x", "mouse_region_y", "mouse_x", "mouse_y", "pressure", "tilt"]
if bpy.app.version >= (3, 2, 1):
event_options += ["type_prev", "value_prev"]
for option in event_options: self._event[option] = getattr(event, option)
def draw_callback_px(self, context):
event = self._event
if event.keys():
event = dotdict(event)
try:
{self.indent(draw_code, 7) if draw_code.strip() else "pass"}
except Exception as error:
print(error)
def execute(self, context):
global _{self.static_uid}_running
_{self.static_uid}_running = False
context.window.cursor_set("DEFAULT")
{f"bpy.types.{self.draw_space}.draw_handler_remove(self._handle, 'WINDOW')" if self.draw_text else ""}
{self.indent(self.outputs['After Modal'].python_value, 5)}
for area in context.screen.areas:
area.tag_redraw()
return {{"FINISHED"}}
def modal(self, context, event):
global _{self.static_uid}_running
if not context.area or not _{self.static_uid}_running:
self.execute(context)
return {{'CANCELLED'}}
self.save_event(event)
{"context.area.tag_redraw()" if self.draw_text else ""}
context.window.cursor_set('{self.cursor}')
try:
{self.indent(modal_code, 6) if modal_code.strip() else "pass"}
except Exception as error:
print(error)
{self.indent(normalize_code(escape), 5) if self.enable_escape else ""}
return {"{'PASS_THROUGH'}" if self.keep_interactive else "{'RUNNING_MODAL'}"}
def invoke(self, context, event):
global _{self.static_uid}_running
if _{self.static_uid}_running:
_{self.static_uid}_running = False
return {{'FINISHED'}}
else:
self.save_event(event)
self.start_pos = (event.mouse_x, event.mouse_y)
{self.indent(self.outputs['Before Modal'].python_value, 6)}
{"args = (context,)" if self.draw_text else ""}
{f"self._handle = bpy.types.{self.draw_space}.draw_handler_add(self.draw_callback_px, args, 'WINDOW', 'POST_PIXEL')" if self.draw_text else ""}
context.window_manager.modal_handler_add(self)
_{self.static_uid}_running = True
return {{'RUNNING_MODAL'}}
"""
self.code_register = f"""
{self.indent(props_register_list, 4)}
bpy.utils.register_class(SNA_OT_{self.operator_python_name.title()})
"""
self.code_unregister = f"""
{self.indent(props_unregister_list, 4)}
bpy.utils.unregister_class(SNA_OT_{self.operator_python_name.title()})
"""
@@ -0,0 +1,22 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ModalShortcutPressedNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ModalShortcutPressedNode"
bl_label = "Modal Shortcut Pressed"
node_color = "DEFAULT"
def on_create(self, context):
items = bpy.types.Event.bl_rna.properties["type"].enum_items
self.add_enum_input("Type")["items"] = str([item.identifier for item in items])
self.add_boolean_input("Alt")
self.add_boolean_input("Shift")
self.add_boolean_input("Ctrl")
self.add_boolean_input("Os Key")
self.add_boolean_output("Shortcut Pressed")
def evaluate(self, context):
self.outputs[0].python_value = f"(event.type == {self.inputs['Type'].python_value} and event.value == 'PRESS' and event.alt == {self.inputs['Alt'].python_value} and event.shift == {self.inputs['Shift'].python_value} and event.ctrl == {self.inputs['Ctrl'].python_value})"
@@ -0,0 +1,42 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ModalViewportMovedNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ModalViewportMovedNode"
bl_label = "Modal Viewport Moved"
node_color = "DEFAULT"
def on_create(self, context):
self.add_boolean_output("Viewport Moved")
def evaluate(self, context):
self.code_imperative = """
def is_event_view_move(event):
view2d_ops = ["view2d.pan", "view2d.scroll_right", "view2d.scroll_left", "view2d.scroll_down",
"view2d.scroll_up", "view2d.ndof", "view2d.ndof", "view2d.ndof", "view2d.zoom_out",
"view2d.zoom_in", "view2d.zoom", "view2d.zoom_border"]
view3d_ops = ["view3d.move", "view3d.zoom", "view3d.dolly", "view3d.view_selected", "view3d.smoothview",
"view3d.view_all", "view3d.view_axis", "view3d.view_persportho", "view3d.view_orbit",
"view3d.view_center_pick", "view3d.ndof_orbit_zoom", "view3d.ndof_orbit", "view3d.ndof_pan",
"view3d.ndof_all", "view3d.view_roll", "view3d.zoom_border"]
items_2d = bpy.context.window_manager.keyconfigs[bpy.context.preferences.keymap.active_keyconfig].keymaps['View2D'].keymap_items
for item in items_2d:
if item.idname in view2d_ops:
if event.type == item.type and event.shift == bool(item.shift) and event.alt == bool(item.alt) and event.ctrl == bool(item.ctrl) and event.oskey == bool(item.oskey):
return True
items_3d = bpy.context.window_manager.keyconfigs[bpy.context.preferences.keymap.active_keyconfig].keymaps['3D View'].keymap_items
for item in items_3d:
if item.idname in view3d_ops:
if event.type == item.type and event.shift == bool(item.shift) and event.alt == bool(item.alt) and event.ctrl == bool(item.ctrl) and event.oskey == bool(item.oskey):
return True
return False
"""
self.outputs[0].python_value = f"is_event_view_move(event)"
@@ -0,0 +1,46 @@
import bpy
from ....utils import normalize_code
from ...base_node import SN_ScriptingBaseNode
class SN_ReturnModalNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ReturnModalNode"
bl_label = "Return Modal Operator"
node_color = "PROGRAM"
return_type: bpy.props.EnumProperty(name="Return Type",
description="The way this modal should be finished",
items=[("FINISHED", "Finish", "End the modal"),
("CANCELLED", "Cancel", "Cancel the modal"),
("PASS_THROUGH", "Interactive", "Keep the modal running and let the events be used by other operators"),
("RUNNING_MODAL", "Not Interactive", "Keep the modal running but block other uses of the event")],
default="FINISHED",
update=SN_ScriptingBaseNode._evaluate)
enable_escape: bpy.props.BoolProperty(default=True,
name="Default Escape",
description="Finish the modal automatically when pressing escape or rightclicking. If this is turned off you need to add a way to finish a modal yourself",
update=SN_ScriptingBaseNode._evaluate)
def on_create(self, context):
self.add_execute_input()
def draw_node(self, context, layout):
col = layout.column()
col.prop(self, "return_type", expand=True)
layout.prop(self, "enable_escape")
def evaluate(self, context):
escape = """
if event.type in ['RIGHTMOUSE', 'ESC']:
self.execute(context)
return {'CANCELLED'}
"""
self.code = f"""
{self.indent(normalize_code(escape), 2) if self.enable_escape else ""}
{"self.execute(context)" if self.return_type == "FINISHED" else ""}
return {{"{self.return_type}"}}
"""
@@ -0,0 +1,35 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_SetModalCursorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SetModalCursorNode"
bl_label = "Set Modal Cursor"
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
def cursor_items(self, context):
items = []
types = ['CROSSHAIR', 'DEFAULT', 'NONE', 'WAIT', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER', 'PICK_AREA', 'STOP', 'COPY', 'CROSS', 'MUTE', 'ZOOM_IN', 'ZOOM_OUT']
for cursor in types:
items.append((cursor, cursor.replace("_", " ").title(), cursor))
return items
cursor: bpy.props.EnumProperty(items=cursor_items,
name="Cursor",
description="The cursor to use while the modal is running",
update=SN_ScriptingBaseNode._evaluate)
def draw_node(self, context, layout):
layout.prop(self, "cursor")
def evaluate(self, context):
self.code = f"""
context.window.cursor_set('{self.cursor}')
{self.indent(self.outputs[0].python_value, 2)}
"""
@@ -0,0 +1,46 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_TextSizeNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_TextSizeNode"
bl_label = "Text Size"
bl_width_default = 200
def on_create(self, context):
self.add_string_input("Text").default_value = "My Text"
self.add_string_input("Font").subtype = "FILE_PATH"
self.add_float_input("Size").default_value = 20
self.add_integer_input("DPI").default_value = 72
self.add_float_output("Width")
self.add_float_output("Height")
def evaluate(self, context):
self.code_imperative = f"""
def get_text_dimensions(text, font, size, dpi):
font_id = 0
if font and os.path.exists(font):
font_id = blf.load(font)
if font_id == -1:
print("Couldn't load font!")
else:
if bpy.app.version >= (3, 4, 0):
blf.size(font_id, size)
else:
blf.size(font_id, size, dpi)
return blf.dimensions(font_id, text)
"""
self.code_import = """
import blf
import os
"""
self.outputs[
"Width"
].python_value = f"get_text_dimensions({self.inputs['Text'].python_value}, {self.inputs['Font'].python_value}, {self.inputs['Size'].python_value}, {self.inputs['DPI'].python_value})[0]"
self.outputs[
"Height"
].python_value = f"get_text_dimensions({self.inputs['Text'].python_value}, {self.inputs['Font'].python_value}, {self.inputs['Size'].python_value}, {self.inputs['DPI'].python_value})[1]"
@@ -0,0 +1,19 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_BreakNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_BreakNode"
bl_label = "Break"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
def evaluate(self, context):
self.code = f"""
break
"""
@@ -0,0 +1,43 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....utils import normalize_code
class SN_EnumMapNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_EnumMapNode"
bl_label = "Enum Map (Execute)"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_string_input("Enum Value")
self.add_execute_output("Continue")
self.add_execute_output("Other Option")
out = self.add_dynamic_execute_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['Continue'].python_value, 5)}
"""
@@ -0,0 +1,27 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_IfElseExecuteNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_IfElseExecuteNode"
bl_label = "If/Else (Execute)"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_boolean_input("Condition")
self.add_execute_output("True")
self.add_execute_output("False")
self.add_execute_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)}
"""
@@ -0,0 +1,57 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ForExecuteNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ForExecuteNode"
bl_label = "Loop For (Execute)"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_collection_property_input("Collection")
self.add_execute_output("Repeat")
self.add_execute_output("Continue")
self.add_property_output("Item").changeable = True
self.add_integer_output("Index")
def update_type(self, context):
inp = self.convert_socket(self.inputs[1], self.socket_names[self.for_type])
inp.name = self.for_type
self.convert_socket(self.outputs["Item"], self.socket_names["Data"] if self.for_type == "List" else self.socket_names["Property"])
self._evaluate(context)
for_type: bpy.props.EnumProperty(name="Type",
description="Collection Type",
items=[("List", "List", "List"),
("Collection", "Collection", "Collection")],
default="Collection",
update=update_type)
reverse: bpy.props.BoolProperty(name="Reverse",
description="Reverse the order the loop runs through the items",
default=False,
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.inputs[1].is_linked:
self.outputs["Index"].python_value = f"i_{self.static_uid}"
self.outputs["Item"].python_value = f"{self.inputs[1].python_value}[i_{self.static_uid}]"
self.code = f"""
for i_{self.static_uid} in range({f"len({self.inputs[1].python_value})" if not self.reverse else f"len({self.inputs[1].python_value})-1,-1,-1"}):
{self.indent(self.outputs['Repeat'].python_value, 7) if self.outputs['Repeat'].python_value.strip() else 'pass'}
{self.indent(self.outputs['Continue'].python_value, 6)}
"""
else:
self.code = f"""print("No Collection connected to {self.name}!")"""
self.outputs["Index"].reset_value()
self.outputs["Item"].reset_value()
def draw_node(self, context, layout):
layout.prop(self, "for_type")
layout.prop(self, "reverse")
@@ -0,0 +1,26 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RepeatExecuteNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RepeatExecuteNode"
bl_label = "Loop Repeat (Execute)"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_integer_input("Repetitions").default_value = 2
self.add_execute_output("Repeat")
self.add_execute_output("Continue")
self.add_integer_output("Step")
def evaluate(self, context):
self.outputs["Step"].python_value = f"i_{self.static_uid}"
self.code = f"""
for i_{self.static_uid} in range({self.inputs['Repetitions'].python_value}):
{self.indent(self.outputs['Repeat'].python_value, 6) if self.outputs['Repeat'].python_value.strip() else 'pass'}
{self.indent(self.outputs['Continue'].python_value, 5)}
"""
@@ -0,0 +1,84 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_OpenMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_OpenMenuNode"
bl_label = "Open Menu"
node_color = "PROGRAM"
bl_width_default = 240
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
if node.bl_idname in ["SN_PanelNode", "SN_MenuNode", "SN_PieMenuNode"]:
self._evaluate(bpy.context)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
default="CUSTOM",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
ref_SN_MenuNode: bpy.props.StringProperty(name="Custom Parent",
description="The menu that should be shown",
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Menu Node Tree",
description="The node tree to select the menu from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
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):
if self.parent_type == "CUSTOM":
if self.ref_ntree and self.ref_SN_MenuNode in self.ref_ntree.nodes:
self.code = f"""
bpy.ops.wm.call_menu(name="{self.ref_ntree.nodes[self.ref_SN_MenuNode].idname}")
{self.indent(self.outputs[0].python_value, 5)}
"""
else:
self.code = f"""
bpy.ops.wm.call_menu(name="{self.menu_parent}")
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
if self.parent_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_MenuNode", parent_tree.node_collection("SN_MenuNode"), "refs", text="")
row.prop(self, "parent_type", text="", icon_only=True)
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
else:
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
row.prop(self, "parent_type", text="", icon_only=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,85 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_OpenPanelNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_OpenPanelNode"
bl_label = "Open Panel"
node_color = "PROGRAM"
bl_width_default = 240
def on_create(self, context):
self.add_execute_input()
self.add_boolean_input("Keep Open After Click")
self.add_execute_output()
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
if node.bl_idname in ["SN_PanelNode", "SN_MenuNode", "SN_PieMenuNode"]:
self._evaluate(bpy.context)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
default="CUSTOM",
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 that should be shown",
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)
panel_parent: bpy.props.StringProperty(name="Panel",
default="RENDER_PT_context",
description="The panel that should be displayed",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.parent_type == "CUSTOM":
if self.ref_ntree and self.ref_SN_PanelNode in self.ref_ntree.nodes:
self.code = f"""
bpy.ops.wm.call_panel(name="{self.ref_ntree.nodes[self.ref_SN_PanelNode].last_idname}", keep_open={self.inputs[1].python_value})
{self.indent(self.outputs[0].python_value, 5)}
"""
else:
self.code = f"""
bpy.ops.wm.call_panel(name="{self.panel_parent}", keep_open={self.inputs[1].python_value})
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
if self.parent_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_PanelNode", parent_tree.node_collection("SN_PanelNode"), "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
else:
name = f"{self.panel_parent.replace('_PT_', ' ').replace('_', ' ').title()}"
op = row.operator("sn.activate_subpanel_picker", icon="EYEDROPPER", text=name)
op.node_tree = self.node_tree.name
op.node = self.name
row.prop(self, "parent_type", text="", icon_only=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "panel_parent")
@@ -0,0 +1,84 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_OpenPieMenuNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_OpenPieMenuNode"
bl_label = "Open Pie Menu"
node_color = "PROGRAM"
bl_width_default = 240
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.ref_ntree = self.node_tree
def on_ref_update(self, node, data=None):
if node.bl_idname in ["SN_PanelNode", "SN_MenuNode", "SN_PieMenuNode"]:
self._evaluate(bpy.context)
parent_type: bpy.props.EnumProperty(name="Parent Type",
description="Use a custom panel as a parent",
default="CUSTOM",
items=[("BLENDER", "Blender", "Blender", "BLENDER", 0),
("CUSTOM", "Custom", "Custom", "FILE_SCRIPT", 1)],
update=SN_ScriptingBaseNode._evaluate)
ref_SN_PieMenuNode: bpy.props.StringProperty(name="Custom Parent",
description="The menu that should be shown",
update=SN_ScriptingBaseNode._evaluate)
ref_ntree: bpy.props.PointerProperty(type=bpy.types.NodeTree,
name="Menu Node Tree",
description="The node tree to select the menu from",
poll=SN_ScriptingBaseNode.ntree_poll,
update=SN_ScriptingBaseNode._evaluate)
menu_parent: bpy.props.StringProperty(name="Pie Menu",
default="VIEW3D_MT_shading_pie",
description="The menu that should be displayed",
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
if self.parent_type == "CUSTOM":
if self.ref_ntree and self.ref_SN_PieMenuNode in self.ref_ntree.nodes:
self.code = f"""
bpy.ops.wm.call_menu_pie(name="{self.ref_ntree.nodes[self.ref_SN_PieMenuNode].idname}")
{self.indent(self.outputs[0].python_value, 5)}
"""
else:
self.code = f"""
bpy.ops.wm.call_menu_pie(name="{self.menu_parent}")
{self.indent(self.outputs[0].python_value, 4)}
"""
def draw_node(self, context, layout):
row = layout.row(align=True)
if self.parent_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_PieMenuNode", parent_tree.node_collection("SN_PieMenuNode"), "refs", text="")
row.prop(self, "parent_type", text="", icon_only=True)
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
else:
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
row.prop(self, "parent_type", text="", icon_only=True)
def draw_node_panel(self, context, layout):
layout.prop(self, "menu_parent")
@@ -0,0 +1,120 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ShortcutOperatorOption(bpy.types.PropertyGroup):
name: bpy.props.StringProperty()
operator: bpy.props.StringProperty()
settings: bpy.props.StringProperty()
class SN_OT_ShortcutToNode(bpy.types.Operator):
bl_idname = "sn.shortcut_to_node"
bl_label = "Shortcut To Node"
bl_description = "Add a node with the operator from this shortcut pasted in"
bl_options = {"REGISTER", "INTERNAL"}
operator: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
settings: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
button: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
if self.button:
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_ButtonNodeNew", use_transform=True)
else:
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_RunOperatorNode", use_transform=True)
node = context.space_data.node_tree.nodes.active
node.pasted_operator = self.operator
if self.settings:
settings = self.settings.split("|")
for setting in settings:
name = setting.split("&&")[0].replace("_", " ").title()
value = setting.split("&&")[1]
if name in node.inputs:
node.inputs[name].disabled = False
node.inputs[name].default_value = value
return {"FINISHED"}
class SN_FindShortcutOperator(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_FindShortcutOperator"
bl_label = "Find Operator From Shortcut"
bl_width_default = 240
options: bpy.props.CollectionProperty(type=SN_ShortcutOperatorOption)
selected: bpy.props.StringProperty()
def update_shortcut(self, context):
options = self.find_operators_for_key()
self.options.clear()
for option in options:
item = self.options.add()
item.name = option[0]
item.operator = option[1]
item.settings = option[2]
key: bpy.props.StringProperty(name="Key", default="A", update=update_shortcut)
recording: bpy.props.BoolProperty(name="Recording", default=False)
shift: bpy.props.BoolProperty(name="Shift", default=True, update=update_shortcut)
ctrl: bpy.props.BoolProperty(name="Ctrl", default=False, update=update_shortcut)
alt: bpy.props.BoolProperty(name="Alt", default=False, update=update_shortcut)
oskey: bpy.props.BoolProperty(name="OS Key", default=False, update=update_shortcut)
def on_create(self, context):
self.update_shortcut(context)
def draw_node(self, context, layout):
layout.operator("sn.record_key", text=self.key, depress=self.recording).node = self.name
row = layout.row(align=True)
row.prop(self, "shift", toggle=True)
row.prop(self, "ctrl", toggle=True)
row.prop(self, "alt", toggle=True)
row.prop(self, "oskey", toggle=True)
layout.prop_search(self, "selected", self, "options", text="")
row = layout.row(align=True)
row.enabled = self.selected in self.options
row.scale_y = 1.2
op = row.operator("sn.shortcut_to_node", text="Run Operator", icon="POSE_HLT")
op.button = False
op.operator = "" if not self.selected in self.options else self.options[self.selected].operator
op.settings = "" if not self.selected in self.options else self.options[self.selected].settings
op = row.operator("sn.shortcut_to_node", text="Button", icon="MOUSE_LMB")
op.button = True
op.operator = "" if not self.selected in self.options else self.options[self.selected].operator
op.settings = "" if not self.selected in self.options else self.options[self.selected].settings
def find_operators_for_key(self):
options = []
for keymap in bpy.context.window_manager.keyconfigs.user.keymaps:
for item in keymap.keymap_items:
if item.type == self.key and item.shift == self.shift and item.ctrl == self.ctrl \
and item.alt == self.alt and item.oskey == self.oskey:
if item.idname:
props = ""
prop_str = ""
settings = ""
if item.properties:
for prop in item.properties.keys():
if type(item.properties[prop]) == str:
props += f"{prop}='{item.properties[prop]}', "
prop_str += f"'{item.properties[prop]}', "
else:
props += f"{prop}={item.properties[prop]}, "
prop_str += f"{item.properties[prop]}, "
settings += f"{prop}&&{item.properties[prop]}|"
name = item.name if item.name else item.idname.replace("_", " ").title()
area = keymap.space_type.replace("_", " ").title() if keymap.space_type != "EMPTY" else "Any"
if prop_str:
prop_str = f" [{prop_str[:-2]}]"
settings = settings[:-1]
op = f"bpy.ops.{item.idname}('INVOKE_DEFAULT', {props})"
options.append((f"{name} ({area}){prop_str}", op, settings))
return options
@@ -0,0 +1,122 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
from ....settings.data_properties import get_item_type
class SN_OverrideItem(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="Name", default="")
identifier: bpy.props.StringProperty(name="Identifier", default="")
type: bpy.props.StringProperty(name="Type", default="")
class SN_OT_AddOverrideInput(bpy.types.Operator):
bl_idname = "sn.add_override_input"
bl_label = "Add Override Input"
bl_description = "Adds an override input"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(name="Node", default="")
name: bpy.props.StringProperty(name="Name", default="")
identifier: bpy.props.StringProperty(name="Identifier", default="")
type: bpy.props.StringProperty(name="Type", default="")
def execute(self, context):
node = context.space_data.node_tree.nodes[self.node]
if not self.name in node.inputs:
inp = node._add_input(node.socket_names[self.type], self.name)
inp.prev_dynamic = True
print("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - ")
print(f"Value of {self.name}:")
print(context.scene.sn.copied_context[0][self.identifier])
print("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - ")
self.report({"INFO"}, f"Printed the current value of {self.name} in the console")
return {"FINISHED"}
class SN_OT_AddOverride(bpy.types.Operator):
bl_idname = "sn.add_override"
bl_label = "Add Override"
bl_description = "Opens a popup to add overrides"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(name="Node", default="")
overrides: bpy.props.CollectionProperty(type=SN_OverrideItem)
override: bpy.props.StringProperty(name="Override", default="")
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.label(text="Add Override")
if context.scene.sn.copied_context == []:
row = layout.row()
row.enabled = False
row.label(text="Rightclick any option to copy the context in that space")
else:
ctxt = f"Copied Context: {context.scene.sn.copied_context[0]['area'].type.replace('_', ' ').title()} {context.scene.sn.copied_context[0]['region'].type.replace('_', ' ').title()}"
layout.label(text=ctxt)
layout.prop_search(self, "override", self, "overrides", text="")
row = layout.row()
item = None if not self.override in self.overrides.keys() else self.overrides[self.override]
row.enabled = item != None
op = row.operator("sn.add_override_input", text="Add", icon="ADD")
op.node = self.node
if item != None:
op.name = item.name
op.identifier = item.identifier
op.type = item.type
def invoke(self, context, event):
node = context.space_data.node_tree.nodes[self.node]
self.overrides.clear()
if context.scene.sn.copied_context != []:
for key in context.scene.sn.copied_context[0].keys():
item = self.overrides.add()
item.name = key.replace("_", " ").title()
item.identifier = key
item.type = get_item_type(context.scene.sn.copied_context[0][key])
if not item.type in node.socket_names:
item.type = "Data"
return context.window_manager.invoke_popup(self, width=300)
class SN_OverrideContextNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_OverrideContextNode"
bl_label = "Override Context"
node_color = "PROGRAM"
bl_width_default = 200
def on_create(self, context):
self.add_execute_input()
self.add_execute_output("With Override")
self.add_execute_output("Continue")
def evaluate(self, context):
overrides = ""
override_vars = []
for inp in self.inputs[1:]:
identifier = inp.name.lower().replace(' ', '_')
var_name = f"{identifier}_{self.static_uid}"
override_vars.append(f"{var_name} = {inp.python_value}")
overrides += f"{identifier}={var_name}, "
self.code = f"""
{self.indent(override_vars, 5)}
with bpy.context.temp_override({overrides}):
{self.indent(self.outputs[0].python_value, 6) if self.indent(self.outputs[0].python_value, 6).strip() else "pass"}
{self.indent(self.outputs[1].python_value, 5)}
"""
def draw_node(self, context, layout):
row = layout.row()
row.scale_y = 1.25
row.operator("sn.add_override", text="Add Override", icon="ADD").node = self.name
@@ -0,0 +1,23 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RefreshViewNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RefreshViewNode"
bl_label = "Refresh View"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
def evaluate(self, context):
self.code = f"""
if bpy.context and bpy.context.screen:
for a in bpy.context.screen.areas:
a.tag_redraw()
{self.indent(self.outputs[0].python_value, 5)}
"""
@@ -0,0 +1,32 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_ReportNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_ReportNode"
bl_label = "Report"
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_string_input()
self.add_execute_output()
type: bpy.props.EnumProperty(name="Type",
description="Type of the report message",
items=[("INFO", "Info", "Info"),
("WARNING", "Warning", "Warning"),
("ERROR", "Error", "Error")],
update=SN_ScriptingBaseNode._evaluate)
def evaluate(self, context):
self.code = f"""
self.report({{'{self.type}'}}, message={self.inputs[1].python_value})
{self.indent(self.outputs[0].python_value, 5)}
"""
def draw_node(self, context, layout):
layout.prop(self, "type")
layout.label(text="This only works when connected to operators", icon="INFO")
@@ -0,0 +1,30 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_SetEditSelectNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SetEditSelectNode"
bl_label = "Set Edit Select"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_boolean_input("Vertex").can_be_disabled = True
self.add_boolean_input("Edge").can_be_disabled = True
self.add_boolean_input("Face").can_be_disabled = True
def evaluate(self, context):
vertex_execute = f"bpy.ops.mesh.select_mode(action='ENABLE' if {self.inputs[1].python_value} else 'DISABLE', type='VERT', use_extend=True)" if not self.inputs[1].disabled else ""
edge_execute = f"bpy.ops.mesh.select_mode(action='ENABLE' if {self.inputs[2].python_value} else 'DISABLE', type='EDGE', use_extend=True)" if not self.inputs[2].disabled else ""
face_execute = f"bpy.ops.mesh.select_mode(action='ENABLE' if {self.inputs[3].python_value} else 'DISABLE', type='FACE', use_extend=True)" if not self.inputs[3].disabled else ""
self.code = f"""
{vertex_execute}
{edge_execute}
{face_execute}
{self.indent(self.outputs[0].python_value, 5)}
"""
@@ -0,0 +1,24 @@
import bpy
from ....utils import normalize_code
from ...base_node import SN_ScriptingBaseNode
class SN_SetHeaderTextNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SetHeaderTextNode"
bl_label = "Set Header Text"
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_property_input("Area")
self.add_string_input("Text")
self.add_boolean_input("Clear Text")
def evaluate(self, context):
self.code = f"""
{self.inputs["Area"].python_value}.header_text_set(None if {self.inputs["Clear Text"].python_value} else {self.inputs["Text"].python_value})
{self.indent(self.outputs[0].python_value, 2)}
"""
@@ -0,0 +1,23 @@
import bpy
from ....utils import normalize_code
from ...base_node import SN_ScriptingBaseNode
class SN_SetStatusTextNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_SetStatusTextNode"
bl_label = "Set Status Text"
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_execute_output()
self.add_string_input("Text")
self.add_boolean_input("Clear Text")
def evaluate(self, context):
self.code = f"""
bpy.context.workspace.status_text_set(None if {self.inputs["Clear Text"].python_value} else {self.inputs["Text"].python_value})
{self.indent(self.outputs[0].python_value, 2)}
"""
@@ -0,0 +1,351 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
from ..templates.PropertyNode import PropertyNode
from ...utils import get_python_name, unique_collection_name
class SN_OperatorNode(SN_ScriptingBaseNode, bpy.types.Node, PropertyNode):
bl_idname = "SN_OperatorNode"
bl_label = "Operator"
def layout_type(self, _):
return "layout"
is_trigger = True
bl_width_default = 200
node_color = "PROGRAM"
def on_node_property_change(self, property):
self.trigger_ref_update({"property_change": property})
self._evaluate(bpy.context)
def on_node_property_add(self, property):
property.allow_pointers = False
self.trigger_ref_update({"property_add": property})
self._evaluate(bpy.context)
def on_node_property_remove(self, index):
self.trigger_ref_update({"property_remove": index})
self._evaluate(bpy.context)
def on_node_property_move(self, from_index, to_index):
self.trigger_ref_update({"property_move": (from_index, to_index)})
self._evaluate(bpy.context)
def on_node_name_change(self):
new_name = self.name.replace('"', "'")
if not self.name == new_name:
names = []
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for ref in ntree.node_collection("SN_OperatorNode").refs:
names.append(ref.node.name)
new_name = unique_collection_name(self.name,
"My Operator",
names,
" ",
includes_name=True)
if not self.name == new_name:
self["name"] = new_name
self.trigger_ref_update()
self._evaluate(bpy.context)
def update_description(self, context):
self["operator_description"] = self.operator_description.replace(
'"', "'")
self._evaluate(context)
def update_popup(self, context):
# width input
if self.invoke_option in ["invoke_props_dialog", "invoke_popup"]:
if not "Width" in self.inputs:
self.add_integer_input("Width").default_value = 300
else:
if "Width" in self.inputs:
self.inputs.remove(self.inputs["Width"])
# interface output
if self.invoke_option in ["invoke_props_dialog", "invoke_props_popup"]:
if not "Popup" in self.outputs:
self.add_dynamic_interface_output("Popup")
else:
if "Popup" in self.outputs:
for i in range(len(self.outputs) - 1, -1, -1):
if self.outputs[i].name == "Popup":
self.outputs.remove(self.outputs[i])
# filepath outputs
if self.invoke_option == "IMPORT" or self.invoke_option == "EXPORT":
if not "Filepath" in self.outputs:
self.add_string_output("Filepath")
self.add_list_output("Filepaths")
if self.invoke_option == "IMPORT":
self.outputs["Filepaths"].set_hide(not self.allow_multiselect)
else:
self.outputs["Filepaths"].set_hide(True)
else:
if "Filepath" in self.outputs:
self.outputs.remove(self.outputs["Filepath"])
self.outputs.remove(self.outputs["Filepaths"])
self._evaluate(context)
operator_description: bpy.props.StringProperty(
name="Description",
description="Description of the operator",
update=update_description,
)
invoke_option: bpy.props.EnumProperty(
name="Popup",
items=[
("none", "None", "None"),
(
"invoke_confirm",
"Confirm",
"Shows a confirmation option for this operator",
),
("invoke_props_dialog", "Popup",
"Opens a customizable property dialog"),
(
"invoke_props_popup",
"Property Update",
"Show a customizable dialog and execute the operator on property changes",
),
(
"invoke_search_popup",
"Search Popup",
"Opens a search menu from a selected enum property",
),
("IMPORT", "Import File Browser",
"Opens a filebrowser to select items"),
("EXPORT", "Export File Browser",
"Opens a filebrowser to a location"),
(
"invoke_popup",
"Show Properties",
"Shows a popup with the operators properties",
),
],
update=update_popup,
)
select_property: bpy.props.StringProperty(
name="Preselected Property",
description=
"The property that is preselected when the popup is opened. This can only be a String or Enum Property!",
update=SN_ScriptingBaseNode._evaluate,
)
def update_multiselect(self, context):
self.outputs["Filepaths"].set_hide(not self.allow_multiselect)
self._evaluate(context)
allow_multiselect: bpy.props.BoolProperty(
default=False,
name="Multiselect",
description="Return multiple selected items",
update=update_multiselect,
)
extensions: bpy.props.StringProperty(
default=".png,.jpg,.exr",
name="File Extensions",
description=
"Allowed file extensions (separated by comma, empty means all are allowed)",
update=SN_ScriptingBaseNode._evaluate,
)
export_extension: bpy.props.StringProperty(
default=".png",
name="Export Extension",
description="Extension that the file is exported with",
update=SN_ScriptingBaseNode._evaluate,
)
hide_properties: bpy.props.BoolProperty(
default=False,
name="Hide Properties",
description="Hide the properties section of this operator",
)
@property
def operator_python_name(self):
return (get_python_name(self.name, replacement="my_generic_operator") +
f"_{self.static_uid.lower()}")
def on_create(self, context):
self.add_boolean_input("Disable")
self.add_string_input("Disabled Warning")
self.add_execute_output("Execute")
self.add_execute_output("Before Popup")
def draw_node(self, context, layout):
row = layout.row(align=True)
row.prop(self, "name")
python_name = get_python_name(self.name,
replacement="my_generic_operator")
row.operator("sn.find_referencing_nodes", text="",
icon="VIEWZOOM").node = self.name
row.operator("sn.copy_python_name", text="",
icon="COPYDOWN").name = ("sna." + python_name)
layout.label(text="Description: ")
layout.prop(self, "operator_description", text="")
layout.prop(self, "invoke_option")
if self.invoke_option == "IMPORT" or self.invoke_option == "EXPORT":
layout.prop(self, "extensions")
if self.invoke_option == "IMPORT":
layout.prop(self, "allow_multiselect")
elif self.invoke_option == "EXPORT":
layout.prop(self, "export_extension")
elif self.invoke_option == "invoke_search_popup":
layout.label(text="Search: ")
layout.prop_search(self,
"select_property",
self,
"properties",
text="")
if (self.select_property in self.properties
and self.properties[self.select_property].property_type
!= "Enum"):
row = layout.row()
row.alert = True
row.label(text="This property needs to be type Enum!")
elif not self.invoke_option in ["none", "invoke_confirm"]:
layout.label(text="Selected: ")
layout.prop_search(self,
"select_property",
self,
"properties",
text="")
if self.select_property in self.properties and not self.properties[
self.select_property].property_type in ["Enum", "String"]:
row = layout.row()
row.alert = True
row.label(
text="This property needs to be type Enum or String!")
if not self.hide_properties:
self.draw_list(layout)
def draw_node_panel(self, context, layout):
layout.prop(self, "hide_properties")
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")
selected_property = ""
invoke_return = "self.execute(context)"
invoke_inline = ""
if not self.invoke_option in ["none", "invoke_confirm"]:
if self.select_property in self.properties and self.properties[
self.select_property].property_type in ["Enum", "String"]:
selected_property = f"bl_property = '{self.properties[self.select_property].python_name}'"
if self.invoke_option == "invoke_confirm":
invoke_return = ("context.window_manager." + self.invoke_option +
"(self, event)")
elif self.invoke_option in ["invoke_props_dialog", "invoke_popup"]:
invoke_return = (
"context.window_manager." + self.invoke_option +
f"(self, width={self.inputs['Width'].python_value})")
elif self.invoke_option == "invoke_search_popup":
if (self.select_property in self.properties
and self.properties[self.select_property].property_type
== "Enum"):
selected_property = f"bl_property = '{self.properties[self.select_property].python_name}'"
invoke_inline = "context.window_manager." + self.invoke_option + "(self)"
else:
if not self.invoke_option in ["none", "IMPORT", "EXPORT"]:
invoke_inline = ("context.window_manager." +
self.invoke_option + "(self, event)")
draw_function = ""
if self.invoke_option in ["invoke_props_dialog", "invoke_props_popup"]:
draw_function = f"""
def draw(self, context):
layout = self.layout
{self.indent([out.python_value for out in self.outputs[2:-1]], 7)}"""
helpers = ""
extensions = ""
exp_ext = ""
files = ""
if self.invoke_option == "IMPORT" or self.invoke_option == "EXPORT":
helpers = (", ImportHelper"
if self.invoke_option == "IMPORT" else ", ExportHelper")
if self.extensions:
extensions = f"filter_glob: bpy.props.StringProperty( default='{self.extensions.replace('.', '*.').replace(',', ';')}', options={{'HIDDEN'}} )"
if self.invoke_option == "EXPORT":
if self.export_extension:
exp_ext = f"filename_ext = '{self.export_extension}'"
elif self.invoke_option == "IMPORT" and self.allow_multiselect:
files = "files: bpy.props.CollectionProperty(name='Filepaths', type=bpy.types.OperatorFileListElement)"
self.outputs["Filepath"].python_value = "self.filepath"
if self.invoke_option == "IMPORT" and self.allow_multiselect:
self.outputs[
"Filepaths"].python_value = "[os.path.join(os.path.dirname(self.filepath), f.name) for f in self.files]"
code = f"""
{self.indent(props_imperative_list, 5)}
class SNA_OT_{self.operator_python_name.title()}(bpy.types.Operator{helpers}):
bl_idname = "sna.{self.operator_python_name}"
bl_label = "{self.name}"
bl_description = "{self.operator_description}"
bl_options = {"{" + '"REGISTER", "UNDO"' + "}"}
{extensions}
{exp_ext}
{files}
{selected_property}
{self.indent(props_code_list, 6)}
@classmethod
def poll(cls, context):
if bpy.app.version >= (3, 0, 0) and {'Disabled Warning' in self.inputs}:
cls.poll_message_set({self.inputs['Disabled Warning'].python_value if 'Disabled Warning' in self.inputs else ""})
return not {self.inputs[0].python_value}
def execute(self, context):
{self.indent(self.outputs[0].python_value, 7)}
return {{"FINISHED"}}
{draw_function}
"""
invoke = f"""
def invoke(self, context, event):
{self.indent(self.outputs[1].python_value, 7)}
{invoke_inline}
return {invoke_return}
"""
if not self.invoke_option in ["IMPORT", "EXPORT"]:
code += invoke
self.code = code
self.code_register = f"""
{self.indent(props_register_list, 7)}
bpy.utils.register_class(SNA_OT_{self.operator_python_name.title()})
"""
self.code_unregister = f"""
{self.indent(props_unregister_list, 7)}
bpy.utils.unregister_class(SNA_OT_{self.operator_python_name.title()})
"""
if self.invoke_option == "IMPORT" or self.invoke_option == "EXPORT":
self.code_import = """
import os
from bpy_extras.io_utils import ImportHelper, ExportHelper
"""
@@ -0,0 +1,274 @@
import bpy
from ..base_node import SN_ScriptingBaseNode
def on_operator_ref_update(self, node, data, ntree, node_ref_name, input_offset=1):
if node.node_tree == ntree and node.name == node_ref_name:
if data:
if "property_change" in data:
prop = data["property_change"]
for inp in self.inputs[input_offset:]:
if not inp.name in node.properties:
inp.name = prop.name
if inp.name == prop.name:
if prop.property_type in ["Integer", "Float", "Boolean"] and prop.settings.is_vector:
socket = self.convert_socket(inp, self.socket_names[prop.property_type + " Vector"])
socket.size = prop.settings.size
else:
prop_type = prop.property_type
if prop_type == "Enum" and prop.stngs_enum.enum_flag:
prop_type = "Enum Set"
socket = self.convert_socket(inp, self.socket_names[prop_type])
if hasattr(prop.settings, "subtype"):
if prop.settings.subtype in socket.subtypes:
socket.subtype = prop.settings.subtype
else:
socket.subtype = "NONE"
if prop.property_type == "Enum":
socket.subtype = "CUSTOM_ITEMS"
socket.custom_items.clear()
socket.custom_items_editable = False
for item in prop.settings.items:
socket.custom_items.add().name = item.name
if "property_add" in data:
prop = data["property_add"]
self._add_input(self.socket_names[prop.property_type], prop.name).can_be_disabled = True
if "property_remove" in data:
self.inputs.remove(self.inputs[data["property_remove"] + input_offset])
if "property_move" in data:
from_index = data["property_move"][0]
to_index = data["property_move"][1]
self.inputs.move(from_index+input_offset, to_index+input_offset)
self._evaluate(bpy.context)
class SN_RunOperatorNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunOperatorNode"
bl_label = "Run Operator"
node_color = "PROGRAM"
bl_width_default = 240
def on_create(self, context):
self.version = 1
self.add_execute_input()
self.add_execute_output()
self.ref_ntree = self.node_tree
def get_context_items(self,context):
items = []
areas = ["DEFAULT", "VIEW_3D", "IMAGE_EDITOR", "NODE_EDITOR", "SEQUENCE_EDITOR", "CLIP_EDITOR", "DOPESHEET_EDITOR",
"DOPESHEET_ACTION_EDITOR", "DOPESHEET_SHAPEKEY_EDITOR", "DOPESHEET_GREASE_PENCIL", "DOPESHEET_MASK_EDITOR", "DOPESHEET_CACHE_FILE",
"GRAPH_EDITOR", "NLA_EDITOR", "TEXT_EDITOR", "CONSOLE", "INFO", "TOPBAR", "STATUSBAR", "OUTLINER",
"PROPERTIES", "FILE_BROWSER", "PREFERENCES"]
for area in areas:
items.append((area,area.replace("_"," ").title(),area.replace("_"," ").title()))
return items
context: bpy.props.EnumProperty(name="Operator Context", description="The context this operator should run in",
items=get_context_items, update=SN_ScriptingBaseNode._evaluate)
use_invoke: bpy.props.BoolProperty(name="Use Invoke",
description="This will run the before popup output and keep the interactive elements. It won't wait for the operations you connect to this nodes output",
default=True,
update=SN_ScriptingBaseNode._evaluate)
def on_ref_update(self, node, data=None):
on_operator_ref_update(self, node, data, self.ref_ntree, self.ref_SN_OperatorNode)
def reset_inputs(self):
""" Remove all operator inputs """
for inp in self.inputs[1:]:
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 = not prop.is_required
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()
if self.pasted_operator:
self.disable_evaluation = True
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.disable_evaluation = False
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):
context_modes = {
"DOPESHEET_ACTION_EDITOR": "ACTION",
"DOPESHEET_SHAPEKEY_EDITOR": "SHAPEKEY",
"DOPESHEET_GREASE_PENCIL": "GPENCIL",
"DOPESHEET_MASK_EDITOR": "MASK",
"DOPESHEET_CACHE_FILE": "CACHEFILE"
}
set_context_mode = ""
set_context = ""
if self.context != "DEFAULT":
set_context = f"bpy.context.area.type = '{self.context}'"
if self.context in context_modes:
set_context_mode = f"bpy.context.space_data.mode = '{context_modes[self.context]}'"
set_context = "bpy.context.area.type = 'DOPESHEET_EDITOR'"
invoke = "" if not self.use_invoke else "'INVOKE_DEFAULT', "
if self.source_type == "BLENDER":
op_name = self.pasted_operator[8:].split("(")[0]
op = eval(self.pasted_operator.split("(")[0])
op_rna = op.get_rna_type()
parameters = ""
for inp in self.inputs[1:]:
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)):
parameters += f"{prop.identifier}={inp.python_value}, "
self.code = f"""
{'prev_context = bpy.context.area.type' if set_context else ''}
{set_context}
{set_context_mode}
bpy.ops.{op_name}({invoke}{parameters[:-2]})
{'bpy.context.area.type = prev_context' if set_context else ''}
{self.indent(self.outputs[0].python_value, 6)}
"""
else:
self.code = f"""
{self.indent(self.outputs[0].python_value, 6)}
"""
if self.ref_ntree and self.ref_SN_OperatorNode:
node = self.ref_ntree.nodes[self.ref_SN_OperatorNode]
parameters = ""
for inp in self.inputs[1:]:
if not inp.disabled:
for prop in node.properties:
if prop.name == inp.name:
parameters += f"{prop.python_name}={inp.python_value}, "
self.code = f"""
{'prev_context = bpy.context.area.type' if set_context else ''}
{set_context}
{set_context_mode}
bpy.ops.sna.{node.operator_python_name}({invoke}{parameters[:-2]})
{'bpy.context.area.type = prev_context' if set_context else ''}
{self.indent(self.outputs[0].python_value, 7)}
"""
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)
layout.prop(self, "context")
layout.prop(self, "use_invoke")
@@ -0,0 +1,58 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RunInIntervalsNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunInIntervalsNode"
bl_label = "Run In Intervals"
bl_width_default = 200
node_color = "PROGRAM"
def on_emergency_stop(self, context):
if self.emergency_stop:
if self.static_uid in bpy.context.scene.sn.function_store:
bpy.app.timers.unregister(bpy.context.scene.sn.function_store[self.static_uid])
del bpy.context.scene.sn.function_store[self.static_uid]
self.emergency_stop = False
emergency_stop: bpy.props.BoolProperty(default=False, update=on_emergency_stop)
def on_create(self, context):
self.add_execute_input()
self.add_float_input("Initial Delay").default_value = 1
self.add_float_input("Interval Delay").default_value = 1
self.add_boolean_input("Stop Condition").default_value = False
self.add_execute_output("Interval")
self.add_execute_output("Instant")
def draw_node(self, context, layout):
layout.prop(self, "emergency_stop", text="Manual Stop", toggle=True, invert_checkbox=self.static_uid in bpy.context.scene.sn.function_store)
def evaluate(self, context):
self.code = f"""
def delayed_{self.static_uid}():
{self.indent(self.outputs['Interval'].python_value, 6) if self.outputs['Interval'].python_value.strip() else 'pass'}
if {self.inputs['Stop Condition'].python_value}:
return None
return {self.inputs['Interval Delay'].python_value}
bpy.app.timers.register(delayed_{self.static_uid}, first_interval={self.inputs['Initial Delay'].python_value})
bpy.context.scene.sn.function_store["{self.static_uid}"] = delayed_{self.static_uid}
{self.indent(self.outputs['Instant'].python_value, 5)}
"""
def evaluate_export(self, context):
self.code = f"""
def delayed_{self.static_uid}():
{self.indent(self.outputs['Interval'].python_value, 6) if self.outputs['Interval'].python_value.strip() else 'pass'}
if {self.inputs['Stop Condition'].python_value}:
return None
return {self.inputs['Interval Delay'].python_value}
bpy.app.timers.register(delayed_{self.static_uid}, first_interval={self.inputs['Initial Delay'].python_value})
{self.indent(self.outputs['Instant'].python_value, 5)}
"""
@@ -0,0 +1,41 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RunMultipleTimesNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunMultipleTimesNode"
bl_label = "Run Multiple Times"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_float_input("Initial Delay").default_value = 1
self.add_float_input("Interval Delay").default_value = 1
self.add_integer_input("Number of Times").default_value = 2
self.add_execute_output("Delayed")
self.add_execute_output("Instant")
def evaluate(self, context):
self.code_imperative = f"""
count_{self.static_uid} = 0
"""
self.code = f"""
global count_{self.static_uid}
count_{self.static_uid} = 0
def delayed_{self.static_uid}():
global count_{self.static_uid}
{self.indent(self.outputs['Delayed'].python_value, 6) if self.outputs['Delayed'].python_value.strip() else 'pass'}
count_{self.static_uid} += 1
if count_{self.static_uid} >= {self.inputs['Number of Times'].python_value}:
return None
return {self.inputs['Interval Delay'].python_value}
bpy.app.timers.register(delayed_{self.static_uid}, first_interval={self.inputs['Initial Delay'].python_value})
{self.indent(self.outputs['Instant'].python_value, 5)}
"""
@@ -0,0 +1,27 @@
import bpy
from ...base_node import SN_ScriptingBaseNode
class SN_RunWithDelayNode(SN_ScriptingBaseNode, bpy.types.Node):
bl_idname = "SN_RunWithDelayNode"
bl_label = "Run With Delay"
bl_width_default = 200
node_color = "PROGRAM"
def on_create(self, context):
self.add_execute_input()
self.add_float_input("Delay").default_value = 1
self.add_execute_output("Delayed")
self.add_execute_output("Instant")
def evaluate(self, context):
self.code = f"""
def delayed_{self.static_uid}():
{self.indent(self.outputs['Delayed'].python_value, 6) if self.outputs['Delayed'].python_value.strip() else 'pass'}
bpy.app.timers.register(delayed_{self.static_uid}, first_interval={self.inputs['Delay'].python_value})
{self.indent(self.outputs['Instant'].python_value, 5)}
"""