2
$\begingroup$

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

https://i.imgur.com/1uujJCq.mp4

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

$\endgroup$
4
  • $\begingroup$ Hi. What exactly do you mean by 'tracking to cursor'? What kind of movement would that be? $\endgroup$ Aug 30, 2019 at 5:30
  • $\begingroup$ Basically have the object orientation follow the mouse, here's an example from another add-on: imgur.com/gallery/cO1GjkT $\endgroup$ Aug 30, 2019 at 8:09
  • $\begingroup$ 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 $\endgroup$ Aug 30, 2019 at 11:42
  • $\begingroup$ 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. $\endgroup$ Aug 30, 2019 at 21:04

1 Answer 1

9
$\begingroup$

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.

enter image description here

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:

$\endgroup$
5
  • $\begingroup$ 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. $\endgroup$ Aug 30, 2019 at 20:59
  • $\begingroup$ @Animationsvfx yes, use the normal returned by the ray cast function to orientate the cube. $\endgroup$
    – lemon
    Aug 31, 2019 at 6:48
  • $\begingroup$ Awesome, I'll test this out when I get home. $\endgroup$ Sep 1, 2019 at 3:20
  • $\begingroup$ 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 $\endgroup$ Sep 1, 2019 at 3:21
  • 1
    $\begingroup$ wow! works like a charm!!! Love it!! $\endgroup$
    – Chris
    Nov 30, 2021 at 12:10

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.