Center mouse position on 3d view

I want to set the position of the mouse cursor to the center of the 3D view from within my script. Given my unsuccessful search on a function provided by Blender, I at least was able to get and set the position with Pynput. However, I am missing the transformation from the Blender Event type’s mouse_x and mouse_y fields to the screen space coordinates expected by Pynput’s setter. I want to avoid involving WinAPI’s ClientToScreen() as well as all the other function calls needed to feed it its dependencies, which would make this very much platform dependent. If I could get width/height of the 3d view, as well as the screen space coordinates of any known point within it, I could do the math. I tried accessing context.screen.areas[0] to see if it would contain anything useful, but the x, y, width and height values in there don’t seem to make a lot of sense. Anyone has a good idea?

In region space:

region = context.region
cx = region.width // 2
cy = region.height // 2

In window space:

region = context.region
cx = region.width // 2 + region.x
cy = region.height // 2 + region.y

For moving the cursor in blender, you can use context.window.cursor_warp(x, y).
It takes window coordinates.

You can get the current mouse region coordinates from the event system using

event.mouse_region_x
event.mouse_region_y
1 Like

Of course it had to be called “warp” or something obscure like that. Works perfectly. Thank you!

1 Like

So, this isn’t actually 100% fool-proof. Calling cursor_warp() will cause MOUSEMOVE events to feed back into modal(), in an unpredictable order with an unpredictable delay. Blender probably blindly throws this at the API of the operating system. I found no definitive solution for this. A lot of hacky code can be written though to eliminate the issue quite reliably. I ignore MOUSEMOVE events with any of these characteristics:

  1. It’s the first MOUSEMOVE event after I called cursor_warp. (Could overengineer this using Python’s time.perf_counter)
  2. The moved distance is unusually large, e.g. >400 pixels. This is an annoying to manage behavior because lower refresh rates will want to have a larger threshold and that’s even more code to fix something which should have been a one-liner.

If someone has better ideas, I’d be glad to hear about them.

Otherwise, I’ll probably switch the workflow of my addon around a bit, because dealing with this issue stinks.

Just spitballing here, but I feel like this could be solved like a normal python problem rather than a blender API problem, ive messed with python modules that dealt with os-level events such as cursor position and keyboard input. Perhaps it’s a vector of research that could be useful to you

Guessing isn’t going to be useful. If you provided some code for context we might have some ideas.

The things I’m currently interested in is: what are you using MOUSEMOVE for? When in the code are you calling cursor_warp()? And depending on the answers, there are ways to mitigate the issue you’re having.

I’m doing nothing too outrageous really. I simply call cursor_warp() at some times during modal(), whereafter I return {"RUNNING_MODAL"}. Then after that I get the mouse movement caused by that as a MOUSEMOVE event, incorrectly interpreting the resulting delta as user input.

    def modal(self, context, event):
        if self.shouldRecenterMouseForWhateverReason():
            context.window.cursor_warp(context.region.x + context.region.width // 2, context.region.y + context.region.height // 2)
            return {"RUNNING_MODAL"}
        if event.type == "MOUSEMOVE":
            xDelta = event.mouse_x - event.mouse_prev_x
            yDelta = event.mouse_y - event.mouse_prev_y
            deltaDistance = math.sqrt(xDelta*xDelta + yDelta*yDelta)
            if deltaDistance > 500:
                print("Whoa!")
            else:
                # Do something with the mouse delta here.
            return {"RUNNING_MODAL"}
        if event.type == " ESCAPE" and event.value == "PRESS":
            return {"CANCELLED"}
        if event.type == "ENTER" and event.value == "PRESS":
            return {"FINISHED"}
        return {"RUNNING_MODAL"}

Also it might be worth noting that the Operator has "GRAB_CURSOR" and "BLOCKING" in its bl_options.

That’s the accumulative nature of GRAB_CURSOR since it allows the cursor to wrap around the region. You can write a method that ensures wrapping but leave out the accumulation (remove GRAB_CURSOR from bl_options).

    def grab_cursor(self, context, event):
        region = context.region
        mrx = event.mouse_region_x
        mry = event.mouse_region_y
        if mrx < 0:
            args = region.x + region.width, event.mouse_y
        elif mrx > region.width:
            args = region.x, event.mouse_y
        elif mry < 0:
            args = event.mouse_x, region.y + region.height
        elif mry > region.height:
            args = event.mouse_x, region.y
        else:
            return
        context.window.cursor_warp(*args)

Then add this at the top of the modal

self.grab_cursor(context, event)
1 Like

Well, this was much easier than I thought. You can literally cursor_warp() on every MOUSEMOVE and run into no problems after removing DISABLE_GRAB. Thanks!

While this is the best behavior so far, there are still ways to mess this up. While even with GRAB_CURSOR one can see the cursor briefly leave the window when moving the mouse quickly, one cannot seem to make Blender lose focus by clicking on another program’s window. Without GRAB_CURSOR this can obviously happen, so I counter the issue a little by sbutracting a portion of the mouse delta from the position I reset the cursor to.

    def resetMouse(self, context, event):
        context.window.cursor_warp(context.region.x + context.region.width // 2 - 0.5*(event.mouse_x - event.mouse_prev_x), \
            context.region.y + context.region.height // 2 - 0.5*(event.mouse_y - event.mouse_prev_y))

Some premium yak shaving right there.