How to move object while tracking to mouse cursor with a modal operator?

I am trying to make a modal operator that can track an object to the mouse location:

I have this example for snapping(volume) an empty within an object, it works great(by Jeacom) & would like the empty to automatically track towards the cursors direction.

Here is the modal I have at the moment:

import bpy
from bpy_extras.view3d_utils import (region_2d_to_origin_3d,
region_2d_to_vector_3d)

def cursor_create(context):
bpy.ops.object.empty_add(type="SPHERE")
context.active_object.show_in_front = True
return context.active_object

def mouse_ray_get(context, event):
co = event.mouse_region_x, event.mouse_region_y
region = context.region
r_data = context.space_data.region_3d

origin = region_2d_to_origin_3d(region, r_data, co)
direction = region_2d_to_vector_3d(region, r_data, co)
return origin, direction

class TestSnap(bpy.types.Operator):
bl_idname = "object.test_snaping"
bl_label = "Test Snaping"
bl_options = {"REGISTER", "UNDO"}

cursor = None

def invoke(self, context, event):
self.cursor = cursor_create(context)
self._timer = context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}

def modal(self, context, event):

origin, direction = mouse_ray_get(context, event)

hit1, location1, normal, index, object, mat =\
context.scene.ray_cast(context.view_layer, origin, direction)

if hit1:

hit2, location2, normal, index, object, mat =\
context.scene.ray_cast(context.view_layer, location1 - 0.00001 * normal, -normal)

if hit2:
self.cursor.location = (location1 + location2) / 2
radius = (location1 - location2).length / 2

self.cursor.scale.x = radius
self.cursor.scale.y = radius
self.cursor.scale.z = radius

if event.type == "ESC":
return {"FINISHED"}

return {"PASS_THROUGH"}

bpy.utils.register_class(TestSnap)


Thanks

• Hi. What exactly do you mean by 'tracking to cursor'? What kind of movement would that be? – Martynas Žiemys Aug 30 '19 at 5:30
• Basically have the object orientation follow the mouse, here's an example from another add-on: imgur.com/gallery/cO1GjkT – Anima tionsvfx Aug 30 '19 at 8:09
• There is a 3d Cursor in Blender and of course mouse cursor as well so it's kind of important to mention which one you are referring to by 'cursor' :) . Your example shows snapping to a surface, so this functionality is already in transform operators. You can enable snapping, set it to surface and then hit g and it's done. You can make a model operator set it up for snapping then actually use transform operator and then set the snapping settings to what they were before at the end of the operation. media.giphy.com/media/fSY5sJsuZRgtBjZgxe/source.gif – Martynas Žiemys Aug 30 '19 at 11:42
• Yea Im talking about the mouse cursor. The reason I want to use custom volume snapping is because I'd like to avoid using transform operator since it interferes with the modal. That way I can just have it run as a handler. I just want the empty that is being snapped to the volume to 'track to'(like the constraints) towards the mouse cursor. Instead of having that empty's rotation just be static. – Anima tionsvfx Aug 30 '19 at 21:04

1 Answer

You can get view origin and mouse location in 3D using:

self.view_point = view3d_utils.region_2d_to_origin_3d(region, region3D, self.mouse_pos)


and

self.world_loc = view3d_utils.region_2d_to_location_3d(region, region3D, self.mouse_pos, self.view_vector)


where self.view_vector is from:

self.view_vector = view3d_utils.region_2d_to_vector_3d(region, region3D, self.mouse_pos)


You can raycast on the scene from self.view_point to self.world_loc.

From that, moving an object is simply giving obj.location a value.

Here the value is either world_loc obtained above or the result of a ray cast on another object. In case of raycasting on another object coordinates are converted in this object space.

Commented script:

import bpy
import bgl
import blf

from bpy_extras import view3d_utils

def draw_callback_px(self, context):
font_id = 0
blf.position(font_id, 15, 100, 0)
blf.size(font_id, 12, 72)
blf.draw(font_id, "Mouse position: " + str(self.mouse_pos) + "/" + str(self.view_point) + "/" + str(self.view_vector) )
blf.position(font_id, 15, 115, 0)
blf.draw(font_id, "Location on plane " + str(self.loc_on_plane)  )
blf.position(font_id, 15, 130, 0)
blf.draw(font_id, "Object location " + str(self.object.location)  )

class ModalDrawOperator(bpy.types.Operator):
"""Draw a line with the mouse"""
bl_idname = "view3d.modal_operator"
bl_label = "Simple Modal View3D Operator"

def modal(self, context, event):
context.area.tag_redraw()

if event.type == 'MOUSEMOVE':

#Get the mouse position thanks to the event
self.mouse_pos = event.mouse_region_x, event.mouse_region_y

#Contextual active object, 2D and 3D regions
self.object = bpy.context.object
region = bpy.context.region
region3D = bpy.context.space_data.region_3d

#The direction indicated by the mouse position from the current view
self.view_vector = view3d_utils.region_2d_to_vector_3d(region, region3D, self.mouse_pos)
#The view point of the user
self.view_point = view3d_utils.region_2d_to_origin_3d(region, region3D, self.mouse_pos)
#The 3D location in this direction
self.world_loc = view3d_utils.region_2d_to_location_3d(region, region3D, self.mouse_pos, self.view_vector)

plane = bpy.data.objects['Plane']
self.loc_on_plane = None
if plane:
world_mat_inv = plane.matrix_world.inverted()
# Calculates the ray direction in the target space
rc_origin = world_mat_inv @ self.view_point
rc_destination = world_mat_inv @ self.world_loc
rc_direction = (rc_destination - rc_origin).normalized()
hit, loc, norm, index = plane.ray_cast( origin = rc_origin, direction = rc_direction )
self.loc_on_plane = loc
if hit:
self.world_loc = plane.matrix_world @ loc

if self.object:
self.object.location = self.world_loc

elif event.type in {'ESC'}:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}

return {'PASS_THROUGH'}

def invoke(self, context, event):
if context.area.type == 'VIEW_3D':
args = (self, context)
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')

#Keeps mouse position current 3D location and current object for the draw callback
#(not needed to make it self attribute if you don't want to use the callback)
self.mouse_pos = [0,0]
self.loc = [0,0,0]
self.object = None
self.view_point = None
self.view_vector = None
self.world_loc = None
self.loc_on_plane = None

context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}

def register():
bpy.utils.register_class(ModalDrawOperator)

def unregister():
bpy.utils.unregister_class(ModalDrawOperator)

if __name__ == "__main__":
register()


Edit:

Little update so that the cube orientation is along the hit normal:

From the norm obtained with ray cast:

norm.rotate( plane.matrix_world.to_euler('XYZ') )
self.normal = norm.normalized()


Then apply it to the object:

self.object.rotation_euler = z.rotation_difference( self.normal ).to_euler()


Updated blend:

• Awesome! Thank you so much! Is it possible to have it self orientate? The cube rotation stays the same. & in the example GIF I posted, they can draw the cube while it rotates along the stroke. For now I am using a track to constraint with a empty as a target. & then I have that empty parented to the cube. – Anima tionsvfx Aug 30 '19 at 20:59
• @Animationsvfx yes, use the normal returned by the ray cast function to orientate the cube. – lemon Aug 31 '19 at 6:48
• Awesome, I'll test this out when I get home. – Anima tionsvfx Sep 1 '19 at 3:20
• Also I was wondering if you're available for freelance development. I've been looking for a developer to improve my tool. Contact me if interested: animationsvfx@gmail.com – Anima tionsvfx Sep 1 '19 at 3:21