2
$\begingroup$

I've seen lots of scripts working interactively, meaning if you change a property value the viewport updates right away. But I only saw this for mesh operations, like adding vertices and then calling mesh.update().

Is there a generic method to make your script interactive, like whatever it does? I can't call just my myClass.execute() method from within draw() as it gives me context errors. On the other hand I can't implement my whole set of operations in the draw() method for the same reason. I'm sorry if I failed to understand some basic structure / behavior of Blender python scripts but I'm just a beginner.

Could someone post an example script, like adding a cube with a property, let's say number of cubes, which updates the viewport on changing the number of cubes? That would be awesome! (EDIT: Really adding / removing cubes, not adding geometry and calling mesh.update())

$\endgroup$

1 Answer 1

5
$\begingroup$

You have basically 2 options: modal operator or scene update handler.

Modal operator is what an interactive script would use, here is an example with over-commented code. It will create panel in tools under Misc. and the operator can be ended with ESC:

import bpy
# for random positioning of cubes
# so they dont stack one over other
from random import random

class Panel(bpy.types.Panel):
    """
    This displays number_of_cubes property in tools panel
    And can run a modal operator
    """
    bl_label = "Custom Panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'

    def draw(self, context):
        # display cube count if modal operator running 
        # else display button to run it
        layout = self.layout
        if context.scene.watcher_running:
            layout.prop(context.scene, "number_of_cubes")
        else:
            layout.operator("my_custom_op.cube_watcher")

class CubeWatcher(bpy.types.Operator):
    """This will run in background monitoring user actions"""
    bl_idname = "my_custom_op.cube_watcher"
    bl_label = "Cube Watcher"

    # this function should be used to do stuff
    # in our example to adjust the number of cubes
    def execute(self, context):

        # get cubes already present in scene
        cube_list = [object for object in context.scene.objects if object.name.find("Cube") >= 0]

        if len(cube_list) < context.scene.number_of_cubes:
            # add cubes
            for i in range(context.scene.number_of_cubes - len(cube_list)):
                bpy.ops.mesh.primitive_cube_add(enter_editmode=False)
                context.active_object.location = (5-10*random(), 5-10*random(), 5-10*random())

        elif len(cube_list) > context.scene.number_of_cubes:
            # remove excess cubes
            for i in range(context.scene.number_of_cubes, len(cube_list)):
                context.scene.objects.unlink(cube_list[i])
        return

    # this function will be called everytime user does something, even moves a mouse
    # should be used to react to user input
    def modal(self, context, event):

        # when user interacts, adjust number of cubes
        # this will react to all event types
        # which is kinda inneficient but simple
        self.execute(context)

        # end modal operator when hitting ESC
        if event.type == 'ESC':
            # set the monitoring property to False
            context.scene.watcher_running = False
            return {'FINISHED'}

        # all other events pass through to blender
        return {'PASS_THROUGH'}

    # this is run when this operator is called
    # usually inicialization code is here
    def invoke(self, context, event):

        # this is important, it will run the modal function
        # without this the operator would just execute and end
        context.window_manager.modal_handler_add(self)

        # set the monitoring property to True
        context.scene.watcher_running = True

        # run self to spawn cubes
        self.execute(context)

        return {'RUNNING_MODAL'}

def register():
    # register classes so blender knows about them
    bpy.utils.register_module(__name__)
    # add a property to scene that will control number of cubes
    bpy.types.Scene.number_of_cubes = bpy.props.IntProperty(default=0, min=0)
    # add a property that will monitor if modal operator is running or not
    bpy.types.Scene.watcher_running = bpy.props.BoolProperty(default=False)

def unregister():
    # unregister classes
    bpy.utils.unregister_module(__name__)
    # remove properties
    del bpy.types.Scene.number_of_cubes
    del bpy.types.Scene.watcher_running

if __name__ == "__main__":
    register()

A handler is a function added to some event, in this case before scene updates. This will produce similar result as code before but will be much more likely buggy or laggy. Next example is simple and does not handle any error cases. Use this only when you know what you are doing.

import bpy
from random import random

def cube_count_handler(scene):
    """This function will be run everytime before scene updates"""
    cube_list = [object for object in scene.objects if object.name.find("Cube") >= 0]
    if len(cube_list) < scene.number_of_cubes:
        for i in range(scene.number_of_cubes - len(cube_list)):
            loc = (5-10*random(), 5-10*random(), 5-10*random())
            bpy.ops.mesh.primitive_cube_add(enter_editmode=False, location=loc)

    elif len(cube_list) > scene.number_of_cubes:
        for i in range(scene.number_of_cubes, len(cube_list)):
            scene.objects.unlink(cube_list[i])
    return

class PropPanel(bpy.types.Panel):
    bl_label = "Custom Panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'

    def draw(self, context):
        layout = self.layout
        layout.prop(context.scene, "number_of_cubes")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.number_of_cubes = bpy.props.IntProperty(default=0, min=0)
    # add handler if not in app.handlers
    if cube_count_handler not in bpy.app.handlers.scene_update_pre:
        bpy.app.handlers.scene_update_pre.append(cube_count_handler)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.number_of_cubes

if __name__ == "__main__":
    register()
$\endgroup$
0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .