2025-07-01
This commit is contained in:
@@ -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()})
|
||||
"""
|
||||
+22
@@ -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})"
|
||||
+42
@@ -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")
|
||||
+84
@@ -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")
|
||||
+120
@@ -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
|
||||
+122
@@ -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
|
||||
+23
@@ -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")
|
||||
+30
@@ -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)}
|
||||
"""
|
||||
+24
@@ -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)}
|
||||
"""
|
||||
+23
@@ -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)}
|
||||
"""
|
||||
+41
@@ -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)}
|
||||
"""
|
||||
Reference in New Issue
Block a user