10
$\begingroup$

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.

$\endgroup$

2 Answers 2

11
+50
$\begingroup$

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")
$\endgroup$
7
  • $\begingroup$ 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. $\endgroup$
    – matali
    Sep 21, 2013 at 7:00
  • $\begingroup$ 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). $\endgroup$
    – lukas_t
    Sep 21, 2013 at 8:36
  • $\begingroup$ it says : 385794616. Running "Translate Cursor" after that seems to remove it : KeyError: 'bpy_struct[key]: key "_prev_object" not found' $\endgroup$
    – matali
    Sep 21, 2013 at 9:06
  • $\begingroup$ 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 ? $\endgroup$
    – matali
    Sep 28, 2013 at 17:28
  • 1
    $\begingroup$ The script didn't work correct with 64 bit pointers, fixed that, it should work everywhere now. $\endgroup$
    – brecht
    Oct 4, 2013 at 15:43
7
$\begingroup$

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)
$\endgroup$
8
  • $\begingroup$ 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 ? $\endgroup$
    – matali
    Sep 28, 2013 at 17:07
  • $\begingroup$ 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. $\endgroup$
    – matali
    Sep 28, 2013 at 17:29
  • $\begingroup$ 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?! $\endgroup$
    – CodeManX
    Sep 28, 2013 at 17:30
  • $\begingroup$ 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) $\endgroup$
    – matali
    Sep 28, 2013 at 18:24
  • $\begingroup$ 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. $\endgroup$
    – CodeManX
    Sep 28, 2013 at 19:30

You must log in to answer this question.

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