2
$\begingroup$

I’m prototyping an addon that will have several custom Tools and, when those tools are selected, a bunch of custom Gizmos in 3D views for manipulating various aspects of objects created by the addon. But I can’t figure out how to get a custom Tool and a custom Gizmo and a custom Operator to all work together.

I’ve been looking at all the relevant samples in release/scripts/templates_py and addons/mesh_snap_utilities_line, and also the source code for the builtin gizmos and tools.

At the moment I’m trying to mimic the behaviour of the builtin Randomize Vertices tool and the builtin value_2d gizmo that it uses, where the gizmo is modal but the operator is not, so that the gizmo calls the operator repeatedly as it is dragged.

So: I select my tool, and my gizmo appears. Then I click and drag on the gizmo.

Problem: my operator gets invoked when I first click on the gizmo, but as soon as the drag begins, both my gizmo and operator seem to be left out of things as the builtin translate happens.

What I would like to happen is that the operator gets invoked repeatedly as the gizmo is dragged. How do I make this happen?

(Additionally, on 2.80 there’s a bug that makes the gizmo be activated and deactivated repeatedly, causing it to flicker and be unusable. I think this is https://developer.blender.org/T60289 -- but at any rate this issue is fixed on the latest 2.81 beta)

Here’s the code I have right now. What am I doing wrong? How should I go about getting a custom Tool, custom Gizmos, and custom Operators to work together like this?

import bpy
import mathutils

class TEST_OT_test_op(bpy.types.Operator):
    bl_idname = "test.test_op"
    bl_label = "Test"
    bl_description = "A test operator"
    bl_options = {'REGISTER'}

    mouse_x : bpy.props.IntProperty()
    mouse_y : bpy.props.IntProperty()

    @classmethod
    def poll(cls, context):
        return True

    def invoke(self, context, event):
        self.mouse_x = event.mouse_x
        self.mouse_y = event.mouse_y
        return self.execute(context)

    def execute(self, context):
        print(f"Test operator: {self.mouse_x}, {self.mouse_y}")
        return {'FINISHED'}


class TEST_GT_test_gizmo(bpy.types.Gizmo):
    bl_target_properties = (
        {"id": "mouse_x", "type": 'INT'},
        {"id": "mouse_y", "type": 'INT'},
    )

    def draw(self, context):
        matrix = mathutils.Matrix.Translation((0.5, 0.5, 0.5))
        self.draw_preset_box(matrix, select_id=0)

    def draw_select(self, context, select_id=0):
        matrix = mathutils.Matrix.Translation((0.5, 0.5, 0.5))
        self.draw_preset_box(matrix, select_id=select_id)

    def setup(self):
        print(f"{self}: setup")

    def modal(self, context, event, tweak):
        self.target_set_value("mouse_x", event.mouse_x)
        self.target_set_value("mouse_y", event.mouse_y)
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        print(f"{self}: invoke(event={event})")
        return {'RUNNING_MODAL'}

    def exit(self, context, cancel):
        print(f"{self}: exit(cancel={cancel})")


class TEST_GGT_test_group(bpy.types.GizmoGroup):
    bl_label = "Test Widget"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'WINDOW'
    bl_options = {'3D'} # 'TOOL_INIT' also sounds appropriate, but then the gizmo doesn't appear!
    bl_operator = "test.test_op" # Just for the tooltip

    @classmethod
    def poll(cls, context):
        o = context.object
        return (o and o.select_get() and o.type == 'MESH')

    def setup(self, context):
        print(f"{self}: setup")
        o = context.object
        giz = self.gizmos.new("TEST_GT_test_gizmo")
        giz.use_draw_modal = True # Is this necessary? (Makes no difference I can see)
        giz.target_set_operator("test.test_op", index=0)
        giz.matrix_basis = o.matrix_world.normalized()
        giz.color = 1.0, 1.0, 1.0
        giz.alpha = 0.5
        giz.color_highlight = 1.0, 0.0, 0.0
        giz.alpha_highlight = 1.0
        self.gizmo = giz

    def refresh(self, context):
        print(f"{self}: refresh")
        o = context.object
        giz = self.gizmo
        giz.matrix_basis = o.matrix_world.normalized()


class TestTool(bpy.types.WorkSpaceTool):
    bl_idname = "TEST.test_tool"
    bl_space_type='VIEW_3D'
    bl_context_mode='OBJECT'
    bl_label = "Test"
    bl_icon = "ops.armature.bone.roll" # Placeholder
    bl_widget = "TEST_GGT_test_group"

classes = (
    TEST_OT_test_op,
    TEST_GT_test_gizmo,
    TEST_GGT_test_group,
    )

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    # Not a good place to add it but okay after={"builtin.ruler"},
    bpy.utils.register_tool(TestTool, separator=True)

def unregister():
    bpy.utils.unregister_tool(TestTool)
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
```
$\endgroup$

1 Answer 1

0
$\begingroup$

Didn’t find an answer to this.

And realised while continuing to research it that what I was asking wasn't actually what I needed: I needed my operators to be modal (with complex behaviour). But digging deeper into the current (2.81) behaviour of Tools/Gizmos/Operators, I couldn't find a way to make them work together nicely:

when a gizmo invokes a modal operator with target_set_operator(), then the operator ends up having to do redundant work of input interpretation and drawing, rending the gizmo nearly pointless. But if the gizmo sets properties with target_set_prop(), then undo is broken after the gizmo is used!

$\endgroup$

You must log in to answer this question.

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