refactor(scroll): move trainer/models/collector to tools/scroll/
This commit is contained in:
99
tools/scroll/collector.py
Normal file
99
tools/scroll/collector.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Scroll collection state and target generation.
|
||||
|
||||
The actual scroll state machine runs in JavaScript (wheel events are
|
||||
client-side). This module handles server-side target generation and
|
||||
trace persistence.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
|
||||
from tools.config import SCROLL_MODES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScrollCollector:
|
||||
"""Manages scroll collection sessions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mode: str,
|
||||
count: int,
|
||||
page_height: int = 10000,
|
||||
viewport_height: int = 900,
|
||||
):
|
||||
if mode not in SCROLL_MODES:
|
||||
raise ValueError(f"Unknown mode: {mode}. Use: {', '.join(SCROLL_MODES)}")
|
||||
self.mode = mode
|
||||
self.count = count
|
||||
self.page_height = page_height
|
||||
self.viewport_height = viewport_height
|
||||
self.collected = 0
|
||||
|
||||
cfg = SCROLL_MODES[mode]
|
||||
self.dist_min = cfg.dist_min
|
||||
self.dist_max = cfg.dist_max
|
||||
self.success_radius = cfg.success_radius
|
||||
|
||||
def next_target(self, current_scrollY: int) -> dict:
|
||||
"""Generate next target scroll position.
|
||||
|
||||
The target must be reachable — i.e. the user must be able to scroll
|
||||
such that the target band enters the viewport's success zone (centered).
|
||||
|
||||
Reachability constraint:
|
||||
To hit a target at T, the user needs scrollTop ≈ T - viewportCenter + bandHeight/2.
|
||||
scrollTop must be in [0, pageHeight - viewportHeight].
|
||||
|
||||
So valid target range is:
|
||||
T_min = viewportCenter - successRadius (scrollTop=0 → target at top of success zone)
|
||||
T_max = maxScrollTop + viewportCenter + successRadius
|
||||
|
||||
Returns:
|
||||
{"target_scrollY": int, "direction": "up"|"down"}
|
||||
"""
|
||||
viewport_center = self.viewport_height // 2
|
||||
max_scroll_top = self.page_height - self.viewport_height
|
||||
|
||||
# Valid range for targetScrollY so it's always reachable
|
||||
target_min = viewport_center - self.success_radius
|
||||
target_max = max_scroll_top + viewport_center + self.success_radius
|
||||
|
||||
# Clamp to ensure sane bounds
|
||||
target_min = max(0, target_min)
|
||||
target_max = min(self.page_height, target_max)
|
||||
|
||||
max_down = min(self.page_height - current_scrollY, target_max - current_scrollY)
|
||||
max_up = min(current_scrollY, current_scrollY - target_min)
|
||||
|
||||
for _ in range(100):
|
||||
dist = random.randint(self.dist_min, self.dist_max)
|
||||
direction = random.choice(["up", "down"])
|
||||
|
||||
if direction == "down" and 0 < dist <= max_down:
|
||||
candidate = current_scrollY + dist
|
||||
if target_min <= candidate <= target_max:
|
||||
return {"target_scrollY": candidate, "direction": "down"}
|
||||
elif direction == "up" and 0 < dist <= max_up:
|
||||
candidate = current_scrollY - dist
|
||||
if target_min <= candidate <= target_max:
|
||||
return {"target_scrollY": candidate, "direction": "up"}
|
||||
|
||||
# Fallback: generate a valid target within bounds
|
||||
valid_down = min(max_down, self.dist_max)
|
||||
valid_up = min(max_up, self.dist_max)
|
||||
|
||||
if valid_down >= self.dist_min:
|
||||
dist = random.randint(self.dist_min, max(self.dist_min, valid_down))
|
||||
return {"target_scrollY": current_scrollY + dist, "direction": "down"}
|
||||
elif valid_up >= self.dist_min:
|
||||
dist = random.randint(self.dist_min, max(self.dist_min, valid_up))
|
||||
return {"target_scrollY": current_scrollY - dist, "direction": "up"}
|
||||
else:
|
||||
# Edge case: move to center of valid range
|
||||
target = max(target_min, min(target_max, (target_min + target_max) // 2))
|
||||
direction = "down" if target > current_scrollY else "up"
|
||||
return {"target_scrollY": target, "direction": direction}
|
||||
|
||||
Reference in New Issue
Block a user