Files
Frametop--frametop/pointer/dashpatch/treadmill-move.js
T
DeeJanuzandClaude Opus 5.5 6d0c542bb8 pointer: a dashboard patch lets the treadmill move SteamVR's windows
SteamVR's dashboard starts a floating window's move only for a press from
the head or a hand: UndockedOverlay.startFloatingWindowMove reads the
pressing device from the mouse event's CapsLock/NumLock bits and logs "mouse
event carried no input path" for anything else. So the 3D mouse as a
treadmill clicked and resized SteamVR's windows but couldn't move them, the
regression since 2026-10-09.

pointer/dashpatch wraps the grab bar's handler in the dashboard page: a
press with no device runs it as the right hand, with that pose answered by
/user/treadmill, and the move then follows /user/treadmill.
frametop-dashpatch.service puts it in over the dashboard's debugger, again
whenever SteamVR restarts. The installer turns the debugger on at
127.0.0.1:8089 (not 8087, which SteamOS forwards to the network on 8088);
the uninstaller turns it off if Frametop turned it on.

Headset test 2026-10-13: window moves with the treadmill role work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:23:04 -06:00

77 lines
3.8 KiB
JavaScript

// Frametop: SteamVR's floating windows move for the 3D mouse's treadmill device (pointer/dashpatch).
// The dashboard's UndockedOverlay.startFloatingWindowMove reads the pressing device from the mouse
// event's CapsLock/NumLock modifier bits (head, left hand, right hand) and gives up when there are
// none, as for a treadmill. This wraps the grab bar's onMouseDown: a press with no source runs the
// original as the right hand, with that one pose query answered by /user/treadmill, and the move
// then follows /user/treadmill.
(() => {
const VERSION = 1;
if (window.__ftTreadmillMove && window.__ftTreadmillMove.version >= VERSION) return 'already ' + window.__ftTreadmillMove.version;
let req = null;
self.webpackChunkvrwebui.push([[Symbol('frametop-treadmill-move')], {}, r => { req = r; }]);
if (!req) return 'no webpack';
// React: module 6540 in SteamVR 2.18.2's dashboard; else the module that defines React's element
// symbol (loaded already, so requiring it runs nothing new)
const isReact = m => m && typeof m.createElement === 'function' && typeof m.Component === 'function' && /^18\./.test(m.version || '');
let React = req.m['6540'] ? req('6540') : null;
if (!isReact(React)) {
React = null;
for (const id of Object.keys(req.m)) {
const src = req.m[id].toString();
if (src.length < 40000 && src.includes('Symbol.for("react.element")') && src.includes('createElement')) {
const m = req(id);
if (isReact(m)) { React = m; break; }
}
}
}
if (!React) return 'no React';
const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
const TREADMILL = '/user/treadmill', RIGHT = '/user/hand/right';
const state = window.__ftTreadmillMove = {version: VERSION, moves: 0, last: ''};
const wrapped = new WeakMap(); // the original bound handler -> ours
const wrap = (orig, inst) => function (e) {
let src = 0;
try { src = (e.getModifierState('CapsLock') ? 1 : 0) | (e.getModifierState('NumLock') ? 2 : 0); } catch (err) { src = -1; }
if (src !== 0 || !inst) return orig(e);
const [pose] = VRHTML.GetPose(TREADMILL, 0);
if (!pose || !pose.bPoseIsValid) { state.last = 'treadmill pose invalid'; return orig(e); }
const fake = new Proxy(e, {get(t, k) {
if (k === 'getModifierState') return m => m === 'CapsLock' || m === 'NumLock';
const v = t[k];
return typeof v === 'function' ? v.bind(t) : v;
}});
const getPose = VRHTML.GetPose;
let first = true; // only the moving device's query (the first one); a hand-docked window's own stays
VRHTML.GetPose = function (path, origin) {
if (first && path === RIGHT) { first = false; path = TREADMILL; }
return getPose.call(this, path, origin);
};
let r;
try { r = orig(fake); } finally { VRHTML.GetPose = getPose; }
if (inst.m_sMoveDevicePath === RIGHT) {
inst.m_sMoveDevicePath = TREADMILL;
inst.setState({sParentDevice: TREADMILL});
state.moves++;
state.last = 'moved by the treadmill';
}
return r;
};
const createElement = React.createElement;
React.createElement = function (type, props) {
if (props && typeof props.onMouseDown === 'function' && props.explicitSGID !== undefined &&
props.onMouseDown.name === 'bound startFloatingWindowMove') {
const owner = internals && internals.ReactCurrentOwner && internals.ReactCurrentOwner.current;
const inst = owner && owner.stateNode;
let ours = wrapped.get(props.onMouseDown);
if (!ours) { ours = wrap(props.onMouseDown, inst); wrapped.set(props.onMouseDown, ours); }
const args = Array.prototype.slice.call(arguments);
args[1] = Object.assign({}, props, {onMouseDown: ours});
return createElement.apply(this, args);
}
return createElement.apply(this, arguments);
};
return 'installed ' + VERSION;
})()