OK @Federaik some pointers to keep you going:
https://docs.blender.org/api/current/bpy.app.handlers.html?highlight=handlers#module-bpy.app.handlers
There are some basic examples there.
In my node system I also use handlers
, some code snippets:
So, this one looks for an execute
call in the node:
def start_exec(scene):
"""Run Execute Function in Nodes"""
for nodetree in [
n for n in bpy.data.node_groups if n.rna_type.name == "Clockworx Music Editor"
]:
for n in nodetree.nodes:
if (hasattr(n, "execute")):
n.execute()
It’s just basically looking for the operation calls in the node, something like this node:
import bpy
from .._base.base_node import CM_ND_BaseNode
from ..cm_functions import connected_node_output
class CM_ND_AudioDebugNode(bpy.types.Node, CM_ND_BaseNode):
bl_idname = "cm_audio.debug_node"
bl_label = "Display Info"
bl_icon = "SPEAKER"
bl_width_default = 220
num_entries : bpy.props.IntProperty(name="Entries #")
output : bpy.props.StringProperty(name="Output", default="")
text_input_1 : bpy.props.StringProperty(name="Out-1", default="")
text_input_2 : bpy.props.StringProperty(name="Out-2", default="")
text_input_3 : bpy.props.StringProperty(name="Out-3", default="")
text_input_4 : bpy.props.StringProperty(name="Out-4", default="")
text_input_5 : bpy.props.StringProperty(name="Out-5", default="")
out_term : bpy.props.BoolProperty(name="View in Terminal", default=False)
def init(self, context):
self.inputs.new("cm_socket.generic", "Input")
def draw_buttons(self, context, layout):
layout.label(text=f"Number of entries: {self.num_entries}", icon = "INFO")
layout.prop(self, "output")
layout.prop(self, "text_input_1", text="")
layout.prop(self, "text_input_2", text="")
layout.prop(self, "text_input_3", text="")
layout.prop(self, "text_input_4", text="")
layout.prop(self, "text_input_5", text="")
layout.context_pointer_set("audionode", self)
layout.operator("cm_audio.display_audio")
layout.prop(self, "out_term")
def function(self):
input = connected_node_output(self, 0)
index = 0
is_list = False
self.text_input_1 = ""
self.text_input_2 = ""
self.text_input_3 = ""
self.text_input_4 = ""
self.text_input_5 = ""
if isinstance(input, list):
self.num_entries = len(input)
if self.out_term:
print("Viewer: '{}'".format(self.name))
print(" ")
ind = 0
for v in input:
print(f"Item {ind}: {v}")
ind = ind + 1
else:
self.output = str(input)
is_list = True
if len(input) > 0:
input = input[0]
if isinstance(input, dict):
self.num_entries = len(input.keys())
if self.out_term:
print("Viewer: '{}'".format(self.name))
print(" ")
for key,value in input.items():
if isinstance(value, list):
print(f"Key: {key}")
ind = 0
for v in value:
print(f"Item {ind}: {v}")
ind = ind + 1
else:
print("Key : {} , Value : {}".format(key,value))
if "collections" in input.keys():
collections = input["collections"]
if not isinstance(collections, list):
collections = [collections]
elif "objects" in input.keys():
objects = input["objects"]
if not isinstance(objects, list):
objects = [objects]
if not is_list:
self.output = str(input)
for i in input.keys():
if index == 0:
self.text_input_1 = f"{i}: {input[i]}"
if index == 1:
self.text_input_2 = f"{i}: {input[i]}"
if index == 2:
self.text_input_3 = f"{i}: {input[i]}"
if index == 3:
self.text_input_4 = f"{i}: {input[i]}"
if index == 4:
self.text_input_5 = f"{i}: {input[i]}"
index = index + 1
else:
if self.out_term:
print(input)
self.output = str(input)
def info(self, context):
self.function()
def execute(self):
self.function()
This is the node:
You can see at the bottom it has and execute
function which calls previous code (function
).
So if you add an operator to menu, for example, there are many ways to trigger handlers, it will look something like this:
You should see the Start Exec and Stop Exec buttons…
This is the code for the Start Exec button:
import bpy
from ..cm_functions import start_exec
class CM_OT_ExecuteStartOperator(bpy.types.Operator):
bl_idname = "cm_audio.execute_start"
bl_label = "CM Execute Start"
@classmethod
def poll(cls, context):
return start_exec not in bpy.app.handlers.frame_change_post
def execute(self, context):
cm = context.scene.cm_pg
if start_exec not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(start_exec)
return {"FINISHED"}
So when this is clicked it starts the handler running, by adding it to the frame_change_post
handlers list. then at every frame change, in this case, every node that has an execute
function will be activated, until you stop the handler with a function like this:
import bpy
from ..cm_functions import start_exec
class CM_OT_ExecuteStopOperator(bpy.types.Operator):
bl_idname = "cm_audio.execute_stop"
bl_label = "CM Execute Stop"
@classmethod
def poll(cls, context):
return start_exec in bpy.app.handlers.frame_change_post
def execute(self, context):
cm = context.scene.cm_pg
if start_exec in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(start_exec)
return {"FINISHED"}
Once you have removed the handler form frame_change_post
it stops running. Now you can also combine/replace these with Timers:
https://docs.blender.org/api/current/bpy.app.timers.html?highlight=timers#module-bpy.app.timers
They let you run a function at time intervals, rather than on frame change. Beware here though not to run very complex functions at very short time intervals, or you will overload your CPU.
Let me know how you get on and I can help some more, if necessary I can put some test code together to check the Custom Property and change the material, but have a go yourself and see how you get on first. You will need to check that the object has a material and for starters, make sure it has only one material in material_slot[0]
. Good luck!
Cheers, Clock.