gamepad-focus
Drives an SVG UI's focus traversal from a VirtualGamepad — the missing wire between "the D-pad registers" and "the D-pad moves the highlight".
Everything either side of it already existed: box/surface expose
focusMove / focusActivate / focusBack, and HardwareGamepadSource /
XrGamepadSource produce a VirtualGamepad each frame. Nothing polled one and called
the other, so a gamepad could light up the on-screen pad and still not touch the UI.
import { HardwareGamepadSource, ui } from 'tosijs-3d'
const { gamepadFocus } = ui
const pad = new HardwareGamepadSource()
const stop = gamepadFocus({ poll: () => pad.poll(), target: panel, claim: panelRootEl })
// …later
stop()
One pad, several UIs — claim
With more than one gamepad-driven UI live on a page (two doc demos, say), a single
D-pad press would drive them all. Pass claim (the UI's root element) and the pad
follows the pointer: the surface you last pressed in owns the pad until another
claims it. Until anything is claimed every instance responds, so a lone UI needs no
wiring — the same last-touched rule B3d uses for scene input focus.
Which controls, and why those
The D-pad and the menu button, deliberately: per the control conventions those are the inputs a VR controller set doesn't otherwise spend, so a UI can claim them without fighting locomotion or the gameplay bindings. The left stick is not used — it's how you walk.
| control | does |
|---|---|
| D-pad ↑↓←→ | focusMove(dx, dy) |
menu / buttonA |
focusActivate() |
buttonB |
focusBack() |
Edge-triggered, with a repeat
A gamepad reports held, not pressed, so polling raw state would fire every frame and send focus rocketing across the panel. Presses are edge-detected, then repeat on a delay-then-interval ramp (the typematic behaviour every keyboard has) so holding a direction walks steadily instead of either sprinting or stopping dead.
The timing is passed in as now rather than read from a clock, so the ramp is
deterministic and testable — same discipline as the rest of the pure models here.