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.

up vote 10 down vote accepted
+50

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):
    """Delete object without asking"""
    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:
            self.report({"WARNING"}, "Previous object not found")
            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("MESH_OT_primitive_uv_sphere_add")
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
  • 1
    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):
    """Delete object without asking"""
    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:
            self.report({"WARNING"}, "Previous object not found")
            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:
            self.report({"WARNING"}, "Previous object not found")
            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("MESH_OT_primitive_uv_sphere_add")
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

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.