16

I have a script, which creates multiple objects through a loop. However, the script freezes the viewport until it is done. Instead, I would like every object to be seen in the viewport one by one as it is created.

In its simplest terms:

import bpy
import random
for i in range(1000):
    XYZcoord = (random.random()*100, random.random()*100, random.random()*100)
    bpy.ops.mesh.primitive_uv_sphere_add(location=XYZcoord)
    # Update the viewport here.

Is there a way to update the viewport after every iteration of the loop to see every newly added object as they are created? Or do I need to use a different method?

dwitvliet
  • 409
  • 1
  • 7
  • 19

3 Answers3

22

The UI (including viewport ) can be updated during the script execution but it is advised against it in the Blender API documentation for these reasons :

Tools that lock Blender in a loop and redraw are highly discouraged since they conflict with Blenders ability to run multiple operators at once and update different parts of the interface as the tool runs.

so to update the UI during execution you can use wm.redraw_timer() :

import bpy
import random
for i in range(1000):
    XYZcoord = (random.random()*100, random.random()*100, random.random()*100)
    bpy.ops.mesh.primitive_uv_sphere_add(location=XYZcoord)
    # Update the viewport here.
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)

a good alternative has been suggested by @Zeffii which is the modal operator triggered by timer

Chebhou
  • 19,533
  • 51
  • 98
  • 2
    Thank you! This hack works great for a quick solution and will be sufficient in many cases. However, I have accepted the answer by @zeffii, as it probably will suit more people. – dwitvliet Apr 21 '15 at 02:50
16

Text Editor > Python > Templates > Operator Modal Timer

I quickly cobbled together a small edit to that template, which I think does what you want. Run it from Text Editor, read the code you don't understand slowly and follow the flow of control carefully. There is no need to manually redraw.

import bpy
import random

class ModalTimerOperator(bpy.types.Operator): """Operator which runs its self from a timer""" bl_idname = "wm.modal_timer_operator" bl_label = "Modal Timer Operator"

limits : bpy.props.IntProperty(default=0) #not 'limits ='
_timer = None

def modal(self, context, event):
    if event.type in {'RIGHTMOUSE', 'ESC'} or self.limits > 30:
        self.limits = 0
        self.cancel(context)
        return {'FINISHED'}

    if event.type == 'TIMER':
        XYZcoord = (random.random()*100, random.random()*100, random.random()*100)
        bpy.ops.mesh.primitive_uv_sphere_add(location=XYZcoord)
        self.limits += 1

    return {'PASS_THROUGH'}

def execute(self, context):
    wm = context.window_manager
    self._timer = wm.event_timer_add(time_step=0.1, window=context.window)
    wm.modal_handler_add(self)
    return {'RUNNING_MODAL'}

def cancel(self, context):
    wm = context.window_manager
    wm.event_timer_remove(self._timer)


def register(): bpy.utils.register_class(ModalTimerOperator)

def unregister(): bpy.utils.unregister_class(ModalTimerOperator)

if name == "main": register()

# test call
bpy.ops.wm.modal_timer_operator()

zeffii
  • 39,634
  • 9
  • 103
  • 186
2

bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) used to work in older versions of Blender, but as of Blender 3.X it doesn't do the trick any more. Through trial and error I found a sequence of operations that does the trick as of Blender 3.1.2, packaged into a simple function:

def Update3DViewPorts():
    for area in bpy.context.window.screen.areas:
        if area.type == 'VIEW_3D':
            area.tag_redraw()
    bpy.ops.wm.save_mainfile()
elentru
  • 31
  • 1
  • I can't reproduce the problem you found in Blender 3.X. On Blender 3.2 in a simple script bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) worked perfectly (though admittedly it was very simple, just moving the camera around). However, it did update the interface as expected. – Joseph Farah Jul 01 '23 at 23:19