# Alternate between user input and automation in one-step script

I would like my script to do the following :

1. Save selected object's name and add an object (automation)
2. Let user move the added object where he wants (user input, stopped with leftclick)
3. Move the 3D cursor to where the object was moved, delete that object and reselect the original object (automation)
4. Let user move the original object where he wants (user input, stopped with leftclick) and terminate the script.

I tried to modify the template "simple modal operator", but without success. I looked the doc, googled, watched other scripts which are modal... nothing. The best I did get was to reach step 3, but then the script seems to ignore the line where I change the active object. The same line works in the python console...

I don't specifically want the solution for my case, but at least if someone could explain how to properly alternate many times between user input and automated steps, it would be nice.

Macro Operators are a way to achieve this (see script below, this does pretty much what you describe). They allow you to concatenate modal and non-modal operators.

Note that adding an object and deleting it right away repeatedly can lead to a lot of 0-user objects accumulating in bpy.data.

import bpy
from bpy.types import Operator, Macro
from bpy.props import *

class TranslateCursorMacro(Macro):
"""Overall macro for combining move and cursor operators"""
bl_idname = "object.translate_cursor_macro"
bl_label = "Translate Cursor"
bl_options = {'REGISTER', 'UNDO'}

@classmethod
def poll(cls, context):
if not context.active_object:
return False
return True

def _find_prev_object(ptr):
for ob in bpy.data.objects:
if str(ob.as_pointer()) == ptr:
return ob

class RememberObject(Operator):
"""Store active object"""
bl_idname = "object.remember_object"
bl_label = "Remember Object"

def execute(self, context):
# XXX This is not super nice, but we don't have real
# properties shared between macro component operators.
# In an addon this might be stored as a module-level
# python variable. Store as string because integer is
# only 32 bit and may truncate 64 bit pointer.
context.scene["_prev_object"] = str(context.active_object.as_pointer())
return {'FINISHED'}

class DeleteNoQuestionsObject(Operator):
bl_idname = "object.delete_no_questions"
bl_label = "Delete Object"

def execute(self, context):
# XXX The usual bpy.ops operator opens a popup
# when invoke. Use this wrapper to avoid that.
bpy.ops.object.delete('EXEC_DEFAULT')
return {'FINISHED'}

class SelectPreviousObject(Operator):
"""Select previous object"""
bl_idname = "object.select_previous_object"
bl_label = "Select Previous Object"

def execute(self, context):
ptr = context.scene.get("_prev_object", None)
if ptr is None:
return {'CANCELLED'}
del context.scene["_prev_object"] # cleanup
ob = _find_prev_object(ptr)
if not ob:
return {'CANCELLED'}

for tob in bpy.data.objects:
tob.select = (tob == ob)
context.scene.objects.active = ob
return {'FINISHED'}

bpy.utils.register_class(TranslateCursorMacro)
bpy.utils.register_class(RememberObject)
bpy.utils.register_class(DeleteNoQuestionsObject)
bpy.utils.register_class(SelectPreviousObject)

TranslateCursorMacro.define("OBJECT_OT_remember_object")
TranslateCursorMacro.define("TRANSFORM_OT_translate")
TranslateCursorMacro.define("VIEW3D_OT_snap_cursor_to_active")
TranslateCursorMacro.define("OBJECT_OT_delete_no_questions")
TranslateCursorMacro.define("OBJECT_OT_select_previous_object")

• Thanks Lukas :) I'm having an error "previous object not found" when using "Translate Cursor". Even using "Remember Object" manually before doesn't help. Sphere is added, cursor is moved, sphere is deleted. The default Cube was selected. Tested on 2.65 (crash) and latest SVN. – matali Sep 21 '13 at 7:00
• What does bpy.context.scene["_prev_object"] say after running the "RememberObject" operator? It should store the as_pointer() number there. This number is a consistent identifier during a blender session (does not stay valid after restart/load file, but is not affected by name changes). – lukas_t Sep 21 '13 at 8:36
• it says : 385794616. Running "Translate Cursor" after that seems to remove it : KeyError: 'bpy_struct[key]: key "_prev_object" not found' – matali Sep 21 '13 at 9:06
• You must have good reason why you programmed the part to save, find and activate the originally selected object that way. Could you please explain why you did so or confirm that CoDEmanX's version is as resilient/stable as yours ? – matali Sep 28 '13 at 17:28
• The script didn't work correct with 64 bit pointers, fixed that, it should work everywhere now. – brecht Oct 4 '13 at 15:43

Great script, lukas_t! I didn't even know about Macros :D

That fancy object ptr thing works for me, no error message in 2.67.

I solved the "remember last object"-problem in another way, maybe that is not going to give errors for you, matali. I simply used static class attributes to store snap settings and the object.

import bpy
from bpy.types import Operator, Macro
from bpy.props import *

class TranslateCursorMacro(Macro):
"""Overall macro for combining move and cursor operators"""
bl_idname = "object.translate_cursor_macro"
bl_label = "Translate Cursor"
bl_options = {'REGISTER', 'UNDO'}

@classmethod
def poll(cls, context):
if not context.active_object:
return False
return True

"""
def _find_prev_object(ptr):
for ob in bpy.data.objects:
if ob.as_pointer() == ptr:
return ob
"""

class Storage:
ob = None
use_snap = False
snap_element = 'CLOSEST'
snap_target = 'INCREMENT'

class RememberObject(Operator):
"""Store active object"""
bl_idname = "object.remember_object"
bl_label = "Remember Object"

def execute(self, context):
# XXX This is not super nice, but we don't have real
# properties shared between macro component operators.
# In an addon this might be stored as a module-level
# python variable.
context.scene["_prev_object"] = context.active_object.as_pointer()

Storage.ob = context.active_object
Storage.use_snap = context.tool_settings.use_snap
Storage.snap_element = context.tool_settings.snap_element
Storage.snap_target = context.tool_settings.snap_target
context.tool_settings.use_snap = True
context.tool_settings.snap_element = 'VERTEX'
context.tool_settings.snap_target = 'ACTIVE'

return {'FINISHED'}

class DeleteNoQuestionsObject(Operator):
bl_idname = "object.delete_no_questions"
bl_label = "Delete Object"

def execute(self, context):
# XXX The usual bpy.ops operator opens a popup
# when invoke. Use this wrapper to avoid that.
bpy.ops.object.delete('EXEC_DEFAULT')
return {'FINISHED'}

class SelectPreviousObject(Operator):
"""Select previous object"""
bl_idname = "object.select_previous_object"
bl_label = "Select Previous Object"

def execute(self, context):
context.tool_settings.use_snap = Storage.use_snap
context.tool_settings.snap_element = Storage.snap_element
context.tool_settings.snap_target = Storage.snap_target

ob = Storage.ob
if ob is None:
return {'CANCELLED'}

"""
ptr = context.scene.get("_prev_object", None)
if ptr is None:
return {'CANCELLED'}
del context.scene["_prev_object"] # cleanup
ob = _find_prev_object(ptr)
if not ob:
return {'CANCELLED'}
"""

for tob in bpy.data.objects:
# Avoid checking every ob, just deselect all an re-reselt previous
#tob.select = (tob == ob)
tob.select = False
ob.select = True
context.scene.objects.active = ob
return {'FINISHED'}

bpy.utils.register_class(TranslateCursorMacro)
bpy.utils.register_class(RememberObject)
bpy.utils.register_class(DeleteNoQuestionsObject)
bpy.utils.register_class(SelectPreviousObject)

TranslateCursorMacro.define("OBJECT_OT_remember_object")
TranslateCursorMacro.define("TRANSFORM_OT_translate")
TranslateCursorMacro.define("VIEW3D_OT_snap_cursor_to_active")
TranslateCursorMacro.define("OBJECT_OT_delete_no_questions")
TranslateCursorMacro.define("OBJECT_OT_select_previous_object")


Just in case anyone wonders how to pass parameters to the operators in the macro:

import bpy

class m(bpy.types.Macro):
bl_idname = "object.macro_test"
bl_label = "Macro Test"

bpy.utils.register_class(m)
op = m.define("TRANSFORM_OT_translate")
op.properties.constraint_axis = (True, False, False)

• It works, thanks a lot CoDEmanX, problem now is who gets the bounty ? I think Lulas can get it as he did the biggest part. You learned something from his script, and I give you a point for your fix all right ? – matali Sep 28 '13 at 17:07
• Is Luka's version working for you ? You don't get the error I have ? Maybe it's due to Linux or the SVN versions I used... If so it may be reported as a bug for 2.69. – matali Sep 28 '13 at 17:29
• Alright. Honour to lukas for sure! Need to test what Macros can do else :D //edit: Lukas' version works for me indeed, not sure if I use it right / in the same way as you do?! – CoDEmanX Sep 28 '13 at 17:30
• Well, I run the script and then spacebar -> "Translate cursor". I get the error after left clicking. What do you do ? Just tested in 2.67b, same error "object not found" after first left click (placing the sphere) – matali Sep 28 '13 at 18:24
• I select an object, turn on snapping, run operator from spacebar menu. Sphere can be snapped, left click, 3d cursor moves and sphere vanishes. Originally selected object is still active and selected, other selected objects are lost from selection however (can easily be fixed). So I guess we use it the same way, I just don't experience any errors. Tested with 2.69 testbuild 1. – CoDEmanX Sep 28 '13 at 19:30