Repository navigation
Expand file tree
/
Copy pathcursor_controller.py
More file actions
196 lines (167 loc) · 8.08 KB
/
Copy pathcursor_controller.py
File metadata and controls
196 lines (167 loc) · 8.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# ============================================================
# NoMouse — cursor_controller.py
# One job: take gesture events and make the OS mouse respond.
# Handles relative trackpad movement with mouse acceleration,
# low-latency direct win32api calls, and One Euro filtering.
# ============================================================
import time
import math
import ctypes
from config import (
POINTER_SENSITIVITY,
POINTER_ACCELERATION_FACTOR,
POINTER_DEADZONE,
POINTER_JITTER_PIXELS,
ONE_EURO_MIN_CUTOFF,
ONE_EURO_BETA,
ONE_EURO_D_CUTOFF,
)
from coordinate_mapper import SCREEN_W, SCREEN_H
from utils.one_euro_filter import OneEuroFilter
class CursorController:
"""
Consumes GestureEvent dicts and translates them into Windows OS inputs
using direct ctypes win32api calls for lowest latency.
Movement is relative (trackpad-style) with a custom acceleration curve.
Coordinate filtering is handled dynamically by a dual One Euro Filter.
"""
# Win32 input event constants
MOUSEEVENTF_LEFTDOWN = 0x0002
MOUSEEVENTF_LEFTUP = 0x0004
MOUSEEVENTF_RIGHTDOWN = 0x0008
MOUSEEVENTF_RIGHTUP = 0x0010
MOUSEEVENTF_MIDDLEDOWN = 0x0020
MOUSEEVENTF_MIDDLEUP = 0x0040
MOUSEEVENTF_WHEEL = 0x0800
def __init__(self):
self._pointer_active = False
self._virtual_x = None # Trackpad virtual position accumulator
self._virtual_y = None
self._prev_norm = None # Previous normalized pointing coordinates
self._last_time = None
self._drag_active = False # Track Win32 LEFTDOWN click-hold state
# Initialize independent X and Y adaptive filters
self._filter_x = OneEuroFilter(
freq=30,
mincutoff=ONE_EURO_MIN_CUTOFF,
beta=ONE_EURO_BETA,
dcutoff=ONE_EURO_D_CUTOFF
)
self._filter_y = OneEuroFilter(
freq=30,
mincutoff=ONE_EURO_MIN_CUTOFF,
beta=ONE_EURO_BETA,
dcutoff=ONE_EURO_D_CUTOFF
)
# ------------------------------------------------------------------
def _get_cursor_pos(self) -> tuple[int, int]:
"""Direct Win32 call to get current mouse cursor coordinates."""
class POINT(ctypes.Structure):
_fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
pt = POINT()
ctypes.windll.user32.GetCursorPos(ctypes.byref(pt))
return pt.x, pt.y
def _set_cursor_pos(self, x: int, y: int):
"""Direct Win32 call to set mouse cursor coordinates."""
ctypes.windll.user32.SetCursorPos(x, y)
# ==================================================================
def process(self, event: dict, screen_pos: tuple[int, int]):
"""
Processes events and executes OS mouse movements / clicks.
"""
now = time.time()
if self._last_time is None:
self._last_time = now
dt = now - self._last_time
if dt <= 0:
dt = 0.001
self._last_time = now
# ------------------------------------------------------------------
# 1. PAUSE — release buttons and freeze relative tracking
# ------------------------------------------------------------------
if event["pause"]:
if self._drag_active:
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
self._drag_active = False
self._pointer_active = False
self._prev_norm = None
return
# ------------------------------------------------------------------
# 2. CURSOR MOVEMENT (Relative trackpad mode + acceleration)
# ------------------------------------------------------------------
if event.get("cursor_freeze"):
# Freeze cursor: reset trackpad reference to prevent drift
self._pointer_active = False
self._prev_norm = None
else:
if event.get("pointer_active"):
norm_x, norm_y = event["cursor"]
# Initialize relative trackpad state on hand entry
if not self._pointer_active:
self._pointer_active = True
curr_x, curr_y = self._get_cursor_pos()
self._virtual_x = curr_x
self._virtual_y = curr_y
self._prev_norm = (norm_x, norm_y)
self._filter_x.t_prev = None # Reset filters
self._filter_y.t_prev = None
# Calculate normalized displacement between frames
dx_norm = norm_x - self._prev_norm[0]
dy_norm = norm_y - self._prev_norm[1]
self._prev_norm = (norm_x, norm_y)
# Compute motion velocity (displacement norm)
dist_norm = math.sqrt(dx_norm**2 + dy_norm**2)
# Apply non-linear acceleration curve
# multipliers map small motions to slow speeds, and fast swipes to high speeds
accel = 1.0 + POINTER_ACCELERATION_FACTOR * dist_norm * 150.0
dx_px = dx_norm * SCREEN_W * POINTER_SENSITIVITY * accel
dy_px = dy_norm * SCREEN_H * POINTER_SENSITIVITY * accel
# Accumulate relative motion in virtual trackpad coordinate space
# Leave a 2-pixel safety margin from screen borders
self._virtual_x = max(2, min(SCREEN_W - 3, self._virtual_x + dx_px))
self._virtual_y = max(2, min(SCREEN_H - 3, self._virtual_y + dy_px))
# Pass accumulated positions through One Euro Filters
filtered_x = self._filter_x(self._virtual_x)
filtered_y = self._filter_y(self._virtual_y)
smooth_x = int(filtered_x)
smooth_y = int(filtered_y)
# Move cursor using Win32 direct call
self._set_cursor_pos(smooth_x, smooth_y)
else:
self._pointer_active = False
self._prev_norm = None
# ------------------------------------------------------------------
# 3. EXECUTE CLICKS & SCROLLS
# ------------------------------------------------------------------
# --- Left Click Tap (Quick click at release) ---
if event.get("left_click_tap"):
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
# --- Left Click Down (or start drag) ---
if event.get("left_click"):
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
self._drag_active = True
# --- Left Click Up (release click / drag) ---
if event.get("left_click_release"):
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
self._drag_active = False
# --- Double Click ---
if event.get("double_click"):
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.05)
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
# --- Right Click Tap (Quick click at release) ---
if event.get("right_click_tap"):
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)
# --- Right Click ---
if event.get("right_click"):
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)
# --- Vertical Scroll ---
scroll_val = event.get("scroll", 0)
if scroll_val != 0:
# Multiplied by 120 (Standard WHEEL_DELTA for 1 scroll tick)
ctypes.windll.user32.mouse_event(self.MOUSEEVENTF_WHEEL, 0, 0, int(scroll_val * 120), 0)