Can a modal operator use snapping?

Is there a way to have a modal operator use volume snapping when using a cube to follow the mouse location?

I have a modal where a small cube’s location is equal to the mouse location, & when I enable snapping(volume or faces) it doesnt work. Does snapping only work with the translate operator?

Thanks

I think snapping is a feature built per operator, so you would have to implement it yourself for your custom operator.

But if you know how to convert region2d to 3dspace, (I assume that’s how you did the cube to mouse thing), it should be simple enough to build a custom volume snapping.

you just need to:

  • ray_cast on the scene based on mouse position
    • if hit, store the hit location and do a second ray_cast starting from previous hit location:
      • if second hit: average first and second hits and use it for your snapping
      • if not second hit: use only the first hit for snapping (surface)
1 Like

I found this, I hope I can modify it a bit so I can successfully get a cube to snap(volume) within a mesh.

Thanks for your suggestion, I will try to follow your steps, hopefully I get something working.

I made a small example so you can get a sense of how it can be done:

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)
2 Likes

No way! You are a programming wizard… This should work, will direction detection be possible as well? Also, Now I can use bgl to draw the circle & match the empties location.

I really appreciate it! I have been looking for a developer for hire as well. I know you are busy with your projects, but go ahead and send me a PM if you are interested!

I think direction detection is a bit harder, as it have to deal with rotations and local curvature but should be possible.

Is there any way I can reduce the shaking when moving the empty around?

You can try interpolate the normals of the triangle instead of using the flat normal, it will give a smoother movement.

Is there any option I need to change to do that? I get a bit confused when working with raycast stuff
Also, If you have time, could you help me out with the direction detection? At the moment I am using the track to constraint which can work pretty good.

changed these two lines to get a snoother movement:

                self.cursor.location += (location1 + location2) / 2
                self.cursor.location /= 2

full code:

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.location += (location1 + location2) / 2
                self.cursor.location /= 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)
1 Like

Works perfect!

Do you know if this can also be done, but a certain value above the surface? like face snapping, but offset a bit from the mesh surface.

for this you can use the hit normals to offset the coordinates.

I have a question about the raycast, can this be done for a specific object, instead of context.scene can I use bpy.data.object[‘Plane’].raycast( origin, direction)?

Thanks

Yes, its possible.
https://docs.blender.org/api/current/bpy.types.Object.html?highlight=object%20ray_cast#bpy.types.Object.ray_cast
If I remenber correctly the coordinates have to be in local space

Is that the return value from mouse_ray_get?

No, you have to multiply the vectors by the inverse object matrix.

I don’t have access to my laptop right now, but I’m assuming this is how you multiply the vector by the objects matrix inversed…

origin, direction = mouse_ray_get(context, event)

obj = bpy.data.objects['Torus']

obj_inv = obj.matrix_world.inversed()

obj_dir = direction @ obj_inv

hit1, location1, normal, *_ =\
            obj.ray_cast(origin, obj_dir)

yes, pretty much