mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
VR keyboard swipe: only the pressing controller's laser builds the path
With both controllers on the keyboard, the idle one's hover (mouse events,
pointerId 1) arrived during the other's press and went into the swipe path
("tust" for "test"). A gesture now belongs to the contact that started it
(touch identifier, pointerId or mouse; gesture-input.js): other contacts'
moves, releases and cancels are ignored, a second press meanwhile stays
Steam's tap. Test with a recorded two-controller swipe.
This commit is contained in:
1 parent
26641d76a0
commit
7d050b8c44
8 files changed
+376
-40
No files matched your search
+2
-1
@@ -104,12 +104,13 @@ modules/
|
||||
swipe-decoder.js Steam (argument of patch.js): swipe path -> words
|
||||
textmodel.js Steam (argument of patch.js): what the keyboard typed
|
||||
corrector.js Steam (argument of patch.js): corrections, completions
|
||||
gesture-input.js Steam (argument of patch.js): a gesture's own events
|
||||
suggestions-panel/
|
||||
patch.js, unpatch.js SteamVR: the strip as a panel above/below the keyboard
|
||||
relay.mjs service vr-keyboard-relay: strip state Steam <-> SteamVR
|
||||
dictionary.nix, gen-dict.py build: dictionary from wordfreq + Hunspell
|
||||
check.nix, tests/ test (also built before the patch): text model,
|
||||
corrector, swipe-decoder accuracy
|
||||
corrector, swipe-decoder accuracy, gesture input
|
||||
dashboard-windows.nix, dashboard-windows/ SteamVR: window scale/distance limits
|
||||
steam-close-button.nix, steam-close-button/ SteamVR: X on the Steam window
|
||||
window-curvature.nix, window-curvature/ SteamVR: curvature wheel
|
||||
|
||||
+8
-2
@@ -101,6 +101,9 @@ default):
|
||||
release on the last. The word is typed with a space before it if needed
|
||||
(`text.autoSpace`); alternatives show in the strip. `'` and `-` are typed,
|
||||
not swiped.
|
||||
With both controllers on the keyboard, a swipe follows only the
|
||||
controller that pressed; the other one's laser doesn't enter its path,
|
||||
and its presses meanwhile are plain taps.
|
||||
- **Suggestions** never change text by themselves: a finished tapped word
|
||||
that isn't in the dictionary gets corrections (itself first;
|
||||
`autocorrect`), a word being tapped gets completions (the typed letters
|
||||
@@ -141,7 +144,9 @@ via its xdotool helper).
|
||||
changes them the keyboard stays stock (see
|
||||
[after a Steam update](ui-patches.md#after-a-steam-update)). Accented words
|
||||
of other languages are in the dictionary but only swipable where the layout
|
||||
has the letters.
|
||||
has the letters. With two controllers on the keyboard, SteamVR sometimes
|
||||
sends no laser movement for a press (seen for the controller that didn't
|
||||
own the keyboard's cursor just before): that press stays a tap.
|
||||
|
||||
**Remove when** Steam's VR keyboard gets swipe typing and suggestions.
|
||||
|
||||
@@ -162,7 +167,8 @@ has the letters.
|
||||
- Found by signature (entries `vr-keyboard`, `vr-keyboard-panel`).
|
||||
|
||||
**Tests:** `nix flake check` (checks `vr-keyboard`: text model, corrector,
|
||||
decoder accuracy on German + English) and `keyboard.vr.checks` for the
|
||||
decoder accuracy on German + English,
|
||||
gesture input from two controllers) and `keyboard.vr.checks` for the
|
||||
configured dictionary.
|
||||
|
||||
**Debugging:** `window.__sfuiSwipeLog` and `__sfuiSwipePaths` in Steam's
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Swipe typing, suggestions and a Backspace drag for Steam's VR keyboard
|
||||
# (steamFrame.keyboard.vr; the extra keys are vr-keyboard-extra-keys.nix).
|
||||
# - vr-keyboard/patch.js (Steam UI, 8080): swipe gestures (swipe-decoder.js),
|
||||
# - vr-keyboard/patch.js (Steam UI, 8080): swipe gestures (swipe-decoder.js;
|
||||
# gesture-input.js: the events of the laser that pressed),
|
||||
# a model of what the keyboard typed (textmodel.js), suggestions (swipe
|
||||
# alternatives, corrections and completions, corrector.js; never changing
|
||||
# text by themselves), Backspace drag (left: delete with a detent at word
|
||||
@@ -55,7 +56,7 @@ let
|
||||
inherit (cfg.backspaceDrag) wordDetentPixels;
|
||||
inherit (cfg) haptics;
|
||||
};
|
||||
extraArgs = [ ./vr-keyboard/swipe-decoder.js ./vr-keyboard/textmodel.js ./vr-keyboard/corrector.js dictionaryJs ];
|
||||
extraArgs = [ ./vr-keyboard/swipe-decoder.js ./vr-keyboard/textmodel.js ./vr-keyboard/corrector.js dictionaryJs ./vr-keyboard/gesture-input.js ];
|
||||
}} $out
|
||||
node --check $out
|
||||
'';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Tests of the VR keyboard (flake check `vr-keyboard`, also built before the
|
||||
# patch by vr-keyboard.nix with the configured dictionary): text model,
|
||||
# corrector and swipe-decoder accuracy (tests/*.test.mjs). The accuracy
|
||||
# gesture input (two controllers), corrector and swipe-decoder accuracy
|
||||
# (tests/*.test.mjs). The accuracy
|
||||
# thresholds (German keyboard geometry, German and English words) fail the
|
||||
# build only for German + English dictionaries, the default here.
|
||||
{ pkgs
|
||||
@@ -20,6 +21,7 @@ in
|
||||
pkgs.runCommand "vr-keyboard-checks" { nativeBuildInputs = [ pkgs.nodejs ]; } ''
|
||||
set -o pipefail
|
||||
node ${./tests/textmodel.test.mjs} ${./textmodel.js}
|
||||
node ${./tests/gesture-input.test.mjs} ${./gesture-input.js} ${./tests/two-controllers.json}
|
||||
node ${./tests/corrector.test.mjs} ${./corrector.js} ${./swipe-decoder.js} ${words} ${lib.optionalString (!strict) "|| echo warning: corrector test failed"}
|
||||
node ${./tests/swipe-decoder.test.mjs} ${./swipe-decoder.js} ${words} 100 1 | tee $out ${lib.optionalString (!strict) "|| true"}
|
||||
node -e '
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// gesture-input.js: which input events belong to the gesture in progress
|
||||
// (patch.js). Evaluates to { VERSION, create }.
|
||||
//
|
||||
// SteamVR's lasers reach the keyboard page as touches (a trigger press: one
|
||||
// touch with a new identifier per press) and as mouse hover (pointerId 1,
|
||||
// no hand distinguisher; Chromium's pointer events for touches are cancelled
|
||||
// after a few moves). With both controllers on the keyboard the idle one's
|
||||
// hover keeps arriving during the other's press. A gesture is therefore owned
|
||||
// by the contact that started it: a touch identifier, a pointerId or the
|
||||
// mouse. Moves, the release and a cancel of other contacts are ignored, so
|
||||
// they never enter the swipe path or end it. A second press while a gesture
|
||||
// runs is ignored (Steam handles it as stock); one whose owner is gone
|
||||
// (missed release) replaces it.
|
||||
(() => {
|
||||
const VERSION = 1;
|
||||
|
||||
function create() {
|
||||
let owner = null, sawPointer = false;
|
||||
|
||||
// The contacts of an event: [{ id, x, y, target }].
|
||||
function contacts(e) {
|
||||
if (e.type.startsWith('touch')) {
|
||||
const list = e.type === 'touchmove' ? e.touches : e.changedTouches;
|
||||
return [...(list || [])].map((t) => ({ id: `t${t.identifier}`, x: t.clientX, y: t.clientY, target: t.target }));
|
||||
}
|
||||
if (e.type.startsWith('pointer')) {
|
||||
sawPointer = true;
|
||||
if (e.pointerType === 'touch') return []; // touch events carry these
|
||||
return [{ id: `p${e.pointerId}`, x: e.clientX, y: e.clientY, target: e.target }];
|
||||
}
|
||||
if (sawPointer) return []; // pointer events carry the mouse
|
||||
return [{ id: 'm', x: e.clientX, y: e.clientY, target: e.target }];
|
||||
}
|
||||
const PHASE = {
|
||||
touchstart: 'down', pointerdown: 'down', mousedown: 'down',
|
||||
touchmove: 'move', pointermove: 'move', mousemove: 'move',
|
||||
touchend: 'up', pointerup: 'up', mouseup: 'up',
|
||||
touchcancel: 'cancel', pointercancel: 'cancel',
|
||||
};
|
||||
|
||||
return {
|
||||
VERSION,
|
||||
get owner() { return owner; },
|
||||
// { phase, id, x, y, target } if the event concerns the gesture (or may
|
||||
// start one: phase 'down'), else null. An 'up' or 'cancel' ends the
|
||||
// ownership.
|
||||
read(e) {
|
||||
const phase = PHASE[e.type];
|
||||
if (!phase) return null;
|
||||
const cs = contacts(e);
|
||||
if (phase === 'down') {
|
||||
// One touch at a time starts a gesture (a multi-touch press is Steam's).
|
||||
if (e.type === 'touchstart' && (e.touches?.length ?? 0) > 1) {
|
||||
if (owner?.[0] === 't' && ![...e.touches].some((t) => `t${t.identifier}` === owner)) owner = null;
|
||||
return null;
|
||||
}
|
||||
const c = cs[0];
|
||||
if (!c) return null;
|
||||
// Busy: ignored, unless the owner is gone (a lone touch, or its own id again).
|
||||
if (owner && !(e.type === 'touchstart' || c.id === owner)) return null;
|
||||
owner = null;
|
||||
return { phase, ...c };
|
||||
}
|
||||
if (!owner) return null;
|
||||
const c = cs.find((x) => x.id === owner);
|
||||
if (!c) return null;
|
||||
if (phase !== 'move') owner = null;
|
||||
return { phase, ...c };
|
||||
},
|
||||
claim(c) { owner = c.id; }, // the 'down' contact starts a gesture
|
||||
release() { owner = null; },
|
||||
};
|
||||
}
|
||||
|
||||
return { VERSION, create };
|
||||
})()
|
||||
@@ -1,8 +1,9 @@
|
||||
// Gestures and suggestions for Steam's VR keyboard (vr-keyboard.nix),
|
||||
// injected into Steam's SharedJSContext (8080). The keyboard is a popup of
|
||||
// that context ("SteamVR - Keyboard"); everything works on its document.
|
||||
// mkPatch convention plus four more arguments: swipe-decoder.js,
|
||||
// textmodel.js, corrector.js and the dictionary text ("word<TAB>zipf*10\n...").
|
||||
// mkPatch convention plus five more arguments: swipe-decoder.js,
|
||||
// textmodel.js, corrector.js, the dictionary text ("word<TAB>zipf*10\n...")
|
||||
// and gesture-input.js.
|
||||
//
|
||||
// - Swipe: the laser arrives as touch events; Steam types the key a touch
|
||||
// started on at release. Once a press leaves its first key we drop that
|
||||
@@ -27,8 +28,8 @@
|
||||
// missing the keyboard stays stock. Only passive listeners; never blocks
|
||||
// Steam's events. Debugging: __sfuiSwipeLog, __sfuiSwipePaths
|
||||
// (scripts/vr-keyboard-replay.mjs).
|
||||
((find, sigs, opts, hooks, D, T, C, DICT) => {
|
||||
const VERSION = 24;
|
||||
((find, sigs, opts, hooks, D, T, C, DICT, P) => {
|
||||
const VERSION = 25;
|
||||
const G = window;
|
||||
const O = opts;
|
||||
|
||||
@@ -376,25 +377,18 @@
|
||||
|
||||
// ---- gestures ------------------------------------------------------------------
|
||||
// down: { kind: 'strip', index } | { kind: 'backspace', ... } | { kind: 'key', ... }.
|
||||
// Touch is read from touch events (they go on after Chromium's
|
||||
// pointercancel); a mouse from pointer events, or mouse events if there
|
||||
// are no pointer events.
|
||||
let down = null, sawPointer = false;
|
||||
// input (gesture-input.js): the events of the contact that started it
|
||||
// (a touch per laser press, they go on after Chromium's pointercancel; a
|
||||
// mouse from pointer events, or mouse events if there are none). The
|
||||
// other controller's hover, a second press: ignored (Steam's, as stock).
|
||||
let down = null;
|
||||
const input = P.create();
|
||||
const keyOf = (t) => t?.closest?.('[data-key]');
|
||||
const stripIndexOf = (t) => {
|
||||
const el = t?.closest?.('#sfui-swipe-strip > div');
|
||||
return el && strip.contains(el) ? +el.dataset.index : -1;
|
||||
};
|
||||
const at = (x, y) => doc.elementFromPoint(x, y);
|
||||
function point(e) {
|
||||
if (e.type.startsWith('touch')) {
|
||||
const t = e.type === 'touchend' || e.type === 'touchcancel' ? e.changedTouches[0] : e.touches[0];
|
||||
return t && !(e.type === 'touchstart' && e.touches.length > 1) ? [t.clientX, t.clientY, t.target] : null;
|
||||
}
|
||||
if (e.type.startsWith('pointer')) { sawPointer = true; if (e.pointerType === 'touch') return null; }
|
||||
else if (sawPointer) return null;
|
||||
return [e.clientX, e.clientY, e.target];
|
||||
}
|
||||
// Keep Steam from typing the pressed key at release and from its long press.
|
||||
function takeOver(d) {
|
||||
const inst = d.inst;
|
||||
@@ -406,22 +400,24 @@
|
||||
} catch (e) { log('takeover-error', String(e)); }
|
||||
}
|
||||
function onDown(e) {
|
||||
const p = point(e);
|
||||
if (!p) return;
|
||||
const [x, y, target] = p;
|
||||
const c = input.read(e);
|
||||
if (!c) return;
|
||||
if (down?.active) clearTrail(); // its release was missed
|
||||
down = start(c);
|
||||
if (down) input.claim(c);
|
||||
}
|
||||
function start({ x, y, target }) {
|
||||
const si = stripIndexOf(target);
|
||||
if (si !== -1) { down = { kind: 'strip', index: si }; return; }
|
||||
if (si !== -1) return { kind: 'strip', index: si };
|
||||
const keyEl = keyOf(target), key = keyEl?.getAttribute('data-key'), inst = keyEl && kbInst(keyEl);
|
||||
down = null;
|
||||
if (!inst) return;
|
||||
if (!inst) return null;
|
||||
hookInst(inst);
|
||||
if (key === 'Backspace' && O.pixelsPerChar > 0) {
|
||||
down = { kind: 'backspace', keyEl, inst, x0: x, active: false, planned: 0,
|
||||
return { kind: 'backspace', keyEl, inst, x0: x, active: false, planned: 0,
|
||||
g: model.dragStart({ px: O.pixelsPerChar, detentPx: O.wordDetentPixels }) };
|
||||
} else if (O.swipe && isLetter(key)) {
|
||||
const lay = currentLayout();
|
||||
if (lay) down = { kind: 'key', key, keyEl, inst, lay, pts: [[x, y]], active: false };
|
||||
}
|
||||
const lay = O.swipe && isLetter(key) && currentLayout();
|
||||
return lay ? { kind: 'key', key, keyEl, inst, lay, pts: [[x, y]], active: false } : null;
|
||||
}
|
||||
// Backspace drag: a character per px of leftward travel (the first at px;
|
||||
// releasing before is Steam's tap), a detent of detentPx at a word border;
|
||||
@@ -445,9 +441,9 @@
|
||||
}
|
||||
}
|
||||
function onMove(e) {
|
||||
const p = point(e);
|
||||
if (!p || !down || down.kind === 'strip') return;
|
||||
const [x, y] = p;
|
||||
const c = input.read(e);
|
||||
if (!c || !down || down.kind === 'strip') return;
|
||||
const { x, y } = c;
|
||||
if (down.kind === 'backspace') { backspaceMove(down, x); return; }
|
||||
const q = down.pts[down.pts.length - 1];
|
||||
if (Math.hypot(x - q[0], y - q[1]) < 2) return;
|
||||
@@ -461,9 +457,9 @@
|
||||
drawTrail(down.pts, 'rgba(26, 159, 255, 0.75)');
|
||||
}
|
||||
function onUp(e) {
|
||||
const p = point(e);
|
||||
if (!p) return;
|
||||
const [x, y] = p, d = down;
|
||||
const c = input.read(e);
|
||||
if (!c) return;
|
||||
const { x, y } = c, d = down;
|
||||
down = null;
|
||||
if (d?.kind === 'strip') { // released anywhere on the strip: the button pressed
|
||||
if (strip.contains(at(x, y))) pick(d.index);
|
||||
@@ -492,7 +488,7 @@
|
||||
commit(d.inst, results);
|
||||
}
|
||||
function onCancel(e) {
|
||||
if (e.type === 'pointercancel' && e.pointerType === 'touch') return; // touch events go on
|
||||
if (!input.read(e)) return; // another contact; Chromium's touch pointercancel
|
||||
if (down?.active) clearTrail();
|
||||
down = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// gesture-input.js: a gesture only gets the events of the contact that
|
||||
// started it. Like patch.js: a 'down' starts a path (claim), moves extend
|
||||
// it, the release ends it. Fixture two-controllers.json: a swipe recorded in
|
||||
// VR (Steam's keyboard page) while the other controller pointed at the
|
||||
// keyboard; its hover (485,106) once broke into the path ("tust" for "test").
|
||||
// usage: node gesture-input.test.mjs <gesture-input.js> <two-controllers.json>
|
||||
import { readFileSync } from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const P = (0, eval)(readFileSync(process.argv[2], 'utf8'));
|
||||
let passed = 0;
|
||||
|
||||
const touch = ([identifier, clientX, clientY]) => ({ identifier, clientX, clientY, target: `@${clientX},${clientY}` });
|
||||
const ev = {
|
||||
touch: (type, touches, changed) => ({ type, touches: touches.map(touch), changedTouches: changed.map(touch) }),
|
||||
pointer: (type, pointerType, pointerId, clientX, clientY) => ({ type, pointerType, pointerId, clientX, clientY, target: 'p' }),
|
||||
mouse: (type, clientX, clientY) => ({ type, clientX, clientY, target: 'm' }),
|
||||
};
|
||||
// A recorded event: [type, touches, changed] | [type, pointerType, id, x, y] | [type, x, y].
|
||||
const fromRecord = (r) => (r[0].startsWith('touch') ? ev.touch(...r) : r[0].startsWith('pointer') ? ev.pointer(...r) : ev.mouse(...r));
|
||||
|
||||
// Feeds events like patch.js; returns the finished paths [{ id, pts, ended }].
|
||||
function run(events, { startIf = () => true } = {}) {
|
||||
const input = P.create(), paths = [];
|
||||
let cur = null;
|
||||
for (const e of events) {
|
||||
const c = input.read(e);
|
||||
if (!c) continue;
|
||||
if (c.phase === 'down') {
|
||||
cur = null;
|
||||
if (startIf(c)) { cur = { id: c.id, pts: [[c.x, c.y]] }; input.claim(c); }
|
||||
} else if (c.phase === 'move') {
|
||||
assert.ok(cur, 'a move without a gesture');
|
||||
cur.pts.push([c.x, c.y]);
|
||||
} else {
|
||||
assert.ok(cur, `${c.phase} without a gesture`);
|
||||
cur.pts.push([c.x, c.y]);
|
||||
cur.ended = c.phase;
|
||||
paths.push(cur);
|
||||
cur = null;
|
||||
}
|
||||
}
|
||||
if (cur) paths.push(cur);
|
||||
return paths;
|
||||
}
|
||||
function test(name, f) { f(); passed++; console.log(`ok ${name}`); }
|
||||
|
||||
test('recorded swipe with the other controller hovering', () => {
|
||||
const rec = JSON.parse(readFileSync(process.argv[3], 'utf8'));
|
||||
assert.ok(rec.some((r) => r[0] === 'pointermove' && r[1] === 'mouse' && r[3] === 485), 'fixture has the stray hover');
|
||||
const paths = run(rec.map(fromRecord));
|
||||
assert.equal(paths.length, 1);
|
||||
const [p] = paths;
|
||||
assert.equal(p.id, 't814');
|
||||
assert.equal(p.ended, 'up');
|
||||
assert.deepEqual(p.pts[0], [334, 74]);
|
||||
const touchMoves = rec.filter((r) => r[0] === 'touchmove').map((r) => r[1][0].slice(1));
|
||||
assert.deepEqual(p.pts.slice(1, -1), touchMoves, 'the path is exactly its touch moves');
|
||||
assert.ok(!p.pts.some(([x, y]) => x === 485 && y === 106), 'no hover of the other controller');
|
||||
});
|
||||
|
||||
test('two touches interleaved: the second is ignored until the first ends', () => {
|
||||
const T = (type, touches, changed) => ev.touch(type, touches, changed);
|
||||
const paths = run([
|
||||
ev.pointer('pointermove', 'mouse', 1, 600, 150), // B hovers
|
||||
T('touchstart', [[1, 100, 50]], [[1, 100, 50]]), // A presses
|
||||
ev.pointer('pointermove', 'mouse', 1, 610, 150), // B's hover meanwhile
|
||||
T('touchmove', [[1, 120, 55]], [[1, 120, 55]]),
|
||||
T('touchstart', [[1, 120, 55], [2, 500, 120]], [[2, 500, 120]]), // B presses
|
||||
T('touchmove', [[1, 120, 55], [2, 520, 120]], [[2, 520, 120]]), // only B moved
|
||||
T('touchmove', [[1, 140, 60], [2, 520, 120]], [[1, 140, 60]]),
|
||||
T('touchend', [[1, 140, 60]], [[2, 530, 121]]), // B releases first
|
||||
T('touchmove', [[1, 160, 70]], [[1, 160, 70]]),
|
||||
ev.mouse('mousemove', 700, 160), // no pointer events seen yet: still not A's
|
||||
T('touchend', [], [[1, 170, 72]]), // A releases
|
||||
T('touchmove', [[3, 300, 90]], [[3, 300, 90]]), // a touch that started elsewhere
|
||||
T('touchstart', [[4, 200, 80]], [[4, 200, 80]]), // B swipes alone
|
||||
ev.pointer('pointermove', 'mouse', 1, 50, 50), // A's hover
|
||||
T('touchmove', [[4, 230, 85]], [[4, 230, 85]]),
|
||||
T('touchend', [], [[4, 240, 90]]),
|
||||
]);
|
||||
assert.deepEqual(paths, [
|
||||
{ id: 't1', pts: [[100, 50], [120, 55], [120, 55], [140, 60], [160, 70], [170, 72]], ended: 'up' },
|
||||
{ id: 't4', pts: [[200, 80], [230, 85], [240, 90]], ended: 'up' },
|
||||
]);
|
||||
});
|
||||
|
||||
test("a cancel of another contact doesn't end the gesture", () => {
|
||||
const paths = run([
|
||||
ev.pointer('pointerdown', 'touch', 7, 10, 10),
|
||||
ev.touch('touchstart', [[9, 10, 10]], [[9, 10, 10]]),
|
||||
ev.pointer('pointercancel', 'touch', 7, 0, 0), // Chromium's, after a few moves
|
||||
ev.touch('touchcancel', [[9, 12, 10]], [[8, 0, 0]]),
|
||||
ev.touch('touchmove', [[9, 30, 12]], [[9, 30, 12]]),
|
||||
ev.touch('touchcancel', [], [[9, 30, 12]]),
|
||||
]);
|
||||
assert.deepEqual(paths, [{ id: 't9', pts: [[10, 10], [30, 12], [30, 12]], ended: 'cancel' }]);
|
||||
});
|
||||
|
||||
test('a missed release: the next lone press takes over', () => {
|
||||
const paths = run([
|
||||
ev.touch('touchstart', [[1, 10, 10]], [[1, 10, 10]]),
|
||||
ev.touch('touchmove', [[1, 20, 10]], [[1, 20, 10]]),
|
||||
ev.touch('touchstart', [[2, 50, 50]], [[2, 50, 50]]), // touch 1 is gone
|
||||
ev.touch('touchend', [], [[1, 25, 10]]),
|
||||
ev.touch('touchend', [], [[2, 60, 50]]),
|
||||
]);
|
||||
assert.deepEqual(paths.map((p) => [p.id, p.ended]), [['t2', 'up']]);
|
||||
});
|
||||
|
||||
test('presses that start no gesture leave the next one free', () => {
|
||||
const paths = run([
|
||||
ev.touch('touchstart', [[1, 10, 10]], [[1, 10, 10]]), // Shift held by one hand
|
||||
ev.touch('touchmove', [[1, 11, 10]], [[1, 11, 10]]),
|
||||
ev.touch('touchend', [], [[1, 11, 10]]),
|
||||
ev.touch('touchstart', [[2, 50, 50]], [[2, 50, 50]]),
|
||||
ev.touch('touchend', [], [[2, 60, 50]]),
|
||||
], { startIf: (c) => c.x > 30 });
|
||||
assert.deepEqual(paths, [{ id: 't2', pts: [[50, 50], [60, 50]], ended: 'up' }]);
|
||||
});
|
||||
|
||||
test('mouse: pointer events by pointerId, mouse events only without them', () => {
|
||||
assert.deepEqual(run([
|
||||
ev.mouse('mousedown', 1, 1), ev.mouse('mousemove', 2, 1), ev.mouse('mouseup', 3, 1),
|
||||
]), [{ id: 'm', pts: [[1, 1], [2, 1], [3, 1]], ended: 'up' }]);
|
||||
assert.deepEqual(run([
|
||||
ev.pointer('pointerdown', 'mouse', 1, 1, 1), ev.mouse('mousedown', 1, 1),
|
||||
ev.pointer('pointermove', 'pen', 2, 9, 9), ev.mouse('mousemove', 9, 9),
|
||||
ev.pointer('pointermove', 'mouse', 1, 2, 1),
|
||||
ev.pointer('pointerup', 'mouse', 1, 3, 1), ev.mouse('mouseup', 3, 1),
|
||||
]), [{ id: 'p1', pts: [[1, 1], [2, 1], [3, 1]], ended: 'up' }]);
|
||||
});
|
||||
|
||||
console.log(`gesture-input: ${passed} tests passed`);
|
||||
@@ -0,0 +1,120 @@
|
||||
[
|
||||
["pointerover","mouse",1,403,122],
|
||||
["mouseover",403,122],
|
||||
["pointermove","mouse",1,403,122],
|
||||
["mousemove",403,122],
|
||||
["pointerover","touch",812,334,74],
|
||||
["pointerdown","touch",812,334,74],
|
||||
["touchstart",[[814,334,74]],[[814,334,74]]],
|
||||
["pointerout","mouse",1,0,0],
|
||||
["pointermove","touch",812,335,74],
|
||||
["pointermove","touch",812,335,75],
|
||||
["pointermove","touch",812,336,75],
|
||||
["pointerover","mouse",1,485,106],
|
||||
["mouseover",485,106],
|
||||
["pointermove","mouse",1,485,106],
|
||||
["mousemove",485,106],
|
||||
["pointermove","touch",812,338,79],
|
||||
["pointermove","touch",812,339,80],
|
||||
["pointermove","touch",812,341,83],
|
||||
["pointermove","touch",812,342,84],
|
||||
["pointermove","touch",812,341,84],
|
||||
["pointermove","touch",812,341,83],
|
||||
["pointermove","touch",812,338,81],
|
||||
["pointermove","touch",812,334,80],
|
||||
["pointermove","touch",812,322,77],
|
||||
["pointermove","touch",812,317,77],
|
||||
["touchmove",[[814,317,77]],[[814,317,77]]],
|
||||
["pointercancel","touch",812,0,0],
|
||||
["pointerout","touch",812,0,0],
|
||||
["touchmove",[[814,304,76]],[[814,304,76]]],
|
||||
["touchmove",[[814,296,76]],[[814,296,76]]],
|
||||
["touchmove",[[814,288,76]],[[814,288,76]]],
|
||||
["touchmove",[[814,276,77]],[[814,276,77]]],
|
||||
["touchmove",[[814,272,79]],[[814,272,79]]],
|
||||
["touchmove",[[814,268,81]],[[814,268,81]]],
|
||||
["touchmove",[[814,265,83]],[[814,265,83]]],
|
||||
["touchmove",[[814,261,86]],[[814,261,86]]],
|
||||
["touchmove",[[814,255,91]],[[814,255,91]]],
|
||||
["touchmove",[[814,253,93]],[[814,253,93]]],
|
||||
["touchmove",[[814,249,96]],[[814,249,96]]],
|
||||
["touchmove",[[814,247,98]],[[814,247,98]]],
|
||||
["touchmove",[[814,241,101]],[[814,241,101]]],
|
||||
["touchmove",[[814,238,102]],[[814,238,102]]],
|
||||
["touchmove",[[814,236,103]],[[814,236,103]]],
|
||||
["touchmove",[[814,233,104]],[[814,233,104]]],
|
||||
["touchmove",[[814,232,106]],[[814,232,106]]],
|
||||
["touchmove",[[814,230,108]],[[814,230,108]]],
|
||||
["touchmove",[[814,229,111]],[[814,229,111]]],
|
||||
["touchmove",[[814,228,113]],[[814,228,113]]],
|
||||
["touchmove",[[814,226,115]],[[814,226,115]]],
|
||||
["touchmove",[[814,225,116]],[[814,225,116]]],
|
||||
["touchmove",[[814,223,117]],[[814,223,117]]],
|
||||
["touchmove",[[814,221,118]],[[814,221,118]]],
|
||||
["touchmove",[[814,218,118]],[[814,218,118]]],
|
||||
["touchmove",[[814,216,119]],[[814,216,119]]],
|
||||
["touchmove",[[814,213,119]],[[814,213,119]]],
|
||||
["touchmove",[[814,210,120]],[[814,210,120]]],
|
||||
["touchmove",[[814,208,121]],[[814,208,121]]],
|
||||
["touchmove",[[814,207,123]],[[814,207,123]]],
|
||||
["touchmove",[[814,205,124]],[[814,205,124]]],
|
||||
["touchmove",[[814,204,126]],[[814,204,126]]],
|
||||
["touchmove",[[814,202,128]],[[814,202,128]]],
|
||||
["touchmove",[[814,200,129]],[[814,200,129]]],
|
||||
["touchmove",[[814,199,129]],[[814,199,129]]],
|
||||
["touchmove",[[814,198,129]],[[814,198,129]]],
|
||||
["touchmove",[[814,197,130]],[[814,197,130]]],
|
||||
["touchmove",[[814,197,131]],[[814,197,131]]],
|
||||
["touchmove",[[814,197,132]],[[814,197,132]]],
|
||||
["touchmove",[[814,197,133]],[[814,197,133]]],
|
||||
["touchmove",[[814,197,134]],[[814,197,134]]],
|
||||
["touchmove",[[814,197,135]],[[814,197,135]]],
|
||||
["touchmove",[[814,197,134]],[[814,197,134]]],
|
||||
["touchmove",[[814,198,134]],[[814,198,134]]],
|
||||
["touchmove",[[814,199,134]],[[814,199,134]]],
|
||||
["touchmove",[[814,200,134]],[[814,200,134]]],
|
||||
["touchmove",[[814,201,134]],[[814,201,134]]],
|
||||
["touchmove",[[814,202,134]],[[814,202,134]]],
|
||||
["touchmove",[[814,204,133]],[[814,204,133]]],
|
||||
["touchmove",[[814,206,133]],[[814,206,133]]],
|
||||
["touchmove",[[814,208,133]],[[814,208,133]]],
|
||||
["touchmove",[[814,210,132]],[[814,210,132]]],
|
||||
["touchmove",[[814,213,132]],[[814,213,132]]],
|
||||
["touchmove",[[814,215,132]],[[814,215,132]]],
|
||||
["touchmove",[[814,218,132]],[[814,218,132]]],
|
||||
["touchmove",[[814,221,131]],[[814,221,131]]],
|
||||
["touchmove",[[814,224,130]],[[814,224,130]]],
|
||||
["touchmove",[[814,229,129]],[[814,229,129]]],
|
||||
["touchmove",[[814,234,128]],[[814,234,128]]],
|
||||
["touchmove",[[814,239,125]],[[814,239,125]]],
|
||||
["touchmove",[[814,247,120]],[[814,247,120]]],
|
||||
["touchmove",[[814,257,111]],[[814,257,111]]],
|
||||
["touchmove",[[814,268,102]],[[814,268,102]]],
|
||||
["touchmove",[[814,280,93]],[[814,280,93]]],
|
||||
["touchmove",[[814,289,86]],[[814,289,86]]],
|
||||
["touchmove",[[814,297,81]],[[814,297,81]]],
|
||||
["touchmove",[[814,304,77]],[[814,304,77]]],
|
||||
["touchmove",[[814,311,74]],[[814,311,74]]],
|
||||
["touchmove",[[814,315,73]],[[814,315,73]]],
|
||||
["touchmove",[[814,320,72]],[[814,320,72]]],
|
||||
["touchmove",[[814,322,72]],[[814,322,72]]],
|
||||
["touchmove",[[814,323,71]],[[814,323,71]]],
|
||||
["touchmove",[[814,324,70]],[[814,324,70]]],
|
||||
["touchmove",[[814,326,68]],[[814,326,68]]],
|
||||
["touchmove",[[814,328,67]],[[814,328,67]]],
|
||||
["touchmove",[[814,330,65]],[[814,330,65]]],
|
||||
["touchmove",[[814,333,64]],[[814,333,64]]],
|
||||
["touchmove",[[814,336,64]],[[814,336,64]]],
|
||||
["touchmove",[[814,339,65]],[[814,339,65]]],
|
||||
["touchmove",[[814,341,65]],[[814,341,65]]],
|
||||
["touchmove",[[814,342,68]],[[814,342,68]]],
|
||||
["touchmove",[[814,343,71]],[[814,343,71]]],
|
||||
["touchmove",[[814,342,76]],[[814,342,76]]],
|
||||
["touchmove",[[814,342,77]],[[814,342,77]]],
|
||||
["touchmove",[[814,342,78]],[[814,342,78]]],
|
||||
["touchmove",[[814,341,78]],[[814,341,78]]],
|
||||
["touchmove",[[814,341,77]],[[814,341,77]]],
|
||||
["touchmove",[[814,341,76]],[[814,341,76]]],
|
||||
["touchmove",[[814,340,76]],[[814,340,76]]],
|
||||
["touchend",[],[[814,340,76]]]
|
||||
]
|
||||
Reference in new issue
Block a user