Using Grease Pencil (Annotation) from Modal in Blender 2.8

In my addon, I need to do a bunch of stuff while running modal. One of those things is drawing with a grease pencil, and then processing the curve. By grease pencil, I mean just the simple annotation tool.
However, I am not able to get the grease pencil to draw from modal. If I call my draw_curve() function defined below from a button, it works fine. But from modal, the cursor does not become a pencil, and I cannot draw. What am I missing?

(I guess it might have something to do with detecting the left mouse press to actually commence drawing a stroke, but this worked in modal in Blender 2.7x without any further steps, and I’m having trouble figuring out what else 2.8 wants.)

Thanks in advance!

class MYADDON_OT_modal_operator(bpy.types.Operator):
    bl_idname = "myaddon.modal_operator"
    bl_label = "Modal Operator"
    bl_options = {"REGISTER", "UNDO"}

    def invoke(self, context, event):
        bpy.ops.object.mode_set(mode='OBJECT')
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        if event.type == 'D' and event.value == 'PRESS':
            draw_curve()
        elif event.type == 'ESC':
            return {'CANCELLED'}
        return {'RUNNING_MODAL'} 

def draw_curve():
    bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = "SURFACE"
    bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
    bpy.ops.gpencil.annotate()
class MYADDON_OT_modal_operator(bpy.types.Operator):
    bl_idname = "myaddon.modal_operator"
    bl_label = "Modal Operator"
    bl_options = {"REGISTER", "UNDO"}

    def invoke(self, context, event):
        bpy.ops.object.mode_set(mode='OBJECT')
        
        context.window_manager.modal_handler_add(self)
        context.area.header_text_set("My modal")
        context.workspace.status_text_set(text="D: Draw | ESC: Cancel") 
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        if event.type == 'D' and event.value == 'PRESS':
            draw_curve(context)
        elif event.type == 'ESC':
            context.area.header_text_set(text=None)
            context.workspace.status_text_set(text=None)
            return {'CANCELLED'}
        return {'RUNNING_MODAL'}

def draw_curve(context):
    context.scene.tool_settings.annotation_stroke_placement_view3d = "SURFACE"
    bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
    bpy.ops.gpencil.annotate('INVOKE_DEFAULT')
1 Like

Thank you Cirno! Passing ‘INVOKE_DEFAULT’ to gpencil.annotate() is the solution.

Unfortunately, this doesn’t allow the left mouse release to pass through to the modal, in order to automatically be able to run operations on the generated grease pencil stroke. Is there any way to be able to detect this left mouse release, at the end of a single grease pencil stroke?

(In 2.7x I never figured out this followup question out, so my users have to move the mouse after the left mouse release in order to initiate further processing, but this seems clumsy.)

I think you can’t, you need to use a macro operator. Here’s an example, save the code as .py and install as addon. To initialize the modal press ctrl+alt+shift+g. Draw with LMB, annotation stroke should be converted to grease pencil object on the left mouse release

bl_info = {
    "name": "MYADDON modal operator",
    "author": "",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "View3D ",
    "description": "",
    "warning": "",
    "wiki_url": "",
    "category": "3D View",
    }
    
    
import bpy


class MYADDON_OT_macro(bpy.types.Macro):
    bl_idname = "myaddon.macro"
    bl_label = "Macro Sequence"


class MYADDON_OT_finalize(bpy.types.Operator):
    bl_idname = "myaddon.finalize"
    bl_label = "Finalize"

    def execute(self, context):
        bpy.ops.gpencil.convert_old_files()
        context.workspace.status_text_set(text="LEFTMOUSE: Draw | ESC: Cancel") 
        return {'FINISHED'}


class MYADDON_OT_modal_operator(bpy.types.Operator):
    bl_idname = "myaddon.modal_operator"
    bl_label = "Modal Operator"
    bl_options = {"REGISTER", "UNDO"}

    def invoke(self, context, event):
        if context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')
            
        self.bak_tool = context.workspace.tools.from_space_view3d_mode(context.mode, create=False).idname
        bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
        context.scene.tool_settings.annotation_stroke_placement_view3d = "SURFACE"
        
        context.window_manager.modal_handler_add(self)
        context.area.header_text_set("My modal")
        context.workspace.status_text_set(text="LEFTMOUSE: Draw | ESC: Cancel") 
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
            draw_curve(context)

        elif event.type == 'ESC':
            context.area.header_text_set(text=None)
            context.workspace.status_text_set(text=None)
            bpy.ops.wm.tool_set_by_id(name=self.bak_tool)
            return {'CANCELLED'}
            
        return {'PASS_THROUGH'}


def draw_curve(context):
    bpy.ops.myaddon.macro('INVOKE_DEFAULT')
    
    
classes = (
    MYADDON_OT_macro,
    MYADDON_OT_modal_operator,
    MYADDON_OT_finalize
)


addon_keymaps = []


def register_keymaps():    
    kc = bpy.context.window_manager.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name='Object Mode', space_type='EMPTY')

        kmi = km.keymap_items.new("myaddon.modal_operator", 'G', 'PRESS', alt=True, ctrl=True, shift=True)
        addon_keymaps.append((km, kmi))


def unregister_keymaps():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()

    
def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls) 
        
    register_keymaps()
    
    gpencil_macro = MYADDON_OT_macro.define("GPENCIL_OT_annotate")
    gpencil_macro.properties.wait_for_input = False
    MYADDON_OT_macro.define("MYADDON_OT_finalize")


def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

    unregister_keymaps()


if __name__ == "__main__":
    register()