mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 05:00:24 +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
3.1 KiB
JavaScript
60 lines
3.1 KiB
JavaScript
// geometry.js: controllers relative to Steam's VR keyboard
|
|
// (vr-keyboard-controllers.nix). Pure functions, used by bridge-patch.js
|
|
// (SteamVR systemui) and tests; consumers may take it as an extraArg too.
|
|
// Evaluates to { VERSION, rotate, multiply, tipPose, toKeyboard, rayHit,
|
|
// rotationDegrees }.
|
|
//
|
|
// Poses are SteamVR transforms { translation: {x,y,z}, rotation: {w,x,y,z} }.
|
|
// - tipPose(device, tip): the controller's tip, where SteamVR's laser starts
|
|
// (the laser mouse's pose is /pose/tip): the device pose
|
|
// (xfDeviceToAbsoluteTracking) times the render model's "tip" component
|
|
// (xfTrackingToComponentLocal), or the device pose without one.
|
|
// - Keyboard kb = { translation, rotation, width }: its top edge centre in
|
|
// tracking space, +z toward the viewer, width in m (bridge-patch.js).
|
|
// Keyboard coordinates: u 0..1 left to right, v down from the top edge in
|
|
// keyboard widths (page px = u * page width, v * page width).
|
|
// - toKeyboard(p, kb): a point -> { u, v, d } (d: m behind the surface).
|
|
// - rayHit(pose, kb): the laser (the pose's -z axis) on the keyboard's
|
|
// plane, from the front -> { u, v, dist (m) } or null.
|
|
(() => {
|
|
const VERSION = 1;
|
|
const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y, z: a.z - b.z });
|
|
const add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y, z: a.z + b.z });
|
|
// v rotated by the unit quaternion q.
|
|
function rotate(q, v) {
|
|
const tx = 2 * (q.y * v.z - q.z * v.y), ty = 2 * (q.z * v.x - q.x * v.z), tz = 2 * (q.x * v.y - q.y * v.x);
|
|
return {
|
|
x: v.x + q.w * tx + q.y * tz - q.z * ty,
|
|
y: v.y + q.w * ty + q.z * tx - q.x * tz,
|
|
z: v.z + q.w * tz + q.x * ty - q.y * tx,
|
|
};
|
|
}
|
|
const qmul = (a, b) => ({
|
|
w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
|
|
x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
|
|
y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
|
|
z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
|
|
});
|
|
const conj = (q) => ({ w: q.w, x: -q.x, y: -q.y, z: -q.z });
|
|
// a * b (b in a's frame).
|
|
const multiply = (a, b) => ({ translation: add(a.translation, rotate(a.rotation, b.translation)), rotation: qmul(a.rotation, b.rotation) });
|
|
const tipPose = (device, tip) => (tip ? multiply(device, tip) : { translation: { ...device.translation }, rotation: { ...device.rotation } });
|
|
const local = (p, kb) => rotate(conj(kb.rotation), sub(p, kb.translation));
|
|
|
|
function toKeyboard(p, kb) {
|
|
const l = local(p, kb);
|
|
return { u: l.x / kb.width + 0.5, v: -l.y / kb.width, d: -l.z };
|
|
}
|
|
function rayHit(pose, kb) {
|
|
const o = local(pose.translation, kb), dir = rotate(conj(kb.rotation), rotate(pose.rotation, { x: 0, y: 0, z: -1 }));
|
|
if (o.z <= 0 || dir.z >= -1e-6) return null; // behind the plane or not pointing at its front
|
|
const t = -o.z / dir.z;
|
|
return { u: (o.x + t * dir.x) / kb.width + 0.5, v: -(o.y + t * dir.y) / kb.width, dist: t };
|
|
}
|
|
// Rotation angle of a quaternion (degrees), e.g. of an animated render
|
|
// model component (the trigger).
|
|
const rotationDegrees = (q) => (2 * Math.acos(Math.min(1, Math.abs(q.w))) * 180) / Math.PI;
|
|
|
|
return { VERSION, rotate, multiply, tipPose, toKeyboard, rayHit, rotationDegrees };
|
|
})()
|