mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-15 04:05:35 +02:00
The posture's and a raise's start used -1 for none, so a replay of a flag (times before it negative) never saw the posture. Found replaying the 2026-10-10 flags through it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
353 lines
14 KiB
C++
353 lines
14 KiB
C++
// 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 <cmath>
|
|
#include <cstdio>
|
|
#include <functional>
|
|
#include <string>
|
|
|
|
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<void()> &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");
|
|
}
|
|
|
|
// Pointing with one hand (its ray up) and pressing Enter with the other: the pointing hand isn't held
|
|
// back. The same hand idle where it was (not pointing): the key holds it back for 2 s.
|
|
void TestPointAndEnter() {
|
|
World w;
|
|
w.hands[0] = Lap(0), w.hands[1] = Pointing(1);
|
|
w.hands[1].busy = true;
|
|
w.Run(1);
|
|
w.ty.Key(w.t);
|
|
w.Run(0.5);
|
|
CHECK(!w.Blocked(1), "the pointing hand: free (%s)", w.ty.Why(1, w.t));
|
|
CHECK(w.Blocked(0), "the hand that pressed the key: held back");
|
|
World v;
|
|
v.hands[0] = Lap(0), v.hands[1] = Pointing(1);
|
|
v.Run(1);
|
|
v.ty.Key(v.t);
|
|
v.Run(0.5);
|
|
CHECK(v.Blocked(1), "a raised hand that isn't in use: held back by the key");
|
|
// a hand on the keys whose ray came up (today, 14:57:37): on the keyboard all the same
|
|
World k;
|
|
k.hands[0] = Keys(0), k.hands[1] = Keys(1);
|
|
k.hands[1].busy = true;
|
|
k.Run(1, 0.2);
|
|
CHECK(k.ty.Parked(1) && k.Blocked(1), "a typing hand with its ray up: on the keyboard");
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
|
|
// Times below zero (a replay's, relative to a flag): the same.
|
|
void TestNegativeClock() {
|
|
World w;
|
|
w.t = -16;
|
|
w.ty.Key(w.t - 60);
|
|
w.hands[0] = Keys(0), w.hands[1] = Keys(1);
|
|
w.Run(0.4);
|
|
CHECK(w.ty.Posture() && w.Blocked(1), "the posture at t < 0");
|
|
w.Run(1, 0.2);
|
|
w.hands[1] = Pointing(1);
|
|
w.Run(0.3);
|
|
CHECK(!w.Blocked(1), "raised at t < 0: free");
|
|
}
|
|
|
|
// 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();
|
|
TestPointAndEnter();
|
|
TestTurned();
|
|
TestOff();
|
|
TestHeading();
|
|
TestNegativeClock();
|
|
if (failures) {
|
|
std::printf("%d failures\n", failures);
|
|
return 1;
|
|
}
|
|
std::printf("typing-test: ok\n");
|
|
return 0;
|
|
}
|