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
Of course it had to be called “warp” or something obscure like that. Works perfectly. Thank you!
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:
- It’s the first
MOUSEMOVE
event after I calledcursor_warp
. (Could overengineer this using Python’stime.perf_counter
) - 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)
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.