Files
lhns--steam-frame-nix/modules/vr-keyboard-controllers/hub.js
T
Pierre Kisters af32cf72ce VR keyboard swipe with two controllers: the path from the controller bridge
With both lasers on the keyboard SteamVR forwards per poll only one laser's
movement, so the pressing one may get no touchmoves and its swipe became a
tap. keyboard.vr.swipe.twoHanded (default on; turns on the controller
bridge): a press is attributed to the hand whose laser hit is nearest to it
(within 30 px, the trigger as a tiebreak; without recent frames the first
frame after the press decides, within 60 px). The path comes from runs of
one source: the touch's own moves while they flow, that hand's laser hit
per bridge frame once they pause for 60 ms, touchmoves again if frames
pause. A touchend disagreeing with the laser during a bridge run ends at
the laser. Touchmoves on the other hand's laser move the attribution.
Without fresh frames: touch events only, as before. A second press during
a swipe stays Steam's tap (one text field, one word at a time).

The bridge sends full rate only on demand now: hub.js demand(ms) goes
back through the relay (CDP binding __sfuiCtlIn) to systemui's
__sfuiCtl.demand; the swipe patch asks from the press to the release.

Tests: gesture-input with the two-controller recording and synthetic bridge
frames (a pressing hand without touchmoves, attribution with both lasers on
the keyboard, a stale bridge, frames stopping mid-swipe, re-attribution,
interleaved hover and presses); hub demand.
2026-10-01 03:45:47 +02:00

66 lines
2.8 KiB
JavaScript

// hub.js: the controller frames in Steam's SharedJSContext
// (vr-keyboard-controllers.nix). A single expression for consumer patches'
// extraArgs (like lib/hooks.js): installs window.__sfuiControllers unless an
// equal or newer VERSION is there (a newer one takes over the subscribers)
// and evaluates to it. relay.mjs calls frame(f) with bridge-patch.js's
// frames and lost() when the SteamVR side goes away.
// subscribe(fn) -> unsubscribe fn(frame) per frame, fn(null) when lost
// last the last frame (null: none / lost)
// demand(ms) full rate for the next ms (0: end), e.g.
// during a laser press; via the relay's
// CDP binding __sfuiCtlIn
// Each frame's hand points (tip, ray) get the keyboard page's px: x, y
// (CSS px of the keyboard popup) and onKeyboard (inside the page), if the
// keyboard popup exists. Frames come at up to ~90 Hz while a hand's tip is
// within 10 cm of the keyboard or its trigger is pulled, else every 1 s, and
// only while SteamVR shows the keyboard (then one with keyboard null).
(() => {
const VERSION = 2;
const G = globalThis;
const have = G.__sfuiControllers;
if (have && have.version >= VERSION) return have;
const subs = have?.subs ?? new Set();
const errors = have?.errors ?? { count: 0, last: null };
let win = null, winAt = -Infinity;
function keyboardWindow(now) {
if (now - winAt < 1000 && win && !win.closed) return win;
winAt = now;
win = [...(G.g_PopupManager?.GetPopups?.() || [])].find((p) => p.window?.document.querySelector('[data-key]'))?.window ?? null;
return win;
}
const px = (p, w, h) => {
if (!p) return p;
const x = p.u * w, y = p.v * w;
return { ...p, x, y, onKeyboard: x >= 0 && y >= 0 && x < w && y < h };
};
function deliver(f) {
hub.last = f;
for (const fn of [...subs]) {
try { fn(f); } catch (e) { errors.count++; errors.last = String(e?.stack ?? e); }
}
}
function frame(f) {
if (!f || typeof f !== 'object') return;
const now = performance.now(), w = f.keyboard ? keyboardWindow(now) : null;
if (w?.innerWidth) {
for (const h of ['left', 'right']) {
const x = f.hands?.[h];
if (x) f.hands[h] = { ...x, tip: px(x.tip, w.innerWidth, w.innerHeight), ray: px(x.ray, w.innerWidth, w.innerHeight) };
}
}
f.receivedAt = now;
deliver(f);
}
const hub = {
version: VERSION, subs, errors, last: null,
frame, lost: () => deliver(null),
demand(ms) {
try { G.__sfuiCtlIn?.(JSON.stringify({ fast: Math.max(0, Math.min(5000, +ms || 0)) })); } catch (e) { errors.count++; errors.last = String(e); }
},
subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
};
G.__sfuiControllers = hub;
return hub;
})()