mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
keyboard.vr.touchTyping (off by default): a key is pressed when a controller's tip, where SteamVR's laser starts (/pose/tip: the device pose times the render model's "tip" component), passes through it from the front; released when pulled back 1 cm, re-armed 5 mm in front. Both hands, also at once. Keys go through the keyboard's own HandleTouchStart / HandleTouchEnd, like a laser press (Backspace repeats while held). Options depth (cm) and haptics. The geometry lives in a reusable controller bridge (vr-keyboard-controllers, internal option): a systemui patch reads the keyboard's pose with SteamVR's SGQueryService (an empty transform of ours in the keyboard's mount) and both controllers' poses, and streams per hand the tip, the laser's hit on the keyboard and the trigger (~90 Hz while relevant) through the vr-keyboard-controllers-relay service to __sfuiControllers in Steam's SharedJSContext. Tests: flake checks vr-keyboard-controllers and vr-keyboard-touch.
60 lines
2.4 KiB
JavaScript
60 lines
2.4 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)
|
|
// 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 every ~11 ms while a hand is near the
|
|
// keyboard, its laser on it or its trigger pulled, else every 250 ms, and
|
|
// only while SteamVR shows the keyboard (then one with keyboard null).
|
|
(() => {
|
|
const VERSION = 1;
|
|
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),
|
|
subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
|
|
};
|
|
G.__sfuiControllers = hub;
|
|
return hub;
|
|
})()
|