fix(humanize): preserve gaussian magnitude in jitter_point; unify swipe return shape
This commit is contained in:
@@ -27,6 +27,28 @@ def test_jitter_point_centered_on_input():
|
|||||||
assert abs(sum(xs) / len(xs)) < 0.3
|
assert abs(sum(xs) / len(xs)) < 0.3
|
||||||
|
|
||||||
|
|
||||||
|
def test_jitter_point_concentrated_near_target():
|
||||||
|
"""Regression: jitter must be polar-Gaussian (concentrated near target),
|
||||||
|
not uniform-on-circle (all points at exactly ``radius``).
|
||||||
|
|
||||||
|
For a folded Gaussian with sigma=radius/2, the expected mean distance is
|
||||||
|
~0.8*sigma ~= 0.4*radius. Using ``< 0.7*radius`` gives a safe margin that
|
||||||
|
fails the previous reprojection bug (mean distance == radius exactly).
|
||||||
|
"""
|
||||||
|
rng = random.Random(1234)
|
||||||
|
radius = 5.0
|
||||||
|
n = 5000
|
||||||
|
distances = [
|
||||||
|
math.hypot(x - 0.0, y - 0.0)
|
||||||
|
for x, y in (jitter_point(0.0, 0.0, radius=radius, rng=rng) for _ in range(n))
|
||||||
|
]
|
||||||
|
mean_distance = sum(distances) / n
|
||||||
|
assert mean_distance < radius * 0.7, (
|
||||||
|
f"mean distance {mean_distance:.3f} is too large; jitter looks like "
|
||||||
|
"uniform-on-circle rather than polar-Gaussian"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_jitter_duration_within_spread():
|
def test_jitter_duration_within_spread():
|
||||||
rng = random.Random(3)
|
rng = random.Random(3)
|
||||||
for _ in range(200):
|
for _ in range(200):
|
||||||
|
|||||||
+18
-10
@@ -90,23 +90,31 @@ def set_rng(rng: random.Random | None) -> None:
|
|||||||
def jitter_point(
|
def jitter_point(
|
||||||
x: float, y: float, *, radius: float, rng: random.Random
|
x: float, y: float, *, radius: float, rng: random.Random
|
||||||
) -> tuple[float, float]:
|
) -> tuple[float, float]:
|
||||||
"""Gaussian offset clamped to a ``radius``-px circle around the target."""
|
"""Polar-Gaussian offset around the target.
|
||||||
|
|
||||||
|
Magnitude is drawn from a folded Gaussian with sigma=radius/2, then
|
||||||
|
capped at ``radius``. The angle is uniform on [0, 2*pi). This concentrates
|
||||||
|
jittered points near the target rather than uniformly on the circle edge,
|
||||||
|
matching how humans tap close to (but not exactly on) a button center.
|
||||||
|
"""
|
||||||
r = abs(rng.gauss(0.0, radius / 2.0))
|
r = abs(rng.gauss(0.0, radius / 2.0))
|
||||||
r = min(r, radius)
|
r = min(r, radius)
|
||||||
angle = rng.uniform(0, 2 * math.pi)
|
angle = rng.uniform(0, 2 * math.pi)
|
||||||
dx = r * math.cos(angle)
|
dx = r * math.cos(angle)
|
||||||
dy = r * math.sin(angle)
|
dy = r * math.sin(angle)
|
||||||
# Reproject to exact radius to absorb FP drift in cos/sin
|
# Guard against cos/sin FP drift pushing distance slightly past radius.
|
||||||
d = math.sqrt(dx * dx + dy * dy)
|
fd = math.hypot(dx, dy)
|
||||||
if d > 0:
|
if fd > radius:
|
||||||
dx = dx / d * radius
|
scale = radius / fd
|
||||||
dy = dy / d * radius
|
dx *= scale
|
||||||
|
dy *= scale
|
||||||
|
# Final clamp: hypot of the returned offset may overshoot by ulps; tighten
|
||||||
|
# to ``radius - 1e-10`` to keep ``<= radius`` after subtraction/hypot.
|
||||||
fx = x + dx
|
fx = x + dx
|
||||||
fy = y + dy
|
fy = y + dy
|
||||||
fd = math.hypot(fx - x, fy - y)
|
fd_final = math.hypot(fx - x, fy - y)
|
||||||
if fd > radius:
|
if fd_final > radius:
|
||||||
# Scale to a slightly tighter radius to absorb FP rounding in addition
|
scale = (radius - 1e-10) / fd_final
|
||||||
scale = (radius - 1e-10) / fd
|
|
||||||
fx = x + dx * scale
|
fx = x + dx * scale
|
||||||
fy = y + dy * scale
|
fy = y + dy * scale
|
||||||
return (fx, fy)
|
return (fx, fy)
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ def swipe(
|
|||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"action": "swipe",
|
"action": "swipe",
|
||||||
|
"start": {"x": start_x, "y": start_y},
|
||||||
|
"end": {"x": end_x, "y": end_y},
|
||||||
"waypoints": waypoints,
|
"waypoints": waypoints,
|
||||||
"duration_ms": dms,
|
"duration_ms": dms,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user