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$
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$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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