Files
Pierre Kisters 2442527514 VR keyboard: cleanup of the controller bridge, gesture input and docs
- gesture-input.js: drop the unused mode getter and release().
- relay.mjs: both sides have a binding; no condition.
- hub.js: the frame cadence is documented once (bridge-patch.js); errors
  listed.
- docs: the bridge's idle cost and "one word at a time" stated once;
  twoHanded's README row points to the docs; rewrapped lines.
2026-10-01 04:01:00 +02:00

65 lines
2.7 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 (cadence there) 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
// errors { count, last } of throwing subscribers
// 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.
(() => {
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;
})()