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$ Commented 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$ Commented 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$ Commented 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$ Commented Aug 30, 2019 at 21:04

1 Answer 1

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

You must log in to answer this question.

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