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()
```