From a67abc62ff4e7a58933905911d84dc96bbff5499 Mon Sep 17 00:00:00 2001 From: DeeJanuz <45082401+DeeJanuz@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:26:46 -0600 Subject: [PATCH] pointer: typing.h, which hands typing holds back (keys, on the keyboard, the posture) A key holds every hand back for POINTER_PINCH_TYPING (now 2 s); a hand in the keyboard zone while typing stays held back until it rises POINTER_TYPING_RAISE above where it typed (or turns its palm); both hands in the keyboard posture count as typing within POINTER_TYPING_RECENT of a key. Tests with made-up keys and hands placed as in the 2026-10-10 flags and the desk take. Co-Authored-By: Claude Opus 5.5 --- pointer/helper/typing.h | 194 +++++++++++++++++++++ pointer/tests/typing-test.cpp | 311 ++++++++++++++++++++++++++++++++++ 2 files changed, 505 insertions(+) create mode 100644 pointer/helper/typing.h create mode 100644 pointer/tests/typing-test.cpp diff --git a/pointer/helper/typing.h b/pointer/helper/typing.h new file mode 100644 index 0000000..c6dd315 --- /dev/null +++ b/pointer/helper/typing.h @@ -0,0 +1,194 @@ +// Typing on a real keyboard: which bare hands may not click, poke or point now. The pointer helper's +// side of "Typing" in ft-pointer.cpp. Header-only and free of OpenVR, so pointer/tests/typing-test.cpp +// can drive it with made-up keys and hands. +// +// Typing touches thumb to index like a pinch, curls the hand like a grip, and a hand on the keys sits +// where the hand ray's "raised" test begins (2026-10-10 headset session: knuckles 0.29-0.31 m under +// the eyes, the ray's limit 0.30; docs/hands-typing.md has the numbers). A key alone held pinches +// back for 1 s, and typing pauses are longer than that. So: +// +// Keys: the input relay says "typing" on a key (at most 4 times a second). For hold (2 s, +// POINTER_PINCH_TYPING) after a key, every hand is held back except one that has left the keyboard +// (below). +// +// On the keyboard (parked): a hand in the keyboard zone while keys come (or in the posture, below) is +// on the keyboard, and stays there, held back, however long the keys stop, until it leaves: its palm +// rises raise (0.08 m, POINTER_TYPING_RAISE) above where it typed, or turns (its normal's downward +// part under turned), for leave (0.1 s). Then it's free at once, keys or not: a pinch right after +// lifting a hand works. It's on the keyboard again once it's back down in the zone while typing. A +// hand unseen for forget (3 s) is forgotten. A hand on the mouse is on the keyboard too (beside it, +// palm down, at the same height): its pinches are the mouse's buttons. +// The zone: the palm at least below (0.20 m, POINTER_TYPING_BELOW) under the eyes, aheadMin to +// aheadMax (0.05-0.45 m) ahead of them across the floor (along the head's heading), the palm facing +// down (palmDown). Typing hands measured 0.29-0.42 m under the eyes and 0.18-0.34 m ahead, palms down +// 0.84-1.00; deliberate pointing 0.0-0.30 m under the eyes, 0.33-0.46 m ahead. Where it typed: the +// palm's height, followed with restTau while it stays within raise / 2 of it. +// +// The posture (before the first key): both hands in the zone, level within level (0.08 m) and at most +// gap (0.32 m) apart, for posture (0.3 s, POINTER_TYPING_POSTURE; 0: keys only), count as typing, so +// the first keystroke's pinch is held back too. Typing hands were 0.15-0.22 m apart and level within +// 1-5 cm; a hand on the mouse 0.41 m from the other. Only within recent (600 s, POINTER_TYPING_RECENT; +// 0: always) of a key: a keyboard in use. Deliberate gaze pinches with the elbows resting can sit as +// low as typing, and both hands resting on a desk would otherwise hold them back. +// +// Times are seconds on one steady clock (CLOCK_MONOTONIC). Positions are the standing universe's +// (+y up), metres. +#pragma once + +#include +#include +#include +#include +#include + +namespace typing { + +struct V3 { + double x = 0, y = 0, z = 0; +}; + +struct Params { + double hold = 2.0; // POINTER_PINCH_TYPING: keys hold every hand back this long (0: no guard at all) + double raise = 0.08; // POINTER_TYPING_RAISE: a hand leaves the keyboard this far above where it typed + double turned = 0.2; // ...or with its palm turned (the normal's downward part under this) + double leave = 0.1; // ...for this long + double below = 0.20; // POINTER_TYPING_BELOW: the zone: the palm at least this far under the eyes + double aheadMin = 0.05, aheadMax = 0.45; // ...this far ahead of them + double palmDown = 0.6; // ...facing down at least this much + double posture = 0.3; // POINTER_TYPING_POSTURE: both hands in the posture this long is typing (0: off) + double gap = 0.32, level = 0.08; // the posture: palms this close, this level + double recent = 600; // POINTER_TYPING_RECENT: the posture only this long after a key (0: always) + double forget = 3.0; // a hand unseen this long is forgotten + double restTau = 0.5; // where it typed follows the palm this slowly +}; + +// One side's hand in a frame: seen, its palm's centre, and how far its palm faces down (the palm +// normal's -y: 1 straight down). +struct Hand { + bool seen = false; + V3 palm; + double down = 0; +}; + +// The head in that frame: where the eyes are, and where it faces (its -z; only the heading counts). +struct Head { + V3 pos, fwd{0, 0, -1}; +}; + +class Typing { +public: + explicit Typing(Params p = {}) : p_(p) {} + void SetParams(const Params &p) { p_ = p; } + const Params ¶ms() const { return p_; } + + // A key on a keyboard at t. + void Key(double t) { lastKey_ = t; } + double LastKey() const { return lastKey_; } + + // Each hands frame (t: its capture time). + void Frame(double t, const Head &head, const Hand hands[2]) { + if (p_.hold <= 0) { + for (Side &s : side_) s = Side{}; + postureSince_ = -1, posture_ = false; + return; + } + double fx = head.fwd.x, fz = head.fwd.z; + const double fl = std::hypot(fx, fz); + if (fl > 1e-6) fx /= fl, fz /= fl; + else fx = 0, fz = -1; + bool in[2] = {false, false}; + for (int k = 0; k < 2; ++k) { + const Hand &h = hands[k]; + if (!h.seen) continue; + const double below = head.pos.y - h.palm.y; + const double ahead = (h.palm.x - head.pos.x) * fx + (h.palm.z - head.pos.z) * fz; + in[k] = below >= p_.below && ahead >= p_.aheadMin && ahead <= p_.aheadMax && h.down >= p_.palmDown; + } + // the posture + bool pose = in[0] && in[1] && std::fabs(hands[0].palm.y - hands[1].palm.y) <= p_.level && + Dist(hands[0].palm, hands[1].palm) <= p_.gap; + pose = pose && p_.posture > 0 && (p_.recent <= 0 || (lastKey_ > -1e17 && t - lastKey_ < p_.recent)); + if (!pose) postureSince_ = -1, posture_ = false; + else if (postureSince_ < 0) postureSince_ = t; + if (pose && !posture_ && t - postureSince_ >= p_.posture - 1e-9) { + posture_ = true; + log.push_back("typing: both hands in the keyboard posture"); + } + const bool typing = Keys(t) || posture_; + for (int k = 0; k < 2; ++k) { + Side &s = side_[k]; + const Hand &h = hands[k]; + if (!h.seen) { + if ((s.parked || s.free) && t - s.seen > p_.forget) { + if (s.parked) log.push_back(std::string("typing: ") + Name(k) + " hand forgotten (unseen)"); + s = Side{}; + } + continue; + } + const double dt = s.seen > -1e17 ? std::clamp(t - s.seen, 0.0, 1.0) : 0; + s.seen = t; + if (s.parked) { + const double rose = h.palm.y - s.restY; + const bool up = rose >= p_.raise, turned = h.down < p_.turned; + if (up || turned) { + if (s.leaveSince < 0) s.leaveSince = t; + if (t - s.leaveSince >= p_.leave - 1e-9) { + s.parked = false, s.free = true, s.leaveSince = -1; + char b[96]; + if (up) std::snprintf(b, sizeof b, "typing: %s hand off the keyboard (raised %.2f m)", Name(k), rose); + else std::snprintf(b, sizeof b, "typing: %s hand off the keyboard (palm turned)", Name(k)); + log.push_back(b); + } + } else { + s.leaveSince = -1; + if (std::fabs(rose) < p_.raise / 2) + s.restY += rose * (1 - std::exp(-dt / std::max(1e-3, p_.restTau))); + } + continue; + } + // back down where it typed (or in the zone, if it never typed here) while typing: on the keyboard + const bool down = in[k] && (!s.known || h.palm.y - s.restY < p_.raise / 2); + if (typing && down) { + s.parked = true, s.free = false, s.leaveSince = -1; + if (!s.known || std::fabs(h.palm.y - s.restY) >= p_.raise / 2) s.restY = h.palm.y; + s.known = true; + log.push_back(std::string("typing: ") + Name(k) + " hand on the keyboard"); + } + } + } + + // Typing by the keys: within hold of the last one. + bool Keys(double t) const { return p_.hold > 0 && lastKey_ > -1e17 && t - lastKey_ < p_.hold; } + bool Posture() const { return posture_; } + bool Parked(int s) const { return side_[s].parked; } + bool Free(int s) const { return side_[s].free; } + + // Why side s may not click, poke or point at t, or nullptr: its hand is on the keyboard, or keys + // came and it hasn't left the keyboard since. + const char *Why(int s, double t) const { + if (p_.hold <= 0 || s < 0 || s > 1) return nullptr; + if (side_[s].parked) return "typing: the hand is on the keyboard"; + if (side_[s].free) return nullptr; + if (Keys(t) || posture_) return "typing"; + return nullptr; + } + bool Blocked(int s, double t) const { return Why(s, t) != nullptr; } + + std::vector log; // changes, since the caller last cleared it + +private: + struct Side { + bool parked = false; // on the keyboard + bool free = false; // left it since (keys don't hold it back) + bool known = false; // restY is where it typed + double restY = 0, seen = -1e18, leaveSince = -1; + }; + static double Dist(const V3 &a, const V3 &b) { return std::sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + (a.z - b.z) * (a.z - b.z)); } + static const char *Name(int k) { return k ? "right" : "left"; } + Params p_; + Side side_[2]; + double lastKey_ = -1e18, postureSince_ = -1; + bool posture_ = false; +}; + +} // namespace typing diff --git a/pointer/tests/typing-test.cpp b/pointer/tests/typing-test.cpp new file mode 100644 index 0000000..0b96e0c --- /dev/null +++ b/pointer/tests/typing-test.cpp @@ -0,0 +1,311 @@ +// Unit tests for typing on a real keyboard (pointer/helper/typing.h): made-up keys and hands at +// ft-hands' 30 Hz, the head at eye height facing -z. The hands sit where the 2026-10-10 headset +// session and the hand recorder's desk take had them (docs/hands-typing.md). +// Build and run: just test-c (or g++ -std=c++17 -I. pointer/tests/typing-test.cpp). +#include "pointer/helper/typing.h" + +#include +#include +#include +#include + +using namespace typing; + +static int failures = 0; +#define CHECK(cond, ...) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: ", __FILE__, __LINE__); \ + std::printf(__VA_ARGS__); \ + std::printf("\n"); \ + ++failures; \ + } \ + } while (0) + +namespace { + +constexpr double kFrame = 1 / 30.0; +constexpr double kEye = 1.2; + +// Palms: on the keyboard (today's first flag: 0.31 m under the eyes, 0.30 ahead, 0.17 apart), +// on the mouse (the desk take: 0.39 under, 0.27 ahead, 0.39 from the other hand), raised to point +// (0.15 under, 0.40 ahead, palm to the side), in the lap. +Hand Keys(int side) { return {true, {side ? 0.085 : -0.085, kEye - 0.31, -0.30}, 0.97}; } +Hand Mouse() { return {true, {0.30, kEye - 0.39, -0.27}, 0.85}; } +Hand Pointing(int side) { return {true, {side ? 0.18 : -0.18, kEye - 0.15, -0.40}, 0.3}; } +Hand Lap(int side) { return {true, {side ? 0.15 : -0.15, kEye - 0.55, -0.05}, 0.5}; } +Hand Unseen() { return {}; } + +struct World { + Typing ty; + double t = 1000; + Head head{{0, kEye, 0}, {0, 0, -1}}; + Hand hands[2] = {Unseen(), Unseen()}; + std::string log; + explicit World(Params p = {}) : ty(p) {} + void Step() { + ty.Frame(t, head, hands); + for (const std::string &l : ty.log) log += l + "\n"; + ty.log.clear(); + t += kFrame; + } + // Runs secs of frames; each: called before each frame; keyEvery: a key that often (0: none). + void Run(double secs, double keyEvery = 0, const std::function &each = {}) { + const double end = t + secs - 1e-9; + double nextKey = t; + while (t < end) { + if (keyEvery > 0 && t >= nextKey) ty.Key(t), nextKey += keyEvery; + if (each) each(); + Step(); + } + } + bool Blocked(int s) const { return ty.Blocked(s, t); } +}; + +// No hands seen: a key holds every hand back for POINTER_PINCH_TYPING (2 s), then nothing. +void TestKeysAlone() { + World w; + CHECK(!w.Blocked(0) && !w.Blocked(1), "no keys: nothing held back"); + w.ty.Key(w.t); + w.Run(1.9); + CHECK(w.Blocked(0) && w.Blocked(1), "1.9 s after a key: held back"); + CHECK(std::string(w.ty.Why(1, w.t)) == "typing", "why: %s", w.ty.Why(1, w.t)); + w.Run(0.2); + CHECK(!w.Blocked(0) && !w.Blocked(1), "2.1 s after: not"); +} + +// The live complaint (14:57:10-30): typing with pauses of a few seconds, the hands on the keys all +// along. The old 1 s guard let the pinches in the pauses through; on the keyboard they stay held back. +void TestPausesKeepTheHandsParked() { + World w; + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(0.1); + CHECK(!w.Blocked(1), "hands on the keys, no key yet (and none for a while): free"); + w.Run(3, 0.2); + CHECK(w.ty.Parked(0) && w.ty.Parked(1), "typing: both hands on the keyboard"); + bool always = true; + w.Run(10, 0, [&] { always = always && w.Blocked(0) && w.Blocked(1); }); + CHECK(always, "a 10 s pause with the hands on the keys: held back throughout"); + CHECK(std::string(w.ty.Why(0, w.t)) == "typing: the hand is on the keyboard", "why: %s", w.ty.Why(0, w.t)); + CHECK(w.log.find("left hand on the keyboard") != std::string::npos && w.log.find("right hand on the keyboard") != std::string::npos, + "logged: %s", w.log.c_str()); + // small movements on the keys (reaching for a key, 3 cm up and forward) don't free it + w.Run(2, 0.25, [&] { + const double k = std::sin(w.t * 9); + w.hands[1].palm.y = Keys(1).palm.y + 0.03 * k, w.hands[1].palm.z = Keys(1).palm.z - 0.04 * std::fabs(k); + }); + CHECK(w.ty.Parked(1), "typing movements keep it on the keyboard"); + // a single wild frame (a misread hand 15 cm up, palm flipped) doesn't free it either + w.hands[1] = Keys(1), w.hands[1].palm.y += 0.15, w.hands[1].down = -0.5; + w.Step(); + w.hands[1] = Keys(1); + w.Run(1); + CHECK(w.ty.Parked(1), "one misread frame: still on the keyboard"); +} + +// Lifting a hand off the keyboard: a deliberate pinch 0.5 s after it began to rise works, even with +// the other hand still typing; the typing hand stays held back. +void TestRaiseFreesAtOnce() { + World w; + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(2, 0.2); + const double from = w.t; + const Hand a = Keys(1), b = Pointing(1); + w.Run(0.5, 0.2, [&] { + const double f = std::clamp((w.t - from) / 0.25, 0.0, 1.0); // up in 0.25 s + w.hands[1].palm = {a.palm.x + (b.palm.x - a.palm.x) * f, a.palm.y + (b.palm.y - a.palm.y) * f, a.palm.z + (b.palm.z - a.palm.z) * f}; + w.hands[1].down = a.down + (b.down - a.down) * f; + }); + CHECK(!w.Blocked(1), "raised 0.5 s ago, keys still coming from the other hand: free"); + CHECK(w.Blocked(0), "the typing hand: held back"); + CHECK(w.log.find("right hand off the keyboard (raised") != std::string::npos, "logged: %s", w.log.c_str()); + // the freed hand stays free while it's up, keys or not + w.Run(3, 0.2); + CHECK(!w.Blocked(1) && w.Blocked(0), "up while the other types: free"); + // when it was freed: within 0.2 s of starting up (8 cm of a 16 cm raise in 0.25 s, then 0.1 s) + World v; + v.hands[0] = Keys(0), v.hands[1] = Keys(1); + v.Run(2, 0.2); + const double start = v.t; + double freedAt = -1; + v.Run(1, 0.2, [&] { + const double f = std::clamp((v.t - start) / 0.25, 0.0, 1.0); + v.hands[1].palm.y = a.palm.y + (b.palm.y - a.palm.y) * f; + if (freedAt < 0 && !v.Blocked(1)) freedAt = v.t; + }); + CHECK(freedAt > 0 && freedAt - start <= 0.3, "free %.2f s after starting up", freedAt - start); +} + +// Back down on the keys: on the keyboard again once keys come, or the posture holds. +void TestBackDown() { + World w; + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(1, 0.2); + w.hands[1] = Pointing(1); + w.Run(0.5); + CHECK(!w.ty.Parked(1) && w.ty.Free(1), "raised: free"); + w.hands[1] = Keys(1); + w.Run(0.2, 0.1); + CHECK(w.ty.Parked(1) && w.Blocked(1), "back down while typing: on the keyboard"); + // up again, and down after the keys stopped: the posture puts it back within 0.3 s + w.hands[1] = Pointing(1); + w.Run(3); + CHECK(!w.Blocked(1), "up, no keys for 3 s: free"); + w.hands[1] = Keys(1); + w.Run(0.2); + CHECK(!w.Blocked(1), "just back down, no key: not yet"); + w.Run(0.3); + CHECK(w.ty.Posture() && w.Blocked(1), "both hands in the posture 0.3 s, a key this session: held back"); +} + +// The posture before the first key: only within POINTER_TYPING_RECENT of a key (a keyboard in use). +void TestPosture() { + World never; + never.hands[0] = Keys(0), never.hands[1] = Keys(1); + never.Run(5); + CHECK(!never.Blocked(0) && !never.Blocked(1), "no key ever: the posture alone holds nothing back"); + World w; + w.ty.Key(w.t); + w.Run(60); + CHECK(!w.Blocked(1), "a key a minute ago, hands away: free"); + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(0.2); + CHECK(!w.Blocked(1), "hands just down: not yet"); + w.Run(0.2); + CHECK(w.Blocked(0) && w.Blocked(1), "0.4 s in the posture: the first keystroke's pinch is held back"); + CHECK(w.log.find("keyboard posture") != std::string::npos, "logged: %s", w.log.c_str()); + // today's second flag: hands lower and nearer (0.41 under the eyes, 0.20 ahead, 0.15 apart) + World low; + low.ty.Key(low.t); + low.Run(5); + low.hands[0] = {true, {-0.075, kEye - 0.41, -0.20}, 0.87}, low.hands[1] = {true, {0.075, kEye - 0.42, -0.20}, 0.96}; + low.Run(0.4); + CHECK(low.Blocked(0) && low.Blocked(1), "lower hands, nearer: the posture too"); + // POINTER_TYPING_RECENT=0: always; POINTER_TYPING_POSTURE=0: keys only + Params always; + always.recent = 0; + World a(always); + a.hands[0] = Keys(0), a.hands[1] = Keys(1); + a.Run(0.4); + CHECK(a.Blocked(1), "recent 0: the posture without any key"); + Params off; + off.posture = 0; + World o(off); + o.ty.Key(o.t); + o.Run(5); + o.hands[0] = Keys(0), o.hands[1] = Keys(1); + o.Run(2); + CHECK(!o.Blocked(1), "posture 0: keys only"); +} + +// What isn't the posture: one hand low alone (a gaze pinch with the elbow resting, the other hand in +// the lap), a hand on the mouse beside the keyboard hand, both hands up pointing. +void TestNotThePosture() { + World w; + w.ty.Key(w.t); + w.Run(5); + w.hands[0] = Lap(0), w.hands[1] = {true, {0.15, kEye - 0.40, -0.30}, 0.9}; + w.Run(2); + CHECK(!w.Blocked(1), "one hand low alone: free (%s)", w.ty.Why(1, w.t)); + w.hands[0] = Keys(0), w.hands[1] = Mouse(); + w.Run(2); + CHECK(!w.Blocked(0) && !w.Blocked(1), "keyboard hand and mouse hand, no key: free"); + w.hands[0] = Pointing(0), w.hands[1] = Pointing(1); + w.Run(2); + CHECK(!w.Blocked(0) && !w.Blocked(1), "both up: free"); +} + +// A mouse user with one hand on the keyboard: a key (a shortcut) puts both on the keyboard, the mouse +// hand too (its pinches are the mouse's buttons); lifting the mouse hand frees it. +void TestMouseUser() { + World w; + w.hands[0] = Keys(0), w.hands[1] = Mouse(); + w.Run(1); + w.ty.Key(w.t); + w.Run(0.1); + CHECK(w.ty.Parked(0) && w.ty.Parked(1), "a shortcut: the keyboard hand and the mouse hand are on the keyboard"); + w.Run(20, 0, [&] { w.hands[1].palm.x = Mouse().palm.x + 0.05 * std::sin(w.t); }); // mousing + CHECK(w.Blocked(1), "mousing 20 s on: still held back"); + w.hands[1] = Pointing(1); + w.Run(0.3); + CHECK(!w.Blocked(1) && w.Blocked(0), "the mouse hand raised: free; the keyboard hand: not"); +} + +// Lost hands: through a short loss it stays on the keyboard; unseen 3 s, forgotten. +void TestLost() { + World w; + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(1, 0.2); + w.hands[1] = Unseen(); + w.Run(2); + w.hands[1] = Keys(1); + w.Step(); + CHECK(w.ty.Parked(1), "unseen 2 s, back where it typed: still on the keyboard"); + w.hands[1] = Unseen(); + w.Run(3.2); + CHECK(!w.ty.Parked(1) && !w.Blocked(1), "unseen 3 s: forgotten"); + // back up where it was raised: free + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(0.5, 0.2); + w.hands[1] = Unseen(); + w.Run(1); + w.hands[1] = Pointing(1); + w.Run(0.2); + CHECK(!w.Blocked(1), "lost on the keys, found raised: free"); +} + +// A palm turned up (the palm menu's pose) leaves the keyboard too. +void TestTurned() { + World w; + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(1, 0.2); + w.hands[1].down = -0.8; + w.Run(0.2); + CHECK(!w.Blocked(1), "palm turned up: free"); + CHECK(w.log.find("palm turned") != std::string::npos, "logged: %s", w.log.c_str()); +} + +void TestOff() { + Params p; + p.hold = 0; + World w(p); + w.hands[0] = Keys(0), w.hands[1] = Keys(1); + w.Run(2, 0.2); + CHECK(!w.Blocked(0) && !w.Blocked(1) && !w.ty.Parked(0), "POINTER_PINCH_TYPING=0: no guard"); +} + +// The head turned (facing +x): ahead counts along its heading. +void TestHeading() { + World w; + w.head.fwd = {1, -0.5, 0}; + w.hands[0] = {true, {0.30, kEye - 0.31, -0.085}, 0.97}, w.hands[1] = {true, {0.30, kEye - 0.31, 0.085}, 0.97}; + w.Run(1, 0.2); + CHECK(w.ty.Parked(0) && w.ty.Parked(1), "facing +x, the keyboard 0.3 m that way: on it"); + World v; + v.head.fwd = {-1, 0, 0}; + v.hands[0] = {true, {0.30, kEye - 0.31, -0.085}, 0.97}, v.hands[1] = {true, {0.30, kEye - 0.31, 0.085}, 0.97}; + v.Run(1, 0.2); + CHECK(!v.ty.Parked(0) && !v.ty.Parked(1), "behind the head: not"); +} + +} // namespace + +int main() { + TestKeysAlone(); + TestPausesKeepTheHandsParked(); + TestRaiseFreesAtOnce(); + TestBackDown(); + TestPosture(); + TestNotThePosture(); + TestMouseUser(); + TestLost(); + TestTurned(); + TestOff(); + TestHeading(); + if (failures) { + std::printf("%d failures\n", failures); + return 1; + } + std::printf("typing-test: ok\n"); + return 0; +}