Need help with my modal operator

I’m making a modal that draws & places empties on a surface, but would like to include steps.

I have this hack which uses the mouse path.

self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
play = len(self.mouse_path)

  
index=  play * 0.1 %  20

step = -index % 2

if self.lmb:
    for x in range(0, 10):
        if step == x:
            
            bpy.ops.object.empty_add(type='PLAIN_AXES')

Is there a better way of doing this?

I’m not sure I understand your question… what are “steps”? Are you talking about loop steps (in which case, just use the optional third parameter of the range function)? are you talking about interpolating the mouse position?

Hey, so I’m trying to make a modal where you can click and drag on a surface and it adds an empty. How would I use that range function?

range is a built in function: https://docs.python.org/3.5/library/stdtypes.html#range

If by steps you mean a fixed distance between each empty you’ll need:

  1. Raycast start location
  2. Raycast end location
  3. Interpolated vectors between 1. and 2. using a fixed distance.

Example which projects an empty onto a surface based on the surface distance self.length.
Run and search for generic operator.

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

def raycast(context, event):
    mxy = event.mouse_region_x, event.mouse_region_y
    region = context.region
    rv3d = context.region_data
    if rv3d:
        vl = context.view_layer
        origin = region_2d_to_origin_3d(region, rv3d, mxy)
        direction = region_2d_to_vector_3d(region, rv3d, mxy)
        hit, loc, *_ = context.scene.ray_cast(vl, origin, direction)
        if hit:
            return loc

class WM_OT_generic_operator(bpy.types.Operator):
    bl_idname = "wm.generic_operator"
    bl_label = "Generic Operator"

    def add_empty(self, context, loc):
        ob = bpy.data.objects.new("Empty", None)
        ob.location = loc
        ob.empty_display_size = 0.1
        context.collection.objects.link(ob)
        self.objects.append(ob)

    def modal(self, context, event):
        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
            return {'PASS_THROUGH'}

        if event.type in {'ESC', 'RET'}:
            if event.type == 'ESC':
                for ob in self.objects:
                    bpy.data.objects.remove(ob)
            return {'FINISHED'}

        if self.active and event.type == 'MOUSEMOVE':
            end_loc = raycast(context, event)

            if self.start_loc and end_loc:
                distance = (end_loc - self.start_loc).length

                while distance >= self.length:
                    factor = self.length / distance
                    location = self.start_loc.lerp(end_loc, factor)
                    self.start_loc = location
                    distance -= self.length
                    self.add_empty(context, location)

        if event.type == 'LEFTMOUSE':
            if not self.active:
                self.start_loc = raycast(context, event)
                if self.start_loc:
                    self.add_empty(context, self.start_loc)
            self.active = event.value == 'PRESS'
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.objects = []
        self.start_loc = None
        wm = context.window_manager
        wm.modal_handler_add(self)
        self.active = False
        self.length = 0.5
        return {'RUNNING_MODAL'}
    
if __name__ == "__main__":
    bpy.utils.register_class(WM_OT_generic_operator)
4 Likes

Awesome, I’ll test this out once I get home.

Hey, when adding the empties, can the empty follow the path? Instead of them having the same rotation.