Merge branch 'hands-ray-fix' into hands-full

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-09 11:13:16 -06:00
commit 2fcd29c0f5
7 files changed
+892 -106

No files matched your search

+1 -1
View File
@@ -281,7 +281,7 @@ Your hands show over the screens: where a tracked hand is between an eye and a s
- `ft-camd` borrows XRService's camera buffers and publishes the four IR tracking cameras to `/run/user/UID/frametop-hands/cam-ring`. It runs on the host as `frametop-camd.service`, with file capabilities that `hands/run.sh install` sets through sudo, and it drops them once set up. A rebuild clears them: `hands/run.sh caps`. The Hand Recorder's installer (`hands/rec/install.sh`) sets them the same way. `hands/run.sh uncaps` takes them back while neither the services nor the Hand Recorder is installed. `hands/run.sh uninstall` and `hands/rec/install.sh uninstall` run it after removing their own part, and `uninstall.sh` always takes them back.
- `ft-hands` runs in the `dev` container as `frametop-hands.service`. It finds and triangulates the hands, and publishes `hands` (read by ft-screens' cutouts) and `gestures` (pinches and grips, read by the pointer helper) next to the ring.
- The install leaves both off, and they don't start with SteamVR. `ft-handsctl on` starts them while SteamVR runs, and `ft-handsctl off` stops them; they also stop with SteamVR. The install links `ft-handsctl` into `~/.local/bin`. `ft-handsctl status` and `ft-handsctl log` (or `hands/run.sh status` and `log`) show how they're doing, `ft-handsctl cutouts on|off` turns just the cutouts off, and `ft-handsctl gestures` shows pinches and grips live.
- Settings in `~/.config/frametop.conf`: `HANDS_SWAP_SIDES` (`auto`, the default: ft-hands tells from the hands when some SteamVR restart has swapped the side cameras' names, and fixes them; `0` or `1` force them, and `hands/tools/check_sides.py --ring` tells which is right), `HANDS_CPUS`, the cameras it tracks with (`HANDS_CAMERAS`, `HANDS_BRIGHT`, `HANDS_BRIGHT_ON`, `HANDS_BRIGHT_OFF`, `HANDS_COLOR_LEFT`, `HANDS_COLOR_CROP`), and the pointer helper's `POINTER_HANDS`, `POINTER_PINCH_GAIN`, `POINTER_PINCH_DEADZONE`, `POINTER_GRIP_GAIN`, `POINTER_GRIP_BELOW`, `POINTER_PINCH_TYPING`, and the hand ray's `POINTER_HAND_RAY`, `POINTER_RAY_DEADZONE`, `POINTER_RAY_PUSH`, `POINTER_RAY_SCROLL`, and `POINTER_RAY_RELEASE`. The example config explains each.
- Settings in `~/.config/frametop.conf`: `HANDS_SWAP_SIDES` (`auto`, the default: ft-hands tells from the hands when some SteamVR restart has swapped the side cameras' names, and fixes them; `0` or `1` force them, and `hands/tools/check_sides.py --ring` tells which is right), `HANDS_CPUS`, the cameras it tracks with (`HANDS_CAMERAS`, `HANDS_BRIGHT`, `HANDS_BRIGHT_ON`, `HANDS_BRIGHT_OFF`, `HANDS_COLOR_LEFT`, `HANDS_COLOR_CROP`), and the pointer helper's `POINTER_HANDS`, `POINTER_PINCH_GAIN`, `POINTER_PINCH_DEADZONE`, `POINTER_GRIP_GAIN`, `POINTER_GRIP_BELOW`, `POINTER_PINCH_TYPING`, and the hand ray's `POINTER_HAND_RAY`, `POINTER_RAY_DEADZONE`, `POINTER_RAY_PUSH`, `POINTER_RAY_SCROLL`, `POINTER_RAY_RELEASE`, and `POINTER_RAY_GRACE`. The example config explains each.
Details, options, and the recording and replay tools are in [hands/README.md](../hands/README.md).
+1 -1
View File
@@ -227,7 +227,7 @@ Settings in `~/.config/frametop.conf`:
- `POINTER_GRIP_GAIN` (1): a grip moves the pointer this many times the hand's angle.
- `POINTER_GRIP_BELOW` (0.35): grips that begin more than this many metres below the eyes are ignored, because hands resting on a desk curl like a loose fist. Pinches have no such limit: deliberate ones sat 0.35-0.45 m below the eyes with the elbow resting.
- `POINTER_PINCH_TYPING` (1): no pinch begins within this many seconds of a key press, because typing touches thumb to index.
- `POINTER_HAND_RAY` (1), `POINTER_RAY_DEADZONE` (2.5 degrees the ray turns during a pinch before the press drags), `POINTER_RAY_PUSH` (1), `POINTER_RAY_SCROLL` (1; -1 turns scrolling round), `POINTER_RAY_RELEASE` (2 s after the ray goes before a pointer it woke lets go): the hand ray.
- `POINTER_HAND_RAY` (1), `POINTER_RAY_DEADZONE` (2.5 degrees the ray turns during a pinch before the press drags), `POINTER_RAY_PUSH` (1), `POINTER_RAY_SCROLL` (1; -1 turns scrolling round), `POINTER_RAY_RELEASE` (2 s after the ray goes before a pointer it woke lets go), `POINTER_RAY_GRACE` (0.5 s a hand keeps the laser after its ray drops; the other hand's ray takes it after that, or at once by pinching or raising): the hand ray.
## Recordings
+23 -10
View File
@@ -39,7 +39,10 @@
// claim, or the fingertip at an overlay)
// not a fist no grip down a grip down
// While a ray pinch is held (Hold), the ray stays up whatever the tests say, until hideFor after
// the release.
// the release. A pinch may also bring a ray up (ShowForPinch): one that hid within a moment (the
// pointer helper's grace; not one hidden for "off", "near" or a reset), or one whose show tests
// all pass but not yet for showFor. Closing the fingers turns the hand a little, so a ray near a
// hide limit can drop just as the pinch begins.
//
// Filter (RayFilter): One Euro filters (Casiez, Roussel and Vogel, CHI 2012) on the ray's yaw and
// pitch (minCutoff 0.8 Hz, beta 0.02 per deg/s, dCutoff 1 Hz) and per axis on the origin
@@ -288,14 +291,14 @@ public:
}
} else {
if (!sideIsHand) {
Hide("off");
Hide("off", f.t);
} else if (f.near && !held_) {
Hide("near");
Hide("near", f.t);
} else if (keepOk || held_ || f.t < holdUntil_) {
failSince_ = -1;
} else {
if (failSince_ < 0) failSince_ = f.t;
if (f.t - failSince_ >= p_.hideFor - 1e-9) Hide(tests_.fails.empty() ? "pose" : tests_.fails);
if (f.t - failSince_ >= p_.hideFor - 1e-9) Hide(tests_.fails.empty() ? "pose" : tests_.fails, f.t);
}
}
if (shown_) out_ = filter_(raw_, f.t);
@@ -303,15 +306,24 @@ public:
// Each tick, with the time now on the capture clock: the hand lost (no frame for lostFor) hides it.
void Tick(double now, bool sideIsHand) {
if (!shown_) return;
if (!sideIsHand) Hide("off");
else if (!held_ && now - lastFrame_ > p_.lostFor && now >= holdUntil_) Hide("lost");
if (!sideIsHand) Hide("off", now);
else if (!held_ && now - lastFrame_ > p_.lostFor && now >= holdUntil_) Hide("lost", now);
}
// A ray pinch is held: the ray stays up until hideFor after it ends.
void Hold(bool on, double t) {
if (held_ && !on) holdUntil_ = t + p_.hideFor;
held_ = on;
}
void Reset() { Hide("reset"); }
void Reset() { Hide("reset", -1e9); }
// A pinch began on this hand at t: may its ray show for it (see the top)? within: how long ago
// it may have hidden.
bool CanShowForPinch(double t, double within) const {
return shown_ || showSince_ >= 0 || (revivable_ && t - hidAt_ <= within);
}
// Show it (filter started afresh from the hand now), for a pinch that may (CanShowForPinch).
void ShowForPinch() {
if (!shown_) Show(lastFrame_);
}
bool Shown() const { return shown_; }
const Ray &Current() const { return out_; } // filtered
@@ -329,9 +341,10 @@ private:
out_ = filter_(raw_, t);
event = "show";
}
void Hide(const std::string &why) {
void Hide(const std::string &why, double t) {
if (!shown_) return;
shown_ = false, failSince_ = -1, showSince_ = -1, held_ = false, holdUntil_ = 0;
hidAt_ = t, revivable_ = why != "off" && why != "near" && why != "reset";
event = "hide " + why;
}
int side_;
@@ -342,8 +355,8 @@ private:
std::string why_;
V3 lastK_;
double lastT_ = 0, lastFrame_ = -1e9, lastStereo_ = -1e9, reach_ = 0;
double showSince_ = -1, failSince_ = -1, holdUntil_ = 0;
bool haveLast_ = false, shown_ = false, held_ = false;
double showSince_ = -1, failSince_ = -1, holdUntil_ = 0, hidAt_ = -1e9;
bool haveLast_ = false, shown_ = false, held_ = false, revivable_ = false;
};
} // namespace fhray
+230 -94
View File
@@ -286,18 +286,29 @@
// pages, overlays and Frametop's panels like a controller's laser. The model (body, shoulders, the
// pointing tests, filters) is hands/include/fh_ray.h, shared with ft-screens. Per side, from the
// hands file at each new frame; a side whose controller is in use (Handoff), hands not allowed (a
// game), or the mouse used within a second, has no ray. The first ray to show drives the pointer;
// a pinch by the other hand's ray takes it over.
// game), or the mouse used within a second, has no ray. Each side's ray follows one tracked hand
// through a brief flip of ft-hands' left/right call. Which ray drives the pointer is rayowner.h's:
// the first ray to show; its owner keeps it through POINTER_RAY_GRACE (0.5 s) of its ray being down
// (the pointer holds that ray's last pose; closing the fingers can drop a ray for a moment), and
// the other hand takes it by pinching, by raising its ray while the owner's is down, or once the
// grace runs out. Changes go to the log ("hand ray: right -> left (left pinched)"), and so do the
// owner's ray going down and coming back.
// A ray appearing wakes the pointer (role, show) and claims the laser, unless a controller that
// pressed a button within 2 s has it. A controller's trigger then takes the laser natively: our dot
// hides (rayInactive) until a ray pinch claims it back. The pose is the ray's own origin and
// direction (the beam shows, at POINTER_LASER_WIDTH), not the eye-to-cursor trick; our dot shows
// only where the ray meets a panel, and nothing catches the laser in free space.
// Gestures with the ray up are ray gestures, decided once at the gesture's begin: a pinch is the
// left button, pressed 30 ms after a claim it had to make (the claim lands in 11-15 ms), held as
// long as the pinch, so a tap clicks and a hold drags. The ray's direction holds still at the press
// until it has turned POINTER_RAY_DEADZONE (2.5 deg; the evaluation saw ray shifts of 1.3-1.6 deg
// median, 2.4-3.5 at the 90th percentile, 150 ms into a pinch), then follows at gain 1. A middle
// Gestures by a hand whose ray is up, or may come up for it (it dropped within the grace, or is on
// its way up: fh_ray.h's ShowForPinch; it then shows), are ray gestures, decided once at the
// gesture's begin. A pinch by the other hand takes the ray and presses along its own ray. A pinch
// is the left button, pressed along the ray as it was just before the fingers began to close (the
// aim lock, rayowner.h), once a pose along it has gone out and 30 ms after a claim it had to make
// (the claim lands in 11-15 ms), held as long as the pinch, so a tap clicks and a hold drags; a
// tap that ends before its press could go out clicks then (released 40 ms on). The direction holds
// still at the lock until the ray has turned POINTER_RAY_DEADZONE (2.5 deg) from where it was at
// the begin (the evaluation saw ray shifts of 1.3-1.6 deg median, 2.4-3.5 at the 90th percentile,
// 150 ms into a pinch), then follows at gain 1. Each pinch logs one line: what it did ("pinch
// right: clicked on frametop.screen.0 (right ray, aim 100 ms before, held 0.15 s)") or why not. A middle
// pinch (thumb to middle finger, fh_gestures v3) is a right click on a tap; held and moved, it
// scrolls: the virtual stick deflects with the hand's displacement from the begin (0.5 cm dead zone,
// full at 4 cm; up and down, and across the ray), times POINTER_RAY_SCROLL (1; -1 turns it round).
@@ -373,7 +384,7 @@
// POINTER_HANDS (0), POINTER_PINCH_GAIN (0.5), POINTER_PINCH_DEADZONE (1.5 deg),
// POINTER_GRIP_GAIN (1), POINTER_GRIP_BELOW (0.35 m), POINTER_PINCH_TYPING (1 s): hands, above.
// POINTER_HAND_RAY (1), POINTER_RAY_DEADZONE (2.5 deg), POINTER_RAY_PUSH (1), POINTER_RAY_SCROLL (1),
// POINTER_RAY_RELEASE (2 s): the hand ray, above.
// POINTER_RAY_RELEASE (2 s), POINTER_RAY_GRACE (0.5 s): the hand ray, above.
// POINTER_GAZE_MOUSE (precision), POINTER_HEAD_DEADZONE (0.5 deg), POINTER_KEY_TAP (0.25 s),
// POINTER_ROLE (right): gaze precision and keyboard clicks, above.
#include <openvr.h>
@@ -381,6 +392,7 @@
#include "../../hands/include/fh_ray.h"
#include "handoff.h"
#include "handsin.h"
#include "rayowner.h"
#include "vrbuttons.h"
#include "vrmath.h"
@@ -793,9 +805,9 @@ int main() {
bool handsOn = false;
double pinchGain = 0.5, pinchDeadzone = 1.5, gripGain = 1.0, handBelow = 0.35, typingHold = 1.0;
// The hand ray (see "Hand ray" at the top): POINTER_HAND_RAY, POINTER_RAY_DEADZONE, POINTER_RAY_PUSH,
// POINTER_RAY_SCROLL, POINTER_RAY_RELEASE.
// POINTER_RAY_SCROLL, POINTER_RAY_RELEASE, POINTER_RAY_GRACE.
bool rayOn = true, rayPush = true;
double rayDeadzone = 2.5, rayScroll = 1.0, rayRelease = 2.0;
double rayDeadzone = 2.5, rayScroll = 1.0, rayRelease = 2.0, rayGrace = 0.5;
// Gaze precision (see the top): POINTER_GAZE_MOUSE (precision: the mouse's left button
// holds back its press in gaze mode; direct: it clicks at once), POINTER_ROLE.
bool gazeMousePrecision = true;
@@ -840,6 +852,7 @@ int main() {
rayPush = ConfDouble(conf, "POINTER_RAY_PUSH", 1) != 0;
rayScroll = std::clamp(ConfDouble(conf, "POINTER_RAY_SCROLL", 1.0), -5.0, 5.0);
rayRelease = std::clamp(ConfDouble(conf, "POINTER_RAY_RELEASE", 2.0), 0.0, 60.0);
rayGrace = std::clamp(ConfDouble(conf, "POINTER_RAY_GRACE", 0.5), 0.0, 3.0);
const auto gm = conf.find("POINTER_GAZE_MOUSE");
gazeMousePrecision = gm == conf.end() || gm->second != "direct";
const auto mm = conf.find("POINTER_GAZE_MOUSE_MOVE");
@@ -1031,11 +1044,17 @@ int main() {
bool keyDrag = false; // gaze_right pressed while gaze_left aimed: a left press, either key ends it
double pressYaw = 0, pressPitch = 0; // the pointer at the press (a quick tap clicks there)
// A ray hold (see "Hand ray" at the top): kind 0 a pinch (the left button), 2 a middle pinch
// (right click or scroll); its press waits for the laser claim (pressAt); scrolling, and the
// pinch point in the room at the begin; push and pull (pushOn, from reach h0 and distance d0).
int rayKind = 0;
bool pendingPress = false, scrolling = false, pushOn = false;
Clock::time_point pressAt{};
// (right click or scroll); its press waits for the laser claim (pressAt) and for a pose along
// the locked aim to have gone out (aimSent); scrolling, and the pinch point in the room at the
// begin; push and pull (pushOn, from reach h0 and distance d0). raySlot: the ray's side (the
// ray that follows the pinching hand; side is the gesture's). released: the pinch ended before
// its press went out, which then clicks (released at releaseAt). For the log: took (it took
// the ray from the other hand), lockAge (the aim's age at the begin, s; < 0 none), the hit.
int rayKind = 0, raySlot = -1;
bool pendingPress = false, scrolling = false, pushOn = false, aimSent = false, released = false, took = false;
Clock::time_point pressAt{}, releaseAt{}, beganAt{};
double lockAge = -1;
std::string pressHit;
Vec3 beginPoint;
double h0 = 0, d0 = 0, scrollX = 0, scrollY = 0;
Vec3 origin;
@@ -1087,6 +1106,12 @@ int main() {
// pointer, and what happened to the laser.
fhray::Body body;
fhray::HandRay rays[2] = {fhray::HandRay(0), fhray::HandRay(1)};
// Which ray drives the pointer, which hand each ray follows, and each ray's recent aims for a
// pinch's aim lock (rayowner.h); rayHid: why each ray last hid, for the log.
rayown::Owner rayOwner;
rayown::Slots raySlots;
rayown::AimHistory rayAims[2];
std::string rayHid[2];
bool rayInactive = false; // a controller took the laser: our dot hides until a pinch claims
bool rayWoke = false; // the ray woke the pointer (it goes again when the ray has gone a while)
Clock::time_point rayGoneAt{}; // when the last ray went
@@ -1102,7 +1127,6 @@ int main() {
};
std::vector<PendingBegin> pendingBegins;
bool gripDown[2] = {false, false}; // a grip is down on that side (the gestures file, last read)
Clock::time_point rayShownAt[2] = {}; // when each side's ray last showed (the first one keeps the pointer)
bool rayHoldCancel = false; // the driving ray went mid-hold: the hold ends, as lost
// Let go of the virtual controller (its role and laser), as the other releases in the main loop do.
auto releasePointer = [&](const char *why) {
@@ -1139,13 +1163,9 @@ int main() {
} else if (!laserOurs() && !controllerBusy(t) && !claimPending && !claimHeld) {
claimPending = true, claimAt = t;
}
if (debug) std::printf("ray %s drives the pointer\n", s ? "right" : "left");
if (debug) std::fflush(stdout);
};
auto rayLeave = [&](bool controller) {
if (raySide < 0) return;
if (debug) std::printf("ray %s let go of the pointer%s\n", raySide ? "right" : "left", controller ? " (controller)" : "");
if (debug) std::fflush(stdout);
raySide = -1;
rayGoneAt = Clock::now();
rayHoldCancel = true;
@@ -1154,6 +1174,19 @@ int main() {
// as when a controller moves (last used wins), unless the mouse is in use
if (controller && active && Clock::now() - lastMouse > std::chrono::seconds(1)) releasePointer("hand ray off (a controller, or a game)");
};
// The ray owner's changes (rayowner.h), to the log, one line each; apply: the pointer follows
// (rayEnter, rayLeave).
auto ownerChanges = [&](bool apply) {
for (const std::string &l : rayOwner.log) std::printf("%s\n", l.c_str());
for (const rayown::Change &c : rayOwner.changes) {
std::printf("hand ray: %s -> %s (%s)\n", rayown::SideName(c.from), rayown::SideName(c.to), c.why.c_str());
if (!apply) continue;
if (c.to >= 0) rayEnter(c.to);
else rayLeave(c.off);
}
if (!rayOwner.log.empty() || !rayOwner.changes.empty()) std::fflush(stdout);
rayOwner.log.clear(), rayOwner.changes.clear();
};
// Gaze mode outside games, and its dot (see the top): lastMove/lastHeld/pulseAt.
bool inGame = false, gazeAwake = false, gameSent = false;
Clock::time_point inGameAt{}, gazeAwakeAt{}, gameSentAt{};
@@ -1482,6 +1515,8 @@ int main() {
if (raySide >= 0 || rays[0].Shown() || rays[1].Shown()) {
rays[0].Reset(), rays[1].Reset();
rays[0].event.clear(), rays[1].event.clear();
rayOwner.Drop("the mouse");
ownerChanges(false);
if (raySide >= 0) raySide = -1, rayGoneAt = Clock::now(), rayHoldCancel = true;
}
rayWoke = false;
@@ -2007,11 +2042,14 @@ int main() {
const double tc = double(handsRoom.capture_ns) / 1e9;
const Vec3 hp = Position(capHead), hf = Rotate(capHead, Vec3{0, 0, -1});
body.Update(tc, {hp.x, hp.y, hp.z}, {hf.x, hf.y, hf.z});
// the hand each side's ray follows: ft-hands' side call, held through a brief flip (rayowner.h)
std::vector<rayown::SeenHand> seenHands;
for (const hin::RoomHand &c : handsRoom.hands) seenHands.push_back({c.id, c.side});
int pick[2];
raySlots.Update(tc, seenHands, pick);
for (int s = 0; s < 2; ++s) {
const hin::RoomHand *h = nullptr;
for (const hin::RoomHand &c : handsRoom.hands)
if (c.side == s) h = &c;
if (!h) continue;
if (pick[s] < 0) continue;
const hin::RoomHand *h = &handsRoom.hands[size_t(pick[s])];
auto fv = [](Vec3 v) { return fhray::V3{v.x, v.y, v.z}; };
fhray::HandFrame f;
f.t = tc, f.stereo = h->stereo;
@@ -2038,33 +2076,36 @@ int main() {
}
}
rays[s].Frame(body, f, mayPoint(s));
rayAims[s].Add(tc, rays[s].Current(), rays[s].Shown(), h->pinchD[0]);
}
}
for (int s = 0; s < 2; ++s) {
rays[s].Tick(secsOf(tnow), mayPoint(s));
if (rays[s].event.empty()) continue;
if (rays[s].event == "show") rayShownAt[s] = tnow;
if (rays[s].event.rfind("hide ", 0) == 0) rayHid[s] = rays[s].event.substr(5);
if (debug) {
const fhray::Tests &ts = rays[s].LastTests();
std::printf("ray %s %s (below %.2f m, ahead %.2f m, palm %.2f, pitch %.0f, yaw off %.0f)\n",
s ? "right" : "left", rays[s].event.c_str(), ts.below, ts.ahead, ts.palm, ts.pitch, ts.yawOff);
std::fflush(stdout);
}
// "hide off": its side has a controller in use, or hands aren't allowed now (a VR game)
if (raySide == s && !rays[s].Shown()) rayLeave(rays[s].event == "hide off");
rays[s].event.clear();
}
if (raySide < 0) {
int pick = -1;
for (int s = 0; s < 2; ++s)
if (rays[s].Shown() && (pick < 0 || rayShownAt[s] < rayShownAt[pick])) pick = s;
if (pick >= 0) rayEnter(pick);
}
// Which ray drives the pointer: the owner keeps it through a grace after its ray goes down
// (rayowner.h); one whose side may not point (a controller in use, a VR game) leaves at once.
rayown::Params op;
op.grace = rayGrace;
rayOwner.SetParams(op);
const bool shown[2] = {rays[0].Shown(), rays[1].Shown()}, may[2] = {mayPoint(0), mayPoint(1)};
rayOwner.Update(secsOf(tnow), shown, may, rayHid);
ownerChanges(true);
if (raySide >= 0 && !active && !headsetOff) { // a wake that was held off (just released): again
wake(tnow);
rayWoke = active;
}
} else if (raySide >= 0) {
} else if (raySide >= 0 || rayOwner.Side() >= 0) {
rayOwner.Drop("hand input off");
ownerChanges(false);
rayLeave(false);
}
// The ray woke the pointer and has been gone a while (no mouse, no gaze meanwhile): release it.
@@ -2142,18 +2183,31 @@ int main() {
hold.lastYaw = yaw, hold.lastPitch = pitch;
lastMove = tnow; // the dot shows while it moves (gaze mode)
};
// One line per pinch (index or middle) in the log: what it did (clicked where), or why not.
auto pinchLog = [&](int side, int kind, const std::string &what) {
std::printf("%s %s: %s\n", kind == 2 ? "middle pinch" : "pinch", side ? "right" : "left", what.c_str());
std::fflush(stdout);
};
// A pinch held with no ray up (see "Hands" at the top); its log line says so.
auto beginHold = [&](int side, bool grip, const fh_pinch_t &g) {
handUsed = tnow;
if (hold.src != Src::None) {
// A grip takes over a pinch on the way to it (closing the hand passes through
// one); otherwise one hold at a time.
if (hold.src != Src::Hand || !grip || hold.grip) return;
if (hold.src != Src::Hand || !grip || hold.grip) {
if (!grip) pinchLog(side, 0, "ignored (no ray up; a press is already held)");
return;
}
aimHeld = aimHand = false;
hold = {};
}
if (leftHeld || aimHeld || clickPress || clickRelease) return; // the mouse's button is busy
if (leftHeld || aimHeld || clickPress || clickRelease) { // the mouse's button is busy
if (!grip) pinchLog(side, 0, "ignored (no ray up; the button is held)");
return;
}
if (!active) {
wake(tnow); // the first gesture only wakes the pointer, like the first mouse move
if (!grip) pinchLog(side, 0, "no ray up: it woke the pointer, no click");
return;
}
vr::HmdMatrix34_t head;
@@ -2163,7 +2217,7 @@ int main() {
// puts them straight ahead in the head's frame, where ft-hands can't tell).
const Vec3 at = Position(head) + Rotate(head, Vec3{g.begin_point[0], g.begin_point[1], g.begin_point[2]});
if (!grip && tnow - lastTyping < std::chrono::duration<double>(typingHold)) {
if (debug) std::printf("hand pinch ignored: typing\n");
pinchLog(side, 0, "ignored (no ray up; typing)");
return;
}
if (grip && Position(head).y - at.y > handBelow) {
@@ -2177,6 +2231,9 @@ int main() {
// A grip, or a pinch without gaze mode: a real press where the pointer is (where
// you look, in gaze mode), dragging with the hand until it opens.
startHold(grip || !gazeOn);
if (!grip)
pinchLog(side, 0, gazeOn ? "no ray up: held back at the gaze, clicks where the pointer is at the release"
: "no ray up: a press where the pointer is, on " + (lastHit.empty() ? std::string("nothing") : lastHit));
if (debug) std::printf("hand %s %s began\n", side ? "right" : "left", grip ? "grip" : "pinch");
if (debug) std::fflush(stdout);
};
@@ -2260,7 +2317,10 @@ int main() {
keyPresses.clear();
if (calOpened) { // the calibration panel came up: a press in progress ends, no click
calOpened = false;
if (hold.src == Src::Ray) rays[hold.side].Hold(false, secsOf(tnow)), SendTo(out, "ft_pointer", "scroll 0 0");
if (hold.src == Src::Ray) {
pinchLog(hold.side, hold.rayKind, "no click (the gaze calibration panel came up)");
rays[hold.raySlot].Hold(false, secsOf(tnow)), SendTo(out, "ft_pointer", "scroll 0 0");
}
if (hold.src != Src::None) endHold(true);
if (leftHeld) releaseLeft();
aimHeld = aimHand = clickPress = false;
@@ -2278,6 +2338,7 @@ int main() {
}
fh_gestures_t hg;
const bool handOk = handsOn && handFile.Read(hg);
const char *const kLostPinch = "ft-hands lost the hand, or a grip took the pinch";
// A ray hold (see "Hand ray" at the top): the pinch's press goes out once the laser claim
// has landed; a middle pinch is a right click (a tap) or scrolling (held and moved).
auto rayRoom = [&](const float p[3], uint64_t t_ns, Vec3 &out) {
@@ -2286,14 +2347,34 @@ int main() {
out = hin::ToRoom(head, p);
return true;
};
auto beginRayHold = [&](int side, int kind, const fh_pinch_t &g) {
// A ray pinch (see "Hand ray" at the top). side: the gesture's; rs: the ray's (the one that follows
// the pinching hand). The aim is locked where the ray pointed just before the fingers began to
// close (rayowner.h), and the dead zone counts from the ray now.
auto beginRayHold = [&](int side, int rs, int kind, const fh_pinch_t &g) {
handUsed = tnow;
if (hold.src != Src::None || leftHeld || aimHeld || clickPress || clickRelease) return;
if (raySide != side) rayEnter(side); // a pinch by the other hand claims the pointer
if (hold.src != Src::None || leftHeld || aimHeld || clickPress || clickRelease) {
pinchLog(side, kind, "ignored (a press is already held)");
return;
}
const bool took = rayOwner.Pinched(rs, true) == rayown::Owner::Pinch::Take;
rays[rs].ShowForPinch(); // a ray that dropped as the fingers closed comes back
rays[rs].event.clear();
ownerChanges(true); // a pinch by the other hand takes the ray (rayEnter)
Hold h;
h.src = Src::Ray, h.side = side, h.rayKind = kind, h.pressed = false;
h.src = Src::Ray, h.side = side, h.raySlot = rs, h.rayKind = kind, h.pressed = false;
h.took = took, h.beganAt = tnow;
auto angles = [](const fhray::V3 &d, double &y, double &p) {
const Vec3 n = Normalize({d.x, d.y, d.z});
y = std::atan2(-n.x, -n.z) * 180 / M_PI;
p = std::clamp(std::asin(std::clamp(n.y, -1.0, 1.0)) * 180 / M_PI, -85.0, 85.0);
};
angles(rays[rs].Current().dir, h.refYaw, h.refPitch);
fhray::Ray lock;
if (rayAims[rs].Lock(double(g.begin_ns) / 1e9, handOk && hg.end_m > 0 ? hg.end_m : 0.035, lock, h.lockAge))
angles(lock.dir, yaw, pitch);
else
yaw = h.refYaw, pitch = h.refPitch, h.lockAge = -1;
h.startYaw = h.lastYaw = yaw, h.startPitch = h.lastPitch = pitch;
h.refYaw = yaw, h.refPitch = pitch;
if (!rayRoom(g.begin_point, g.begin_ns, h.beginPoint)) h.beginPoint = rayOrigin;
// the laser: a pinch always claims it (if it isn't ours), and the press waits 30 ms for it
rayInactive = false;
@@ -2302,35 +2383,62 @@ int main() {
h.pendingPress = kind == 0;
h.pressAt = tnow + std::chrono::milliseconds(ours_ ? 0 : 30 + (claimHeld ? 0 : 8));
hold = h;
rays[side].Hold(true, secsOf(tnow));
if (debug) std::printf("ray %s %s began\n", side ? "right" : "left", kind ? "middle pinch" : "pinch");
rays[rs].Hold(true, secsOf(tnow));
if (debug) std::printf("ray %s %s began\n", rs ? "right" : "left", kind ? "middle pinch" : "pinch");
if (debug) std::fflush(stdout);
};
auto endRayHold = [&](bool lost) {
const int side = hold.side;
if (hold.rayKind == 0) {
if (hold.pendingPress && !lost) clickPress = true; // a tap before the press went out: a click
else if (hold.pressed && leftHeld) releaseLeft();
} else {
if (hold.scrolling) SendTo(out, "ft_pointer", "scroll 0 0");
else if (!lost) pressRight = clickPress = true; // a middle tap: a right click, where the ray is
}
if (debug)
std::printf("ray %s %s %s\n", side ? "right" : "left", hold.rayKind ? "middle pinch" : "pinch",
lost ? "lost" : hold.scrolling ? "scrolled" : "released");
if (debug) std::fflush(stdout);
// What a ray pinch did, for its log line.
auto rayPinchSays = [&](const char *what) {
char b[256];
const double held = std::chrono::duration<double>(tnow - hold.beganAt).count();
char aim[48] = "aim now";
if (hold.lockAge >= 0) std::snprintf(aim, sizeof aim, "aim %.0f ms before", hold.lockAge * 1000);
std::snprintf(b, sizeof b, "%s (%s ray%s, %s, held %.2f s)", what, hold.raySlot ? "right" : "left",
hold.took ? ", took it" : "", aim, held);
return std::string(b);
};
auto hitName = [&](const std::string &k) { return k.empty() ? std::string("nothing") : k; };
auto finishRayHold = [&] {
const int rs = hold.raySlot;
hold = {};
if (side >= 0) rays[side].Hold(false, secsOf(tnow));
if (rs >= 0) rays[rs].Hold(false, secsOf(tnow));
};
// A ray pinch ended (lost: by losing the hand, a grip, or the ray; why says which).
auto endRayHold = [&](bool lost, const char *why) {
const int side = hold.side, kind = hold.rayKind;
if (!lost && !hold.pressed && (kind == 0 || !hold.scrolling)) {
// a tap before its press went out (or a middle tap): it clicks once the press can go
// (the press block below), along the locked aim
hold.released = hold.pendingPress = true;
return;
}
if (kind == 0 && hold.pressed && leftHeld) releaseLeft();
if (kind == 2 && hold.scrolling) SendTo(out, "ft_pointer", "scroll 0 0");
std::string what;
if (kind == 2 && hold.scrolling) what = "scrolled";
else if (!hold.pressed) what = std::string("no click: ") + why;
else if (lost) what = "pressed on " + hitName(hold.pressHit) + ", let go: " + why;
else what = (hold.engaged ? "dragged from " : "clicked on ") + hitName(hold.pressHit);
pinchLog(side, kind, rayPinchSays(what.c_str()));
finishRayHold();
};
// A gesture that began: by the ray, held back like the gaze (beginHold), or nothing.
// rayFor: the ray that follows the gesture's hand (rayowner.h's Slots), else its side's.
auto rayFor = [&](int s, const fh_pinch_t &g) {
const int r = raySlots.SideOf(g.hand_id);
return r >= 0 ? r : s;
};
auto canRay = [&](int rs) { return rays[rs].CanShowForPinch(secsOf(tnow), rayGrace); };
auto startGesture = [&](int s, int kind, const fh_pinch_t &g, bool ray) {
ray = ray && rays[s].Shown(); // (a ray that went while ft-screens was asked: not a ray gesture)
const int rs = rayFor(s, g);
// (and still: a ray that went for good while ft-screens was asked isn't; a grip needs it up)
ray = ray && (kind == 1 ? rays[rs].Shown() : canRay(rs));
if (ray && kind != 1) {
beginRayHold(s, kind, g);
beginRayHold(s, rs, kind, g);
} else if (ray || kind == 2) {
// a fist with the ray up, or a middle pinch without it: nothing
if (debug) std::printf("hand %s %s ignored (%s)\n", s ? "right" : "left", kind == 1 ? "grip" : "middle pinch",
ray ? "the ray is up" : "no ray");
if (kind == 2) pinchLog(s, kind, "ignored (no ray up)");
else if (debug) std::printf("hand %s grip ignored (the ray is up)\n", s ? "right" : "left");
} else {
beginHold(s, kind == 1, g);
}
@@ -2340,13 +2448,13 @@ int main() {
if (hold.src == Src::Ray) return hold.rayKind == kind;
return hold.src == Src::Hand && hold.grip == (kind == 1);
};
auto endGesture = [&](bool lost) {
if (hold.src == Src::Ray) endRayHold(lost);
auto endGesture = [&](bool lost, const char *why) {
if (hold.src == Src::Ray) endRayHold(lost, why);
else endHold(lost);
};
if (rayHoldCancel) { // the driving ray went (a controller, the hand lost): its hold ends, as lost
if (rayHoldCancel) { // the driving ray went (a controller, the mouse, hand input off): its hold ends, as lost
rayHoldCancel = false;
if (hold.src == Src::Ray) endRayHold(true);
if (hold.src == Src::Ray) endRayHold(true, "the ray went (a controller, the mouse, or hand input off)");
}
if (handOk && (hg.seq != handSeq || handFile.opens != handOpens)) {
handSeq = hg.seq;
@@ -2360,7 +2468,7 @@ int main() {
for (int s = 0; s < 2; ++s)
rebase = rebase || slots[k][s].begins < seenBegins[k][s] || slots[k][s].ends < seenEnds[k][s];
if (rebase) {
if (hold.src == Src::Hand || hold.src == Src::Ray) endGesture(true);
if (hold.src == Src::Hand || hold.src == Src::Ray) endGesture(true, "ft-hands started over");
pendingBegins.clear();
for (int k = 0; k < 3; ++k)
for (int s = 0; s < 2; ++s) seenBegins[k][s] = slots[k][s].begins, seenEnds[k][s] = slots[k][s].ends;
@@ -2377,10 +2485,14 @@ int main() {
const bool began = g.begins != seenBegins[k][s], ended = g.ends != seenEnds[k][s];
const bool lost = g.flags & FH_PINCH_LOST;
seenBegins[k][s] = g.begins, seenEnds[k][s] = g.ends;
if (holdingKind(s, k) && ended) endGesture(lost); // the one held ended (another may have begun)
if (holdingKind(s, k) && ended) endGesture(lost, kLostPinch); // the one held ended (another may have begun)
for (PendingBegin &p : pendingBegins)
if (p.side == s && p.kind == k && ended) p.ended = true, p.lost = lost;
if (!began || !allowed) continue;
if (!began) continue;
if (!allowed) {
if (k != 1) pinchLog(s, k, "ignored (hands not allowed: a VR game, or the headset off)");
continue;
}
// Not on a side that has a controller in use, and not by a hand at a controller
// (reaching for it or holding it: by position, whatever ft-hands calls its side).
bool atController = arbiter.IsController(s);
@@ -2388,25 +2500,30 @@ int main() {
if (!atController && rayRoom(g.begin_point, g.begin_ns, at))
atController = arbiter.NearController({at.x, at.y, at.z});
if (atController) {
if (debug) std::printf("hand %s %s ignored: at a controller\n", s ? "right" : "left",
k == 1 ? "grip" : k ? "middle pinch" : "pinch");
if (k != 1) pinchLog(s, k, "ignored (at a controller)");
else if (debug) std::printf("hand %s grip ignored: at a controller\n", s ? "right" : "left");
continue;
}
// Ray or gaze is decided once, here, from the state at the begin.
const bool ray = raySide >= 0 ? rays[s].Shown() : rays[s].Shown() && active;
// Ray or gaze is decided once, here, from the state at the begin: a ray pinch when the
// pinching hand's ray is up, or may come up for it (fh_ray.h's ShowForPinch).
const bool ray = canRay(rayFor(s, g));
if (screensNear && nearNs < g.begin_ns) {
PendingBegin p{s, k, g, ray, tnow + std::chrono::milliseconds(60)};
p.ended = !(g.flags & FH_PINCH_DOWN), p.lost = lost;
pendingBegins.push_back(p);
continue;
}
if (screensNear && nearSide[s]) continue; // ft-screens' (a grab or a poke)
if (screensNear && nearSide[s]) { // ft-screens' (a grab or a poke)
if (k != 1) pinchLog(s, k, "left to ft-screens (the hand is near its panel)");
continue;
}
startGesture(s, k, g, ray);
// begun and ended since the last read (a quick tap): its release too
if (!(g.flags & FH_PINCH_DOWN) && holdingKind(s, k)) endGesture(lost);
if (!(g.flags & FH_PINCH_DOWN) && holdingKind(s, k)) endGesture(lost, kLostPinch);
}
// A side that turns controller mid-gesture: the gesture ends, as lost.
if ((hold.src == Src::Hand || hold.src == Src::Ray) && arbiter.IsController(hold.side)) endGesture(true);
if ((hold.src == Src::Hand || hold.src == Src::Ray) && arbiter.IsController(hold.side))
endGesture(true, "its side has a controller in use");
// The held gesture's hand moves the pointer: past the dead zone (a tap's jitter,
// the pinch point shifting as the fingers close), then at the gain, from there.
if (hold.src == Src::Hand) {
@@ -2452,43 +2569,61 @@ int main() {
const PendingBegin q = p;
pendingBegins.erase(pendingBegins.begin() + long(i));
if (reported && nearSide[q.side]) {
if (debug) std::printf("hand %s gesture left to ft-screens (near)\n", q.side ? "right" : "left");
if (q.kind != 1) pinchLog(q.side, q.kind, "left to ft-screens (the hand is near its panel)");
else if (debug) std::printf("hand %s grip left to ft-screens (near)\n", q.side ? "right" : "left");
continue;
}
startGesture(q.side, q.kind, q.g, q.ray);
if (q.ended && holdingKind(q.side, q.kind)) endGesture(q.lost);
if (q.ended && holdingKind(q.side, q.kind)) endGesture(q.lost, kLostPinch);
}
// A ray pinch's press, once the claim has had its 30 ms.
// A ray pinch's press, once the claim has had its 30 ms and a pose along the locked aim has gone
// out (so the press lands there, and lastHit is what it hits).
// (and once a pointer the ray just woke has been bound: the wake's claim comes 300 ms on)
if (hold.src == Src::Ray && hold.pendingPress && tnow >= hold.pressAt && active &&
// A pinch that already ended (a tap, or a middle tap: the right button) clicks: released 40 ms on.
if (hold.src == Src::Ray && hold.pendingPress && hold.aimSent && tnow >= hold.pressAt && active &&
tnow - wokeAt > std::chrono::milliseconds(350)) {
hold.pendingPress = false;
hold.pressed = true;
gazeBack = false;
pressRight = hold.rayKind == 2;
pressLeft();
// push and pull: on a carry (a press on no panel's content: a grab bar, a scene-graph
// control, an edge, one of ft-screens' controls); a panel that starts to move turns it on below
hold.h0 = rays[hold.side].Reach(), hold.d0 = lastDistance;
hold.pushOn = rayPush && (lastHit.empty() || pass.onEdge || pass.scene || (lastHit.rfind("frametop.", 0) == 0 && !FramePanel(lastHit)));
hold.pressHit = lastHit;
if (hold.released) {
hold.releaseAt = tnow + std::chrono::milliseconds(40);
} else {
// push and pull: on a carry (a press on no panel's content: a grab bar, a scene-graph
// control, an edge, one of ft-screens' controls); a panel that starts to move turns it on below
hold.h0 = rays[hold.raySlot].Reach(), hold.d0 = lastDistance;
hold.pushOn = rayPush && (lastHit.empty() || pass.onEdge || pass.scene || (lastHit.rfind("frametop.", 0) == 0 && !FramePanel(lastHit)));
}
}
if (hold.src == Src::Ray && hold.released && hold.pressed && tnow >= hold.releaseAt) {
if (leftHeld) releaseLeft();
pinchLog(hold.side, hold.rayKind,
rayPinchSays(((hold.rayKind == 2 ? "right click on " : "clicked on ") + hitName(hold.pressHit)).c_str()));
finishRayHold();
}
// ft-hands stopped (or hung) with a gesture held: it's over, as lost.
const uint64_t nowNs =
uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(tnow.time_since_epoch()).count());
if ((hold.src == Src::Hand || hold.src == Src::Ray) && (!handOk || !active || nowNs - handPublished > 1'500'000'000ull))
endGesture(true);
endGesture(true, "hand tracking stopped, or the pointer went");
if (hold.src != Src::None && hold.src != Src::Hand && hold.src != Src::Ray && !active) endHold(true);
// The driving ray (see "Hand ray" at the top): the pointer's origin and direction. During a ray
// pinch the direction holds still until the ray has turned rayDeadzone, then follows (steer);
// a middle pinch holds it. Push and pull move the origin along the ray while a carry is held.
// rayMode: a hand's ray has the pointer, driving it or (its owner's grace, rayowner.h) holding
// its last pose.
const bool rayDrives = active && raySide >= 0 && rays[raySide].Shown();
const bool rayMode = active && raySide >= 0;
if (rayDrives) {
const fhray::Ray &r = rays[raySide].Current();
rayOrigin = {r.origin.x, r.origin.y, r.origin.z};
rayDir = Normalize({r.dir.x, r.dir.y, r.dir.z});
const double ry = std::atan2(-rayDir.x, -rayDir.z) * 180 / M_PI;
const double rp = std::asin(std::clamp(rayDir.y, -1.0, 1.0)) * 180 / M_PI;
if (hold.src == Src::Ray && hold.side == raySide) {
if (hold.src == Src::Ray && hold.raySlot == raySide) {
if (hold.rayKind == 0) steer(ry, rp, rayDeadzone, 1.0);
} else {
yaw = ry, pitch = std::clamp(rp, -85.0, 85.0);
@@ -2522,7 +2657,7 @@ int main() {
// Head follow (see the top): past the leash (for the delay), ease the reference to the
// head's facing, and turn the cursor with it. Not in gaze mode: the gaze places it.
if (follow && !gazeOn && active && anchored && hmd.bPoseIsValid && !rayDrives) {
if (follow && !gazeOn && active && anchored && hmd.bPoseIsValid && !rayMode) {
const Vec3 head = LimitPitch(Vec3{-hm[0][2], -hm[1][2], -hm[2][2]}, 85);
if (followReset) {
followRef = head, followLag = 0, leashOutSince = {};
@@ -2747,7 +2882,7 @@ int main() {
// depths). Anything in front of the point on the eye's line of sight wins.
// Not for a hand's ray: like a controller's, it lands where it points.
const double toPoint = std::sqrt(Dot(point - eye, point - eye));
const Hit front = rayDrives ? Hit{} : nearest(eye, Normalize(point - eye));
const Hit front = rayMode ? Hit{} : nearest(eye, Normalize(point - eye));
if (front.along < toPoint - 0.02) {
occluded = true;
bestKey = front.key, bestScene = front.scene;
@@ -2765,8 +2900,8 @@ int main() {
const bool onScene = !dragging && ((bestScene && best < 1e8) || onEdge);
const bool onPanel = dragging || (best < 1e8 && !onScene);
const Vec3 sight = Normalize(point - eye);
if (rayDrives) onVrSettings = false; // the beam starts at the hand: SteamVR finds the page itself
if (!dragging && !reuse && !rayDrives) {
if (rayMode) onVrSettings = false; // the beam starts at the hand: SteamVR finds the page itself
if (!dragging && !reuse && !rayMode) {
// SteamVR Settings (see the top): the dashboard's main panel is hidden and its
// scene-graph panel shows the page.
onVrSettings = false;
@@ -2791,7 +2926,7 @@ int main() {
catcherHidesHit = onVrSettings;
}
if (rayDrives) {
if (rayMode) {
// A hand's ray (see "Hand ray" at the top): the beam shows, as a controller's does, and
// our dot only where it meets a panel. No laser-catching dot in free space: the beam just
// ends. None while a controller has taken the laser (rayInactive).
@@ -2851,14 +2986,14 @@ int main() {
// Controller ray: from the eye, aimed at the cursor point, converted from the
// standing universe to raw tracking space via the HMD's pose in both.
// A hand's ray: from its origin along its direction (push and pull already in the anchor).
const Vec3 aimStanding = rayDrives ? dir : Normalize(point - eye);
const Vec3 aimStanding = rayMode ? dir : Normalize(point - eye);
const auto &S = hmd.mDeviceToAbsoluteTracking, &R = hmdRaw.mDeviceToAbsoluteTracking;
const Vec3 aim = Rotate(R, RotateInverse(S, aimStanding)); // raw <- head <- standing
// Origin partway along the line of sight to the cursor (smaller hit dot).
const double toPoint = std::sqrt(Dot(point - eye, point - eye));
double originDist = std::max(0.0, std::min(toPoint * originFraction, toPoint - originMargin));
if (onVrSettings) originDist = std::min(originDist, SETTINGS_ORIGIN); // SteamVR finds the page
const Vec3 originStanding = rayDrives ? anchor : eye + Normalize(point - eye) * originDist;
const Vec3 originStanding = rayMode ? anchor : eye + Normalize(point - eye) * originDist;
const Vec3 eyeRaw = Position(R) + Rotate(R, RotateInverse(S, originStanding - eye));
lastPoint = point, lastOrigin = originStanding, lastAim = aimStanding; // tilt starts from here
havePoint = true;
@@ -2901,6 +3036,7 @@ int main() {
SendTo(out, "ft_pointer", msg);
posed = {true, tnow, eyeRaw, ayaw, apitch};
}
if (hold.src == Src::Ray) hold.aimSent = true; // its press can go out next frame (see above)
}
// A held-back press (see the top): held still long enough, it's a real press (a drag);
+263
View File
@@ -0,0 +1,263 @@
// Which bare hand's ray drives the pointer, which tracked hand each ray follows, and where a ray
// pinch clicks. The pointer helper's side of the hand ray ("Hand ray" in ft-pointer.cpp; the ray
// itself is hands/include/fh_ray.h). Header-only and free of OpenVR, so pointer/tests/rayowner-test.cpp
// can drive it with made-up timelines.
//
// Owner: one ray drives the pointer (one SteamVR laser). With none, the first ray to show takes it.
// The owner keeps it while its ray is up, and through a grace (0.5 s, POINTER_RAY_GRACE) after its
// ray goes down: the pointer holds the ray's last pose meanwhile, and the ray coming back carries on.
// Closing the fingers turns the hand, and a ray near a limit of fh_ray's gate drops for a moment;
// without the grace the other hand's ray took the laser at once and kept it (the laser "jumping
// between hands", live test 2026-10-09). The other hand takes over:
// - when it pinches with its ray up (or able to come up, fh_ray's ShowForPinch): last used wins;
// - when its ray shows while the owner's is down (a deliberate raise; a ray that was up all along
// doesn't count);
// - when the owner's ray has been down for the grace and its ray is up.
// The owner leaves at once when its side may not point (its controller in use, a game, the mouse) or
// its hand went near a panel (fh_ray's "near": ft-screens' poke and grab, no frozen beam meanwhile),
// and after the grace when no other ray is up.
//
// Pinch: a pinch on the owner's hand is a ray pinch (its ray brought back if it dropped within the
// grace). A pinch on the other hand is a ray pinch when that hand's ray is up or can come up: it
// takes the ray, and the click is where its own ray pointed (not where the old owner's did).
// Otherwise it isn't one (the helper's gaze-style hold).
//
// Slots: which of ft-hands' hands each side's ray follows. ft-hands calls each hand left or right
// per frame, and a call can flip for a few frames (or a pair swap): following the call, a ray's
// knuckle point jumped to the other hand and back. A ray keeps the hand it follows (by id) until
// ft-hands has called it the other side for flipHold (0.3 s), or it's gone; then it takes the hand
// called its side that the other ray doesn't follow.
//
// Aim lock: where a ray pinch clicks. ft-hands sees a pinch once the thumb and index tips are
// within begin_m, and closing them turns the hand (1.3-1.6 deg median in the first 150 ms after the
// begin, 2.4-3.5 at the 90th percentile, on recordings; more before it). So the press goes out
// along the ray as it was just before the fingers began to close: the newest frame, within lockBack
// (0.35 s) before the begin, with the tips still lockOpen (1 cm) further apart than the pinch's
// release distance (end_m, 3.5 cm); without a distance (no skeleton) or with fingers resting nearly
// closed, the frame lockLead (0.1 s) before the begin. Only frames with the ray up count. The rays
// here are fh_ray's filtered ones, what the beam showed.
//
// Times are seconds on one steady clock (CLOCK_MONOTONIC: ft-hands' capture times as seconds).
#pragma once
#include "../../hands/include/fh_ray.h"
#include <cstdint>
#include <cstdio>
#include <deque>
#include <string>
#include <vector>
namespace rayown {
inline const char *SideName(int s) { return s < 0 ? "none" : s ? "right" : "left"; }
struct Params {
double grace = 0.5; // the owner's ray down this long: it's gone
double flipHold = 0.3; // a hand called the other side this long moves to that side's ray
double lockBack = 0.35; // aim lock: how far back before a pinch's begin to look
double lockLead = 0.10; // aim lock: the frame this long before the begin, without a distance
double lockOpen = 0.01; // aim lock: the tips this much further apart than end_m: still open
};
// A change of owner: from, to (-1 none), why, and off (the old owner's side may not point: the
// pointer goes back at once, see rayLeave in ft-pointer.cpp).
struct Change {
int from, to;
std::string why;
bool off = false;
};
class Owner {
public:
explicit Owner(Params p = {}) : p_(p) {}
void SetParams(const Params &p) { p_ = p; }
int Side() const { return side_; }
// The owner's ray is down, within the grace: the pointer holds its last pose.
bool Waiting() const { return side_ >= 0 && downSince_ >= 0; }
// Each tick. shown: each side's ray is up; may: each side may point at all (fh_ray's sideIsHand);
// why: why each ray is down, for the log (fh_ray's hide reason).
void Update(double t, const bool shown[2], const bool may[2], const std::string why[2]) {
for (int s = 0; s < 2; ++s) {
if (shown[s] && !was_[s]) shownAt_[s] = t;
was_[s] = shown[s];
}
if (side_ < 0) {
int pick = -1;
for (int s = 0; s < 2; ++s)
if (shown[s] && (pick < 0 || shownAt_[s] < shownAt_[pick])) pick = s;
if (pick >= 0) Set(pick, "raised", false);
return;
}
const int o = side_, other = 1 - o;
if (!may[o]) {
Set(-1, "off: its controller, a game, or the mouse", true);
return;
}
if (!shown[o] && why[o] == "near") {
Set(-1, "near a panel", false);
return;
}
if (shown[o]) {
if (downSince_ >= 0) {
char b[96];
std::snprintf(b, sizeof b, "hand ray %s: back after %.2f s", SideName(o), t - downSince_);
log.push_back(b);
}
downSince_ = -1;
return;
}
if (downSince_ < 0) {
downSince_ = t, downWhy_ = why[o].empty() ? "pose" : why[o];
log.push_back(std::string("hand ray ") + SideName(o) + ": down (" + downWhy_ + ")");
}
if (shown[other] && shownAt_[other] > downSince_) {
Set(other, std::string(SideName(other)) + " raised while " + SideName(o) + "'s was down", false);
} else if (t - downSince_ >= p_.grace - 1e-9) {
char b[128];
std::snprintf(b, sizeof b, "%s's down %.1f s (%s)%s%s", SideName(o), p_.grace, downWhy_.c_str(),
shown[other] ? ", up: " : "", shown[other] ? SideName(other) : "");
Set(shown[other] ? other : -1, b, false);
}
}
enum class Pinch { Ray, Take, None };
// A pinch began on side s; canRay: its ray is up or may come up for it (fh_ray's CanShowForPinch).
// Ray: the owner's, a ray pinch; Take: the other hand's, it takes the ray and it's a ray pinch;
// None: not a ray pinch.
Pinch Pinched(int s, bool canRay) {
if (!canRay || s < 0 || s > 1) return Pinch::None;
if (s == side_) {
downSince_ = -1; // brought back for the pinch
return Pinch::Ray;
}
Set(s, std::string(SideName(s)) + " pinched", false);
return Pinch::Take;
}
// The pointer went elsewhere (the mouse took it, hands turned off): no owner, at once.
void Drop(const std::string &why) {
if (side_ >= 0) Set(-1, why, false);
}
std::vector<Change> changes; // since the caller last cleared it
std::vector<std::string> log; // the owner's ray going down and coming back, likewise
private:
void Set(int to, const std::string &why, bool off) {
if (to == side_) return;
changes.push_back({side_, to, why, off});
side_ = to, downSince_ = -1;
}
Params p_;
int side_ = -1;
double downSince_ = -1, shownAt_[2] = {-1e9, -1e9};
bool was_[2] = {false, false};
std::string downWhy_;
};
// One tracked hand this frame, as ft-hands calls it.
struct SeenHand {
uint32_t id = 0;
int side = 0; // 0 left, 1 right
};
class Slots {
public:
explicit Slots(Params p = {}) : p_(p) {}
// This frame's hands: which one each side's ray follows (pick[s], an index into hands; -1 none).
void Update(double t, const std::vector<SeenHand> &hands, int pick[2]) {
pick[0] = pick[1] = -1;
for (int s = 0; s < 2; ++s) {
if (!bound_[s]) continue;
int at = -1;
for (size_t i = 0; i < hands.size(); ++i)
if (hands[i].id == id_[s]) at = int(i);
if (at < 0) {
bound_[s] = false; // gone
continue;
}
if (hands[at].side == s) {
flipSince_[s] = -1;
} else {
if (flipSince_[s] < 0) flipSince_[s] = t;
if (t - flipSince_[s] >= p_.flipHold - 1e-9) {
bound_[s] = false; // ft-hands has settled on the other side
continue;
}
}
pick[s] = at;
}
for (int s = 0; s < 2; ++s) {
if (pick[s] >= 0) continue;
for (size_t i = 0; i < hands.size(); ++i)
if (hands[i].side == s && int(i) != pick[1 - s]) {
pick[s] = int(i);
break;
}
bound_[s] = pick[s] >= 0;
if (bound_[s]) id_[s] = hands[pick[s]].id, flipSince_[s] = -1;
}
}
// The side whose ray follows hand id, or -1.
int SideOf(uint32_t id) const {
for (int s = 0; s < 2; ++s)
if (bound_[s] && id_[s] == id) return s;
return -1;
}
private:
Params p_;
uint32_t id_[2] = {0, 0};
bool bound_[2] = {false, false};
double flipSince_[2] = {-1, -1};
};
// One side's recent rays, for the aim lock.
class AimHistory {
public:
explicit AimHistory(Params p = {}) : p_(p) {}
// A hands frame at capture time t: the ray (filtered), whether it's up, and the thumb to index
// distance (m; < 0 unknown).
void Add(double t, const fhray::Ray &ray, bool shown, double pinchD) {
h_.push_back({t, ray, shown, pinchD});
while (!h_.empty() && h_.front().t < t - 1.0) h_.pop_front();
}
// The ray to press along for a pinch that began at tBegin (see the top); endD: the pinch's release
// distance (end_m). False: no frame with the ray up within lockBack. age: how long before the begin.
bool Lock(double tBegin, double endD, fhray::Ray &out, double &age) const {
const double openD = endD + p_.lockOpen;
const S *open = nullptr, *lead = nullptr;
for (auto it = h_.rbegin(); it != h_.rend(); ++it) {
if (it->t > tBegin + 1e-6) continue;
if (it->t < tBegin - p_.lockBack - 1e-6) break;
if (!it->shown) continue;
if (!open && it->d >= openD) open = &*it;
if (!lead && it->t <= tBegin - p_.lockLead + 1e-6) lead = &*it;
}
const S *pick = open ? open : lead;
if (!pick) { // nothing that far back: the oldest up within the window
for (const S &e : h_)
if (e.shown && e.t <= tBegin + 1e-6 && e.t >= tBegin - p_.lockBack - 1e-6) {
pick = &e;
break;
}
}
if (!pick) return false;
out = pick->ray, age = tBegin - pick->t;
return true;
}
void Clear() { h_.clear(); }
private:
struct S {
double t;
fhray::Ray ray;
bool shown;
double d;
};
Params p_;
std::deque<S> h_;
};
} // namespace rayown
+373
View File
@@ -0,0 +1,373 @@
// Unit tests for the hand ray's owner, slots and aim lock (pointer/helper/rayowner.h), and a pinch
// bringing a ray back (hands/include/fh_ray.h's ShowForPinch): made-up timelines at the helper's
// 8 ms frame and ft-hands' 30 Hz.
// Build and run: just test-c (or g++ -std=c++17 -I. pointer/tests/rayowner-test.cpp).
#include "pointer/helper/rayowner.h"
#include <cmath>
#include <cstdio>
#include <functional>
#include <string>
#include <vector>
using namespace rayown;
using fhray::kDeg;
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 kDt = 0.008;
constexpr double kFrame = 1 / 30.0;
// The owner, stepped with each side's ray up or down (and allowed to point).
struct World {
Owner o;
double t = 0;
bool shown[2] = {false, false}, may[2] = {true, true};
std::string why[2] = {"palm", "palm"};
std::vector<Change> changes;
std::vector<std::string> log;
explicit World(Params p = {}) : o(p) {}
void Step() {
o.Update(t, shown, may, why);
for (const Change &c : o.changes) changes.push_back(c);
for (const std::string &l : o.log) log.push_back(l);
o.changes.clear(), o.log.clear();
t += kDt;
}
void Run(double secs, const std::function<void()> &each = {}) {
const double end = t + secs - 1e-9;
while (t < end) {
if (each) each();
Step();
}
}
};
void TestFirstRaised() {
World w;
w.Run(0.2);
CHECK(w.o.Side() == -1 && w.changes.empty(), "no ray: no owner");
w.shown[1] = true;
w.Step();
CHECK(w.o.Side() == 1, "the first ray up takes it: %d", w.o.Side());
CHECK(w.changes.size() == 1 && w.changes[0].from == -1 && w.changes[0].to == 1 && w.changes[0].why == "raised",
"one change, none -> right (raised)");
// the other one showing later doesn't take it
w.shown[0] = true;
w.Run(2);
CHECK(w.o.Side() == 1 && w.changes.size() == 1, "a second ray up leaves the owner: %d, %zu changes", w.o.Side(),
w.changes.size());
}
// The live complaint: both hands up, the owner's ray dropping for a moment as the fingers close.
void TestBriefDropKeepsTheOwner() {
World w;
w.shown[1] = true;
w.Step();
w.shown[0] = true; // the left hand up too, all along
w.Run(0.5);
w.changes.clear();
// ten drops of 0.3 s, one a second
for (int i = 0; i < 10; ++i) {
w.Run(0.7);
w.shown[1] = false;
w.Step();
w.Run(0.29, [&] { CHECK(w.o.Side() == 1 && w.o.Waiting(), "right keeps it while its ray is down"); });
w.shown[1] = true;
}
w.Run(0.1);
CHECK(w.changes.empty(), "brief drops: no owner change (%zu)", w.changes.size());
CHECK(!w.o.Waiting(), "back up: not waiting");
int downs = 0, backs = 0;
for (const std::string &l : w.log) {
downs += l.find(": down (palm)") != std::string::npos;
backs += l.find(": back after") != std::string::npos;
}
CHECK(downs == 10 && backs == 10, "each drop logs once down, once back: %d %d", downs, backs);
// as it was (no grace): every drop handed the laser to the other hand, and it stayed there
Params none;
none.grace = 0;
World old(none);
old.shown[1] = true;
old.Step();
old.shown[0] = true;
old.Run(0.5);
old.shown[1] = false;
old.Run(0.3);
old.shown[1] = true;
old.Run(0.5);
CHECK(old.o.Side() == 0, "without the grace a drop gives the other hand the ray: %d", old.o.Side());
}
void TestGraceRunsOut() {
World w;
w.shown[0] = w.shown[1] = true;
w.Step();
CHECK(w.o.Side() == 0 || w.o.Side() == 1, "an owner");
const int o = w.o.Side(), other = 1 - o;
w.Run(0.5);
w.changes.clear();
w.shown[o] = false;
const double from = w.t;
double took = -1;
w.Run(1, [&] {
if (took < 0 && w.o.Side() == other) took = w.t;
});
CHECK(took >= from + 0.5 - 1e-9 && took < from + 0.5 + 2 * kDt, "the other hand's ray takes it after the grace: %.3f",
took - from);
CHECK(w.changes.size() == 1 && w.changes[0].why.find("down 0.5 s (palm)") != std::string::npos &&
w.changes[0].why.find("up: ") != std::string::npos,
"why: %s", w.changes.empty() ? "-" : w.changes[0].why.c_str());
// with no other ray up, the owner goes after the grace
World v;
v.shown[1] = true;
v.Run(0.3);
v.shown[1] = false;
v.Run(0.45);
CHECK(v.o.Side() == 1, "within the grace: still right");
v.Run(0.1);
CHECK(v.o.Side() == -1 && v.changes.back().to == -1 && !v.changes.back().off, "after it: none, not off");
}
void TestFreshRaiseTakesAtOnce() {
World w;
w.shown[1] = true;
w.Run(0.3);
w.shown[1] = false; // the right hand comes down
w.Run(0.1);
w.shown[0] = true; // and the left goes up (its ray shows, after the right's went)
w.Step();
CHECK(w.o.Side() == 0, "a ray raised while the owner's is down takes it at once: %d", w.o.Side());
CHECK(w.changes.back().why.find("left raised") != std::string::npos, "why: %s", w.changes.back().why.c_str());
}
void TestOffLeavesAtOnce() {
World w;
w.shown[0] = w.shown[1] = true;
w.Run(0.2);
const int o = w.o.Side();
w.may[o] = false, w.shown[o] = false; // its controller in use: fh_ray hides it ("off")
w.Step();
CHECK(w.changes.back().from == o && w.changes.back().to == -1 && w.changes.back().off, "off: leaves at once, off");
w.Step();
CHECK(w.o.Side() == 1 - o, "then the other ray up takes it: %d", w.o.Side());
// near a panel (ft-screens' poke): at once too, not off
World v;
v.shown[1] = true;
v.Run(0.2);
v.shown[1] = false, v.why[1] = "near";
v.Step();
CHECK(v.o.Side() == -1 && !v.changes.back().off && v.changes.back().why == "near a panel", "near: leaves at once");
}
void TestPinches() {
World w;
w.shown[0] = w.shown[1] = true;
w.Step();
const int o = w.o.Side(), other = 1 - o;
w.changes.clear();
CHECK(w.o.Pinched(o, true) == Owner::Pinch::Ray, "the owner's pinch: a ray pinch");
CHECK(w.o.Pinched(other, false) == Owner::Pinch::None, "the other hand without a ray: not a ray pinch");
CHECK(w.o.Side() == o, "and it leaves the owner");
CHECK(w.o.Pinched(other, true) == Owner::Pinch::Take, "the other hand with its ray up: takes it");
CHECK(w.o.Side() == other && w.o.changes.back().why.find("pinched") != std::string::npos, "taken by the pinch");
w.o.changes.clear();
// the owner's ray down, a pinch on it within the grace: back, a ray pinch, no change
w.shown[other] = false;
w.Run(0.2);
CHECK(w.o.Waiting(), "waiting");
CHECK(w.o.Pinched(other, true) == Owner::Pinch::Ray && !w.o.Waiting(), "the owner's pinch brings it back");
w.shown[other] = true;
w.Run(1);
CHECK(w.o.Side() == other && w.changes.empty(), "still the owner, no change");
// no owner at all: a pinch with a ray that can come up takes it
World v;
CHECK(v.o.Pinched(1, true) == Owner::Pinch::Take && v.o.Side() == 1, "no owner: the pinch takes it");
v.o.Drop("the mouse");
CHECK(v.o.Side() == -1 && v.o.changes.back().why == "the mouse", "dropped");
}
void TestSlots() {
Params p;
Slots sl(p);
int pick[2];
double t = 0;
std::vector<SeenHand> hands = {{3, 1}, {4, 0}};
sl.Update(t, hands, pick);
CHECK(pick[0] == 1 && pick[1] == 0, "by ft-hands' call: %d %d", pick[0], pick[1]);
CHECK(sl.SideOf(3) == 1 && sl.SideOf(4) == 0 && sl.SideOf(9) == -1, "SideOf");
// the calls swap for 0.2 s: each ray keeps its hand
for (int i = 0; i < 6; ++i) {
t += kFrame;
sl.Update(t, {{3, 0}, {4, 1}}, pick);
CHECK(pick[1] == 0 && pick[0] == 1, "a brief swap: the rays keep their hands (%d %d)", pick[0], pick[1]);
}
t += kFrame;
sl.Update(t, hands, pick);
CHECK(pick[0] == 1 && pick[1] == 0, "back: unchanged");
// ft-hands settles on the other call: after 0.3 s the rays move
double moved = -1;
const double from = t + kFrame;
for (int i = 0; i < 20 && moved < 0; ++i) {
t += kFrame;
sl.Update(t, {{3, 0}, {4, 1}}, pick);
if (pick[0] == 0) moved = t;
}
CHECK(moved >= from + 0.3 - 1e-9 && moved < from + 0.3 + kFrame + 1e-9, "a held call moves the rays after 0.3 s: %.3f",
moved - from);
CHECK(pick[1] == 1 && sl.SideOf(3) == 0 && sl.SideOf(4) == 1, "and both follow the call (%d %d)", pick[0], pick[1]);
// both called right: the right ray keeps its hand, the left keeps its own for the hold
Slots d(p);
t = 0;
d.Update(t, {{7, 0}, {8, 1}}, pick);
t += kFrame;
d.Update(t, {{7, 1}, {8, 1}}, pick);
CHECK(pick[0] == 0 && pick[1] == 1, "both called right: no hand changes ray (%d %d)", pick[0], pick[1]);
// one hand gone, a new one called that side
t += kFrame;
d.Update(t, {{8, 1}, {9, 0}}, pick);
CHECK(pick[0] == 1 && pick[1] == 0 && d.SideOf(9) == 0, "a new hand takes the free ray (%d %d)", pick[0], pick[1]);
// a lone hand's call flips for a frame: no ray changes hand
Slots one(p);
t = 0;
one.Update(t, {{5, 1}}, pick);
t += kFrame;
one.Update(t, {{5, 0}}, pick);
CHECK(pick[1] == 0 && pick[0] == -1, "a lone hand's flip: still the right ray's (%d %d)", pick[0], pick[1]);
}
fhray::Ray RayAt(double yawDeg) {
fhray::Ray r;
r.dir = fhray::Dir(yawDeg * kDeg, 0);
r.origin = {0.2, 1.3, -0.4};
return r;
}
double YawDeg(const fhray::Ray &r) { return fhray::YawOf(r.dir) / kDeg; }
void TestAimLock() {
// pointing at yaw 0 with the fingers open (6 cm), then closing over 150 ms, turning the ray 3 deg
// down to the begin (2 cm, when ft-hands sees the pinch)
AimHistory h;
const double begin = 2.0;
for (double t = 1.0; t <= begin + 1e-9; t += kFrame) {
const double closing = std::clamp((t - (begin - 0.15)) / 0.15, 0.0, 1.0);
h.Add(t, RayAt(-3 * closing), true, 0.06 - 0.04 * closing);
}
fhray::Ray r;
double age = -1;
CHECK(h.Lock(begin, 0.035, r, age), "a lock");
CHECK(std::fabs(YawDeg(r)) < 1.3, "the aim before the fingers closed far: %.2f deg", YawDeg(r));
CHECK(age > 0.05 && age < 0.15, "from the frame the tips were last apart: %.3f s", age);
// without the distance (no skeleton): the frame 0.1 s before
AimHistory n;
for (double t = 1.0; t <= begin + 1e-9; t += kFrame) n.Add(t, RayAt(-20 * (t - 1)), true, -1);
CHECK(n.Lock(begin, 0.035, r, age), "a lock without distances");
CHECK(age >= 0.1 - 1e-9 && age < 0.1 + kFrame, "0.1 s back: %.3f", age);
// fingers resting nearly closed (never past end_m in the window): 0.1 s back too
AimHistory c;
for (double t = 1.0; t <= begin + 1e-9; t += kFrame) c.Add(t, RayAt(0), true, 0.025);
CHECK(c.Lock(begin, 0.035, r, age) && age >= 0.1 - 1e-9 && age < 0.1 + kFrame, "resting closed: %.3f", age);
// frames with the ray down don't count; none up in the window: no lock
AimHistory d;
for (double t = 1.0; t <= begin + 1e-9; t += kFrame) d.Add(t, RayAt(t > 1.8 ? 10 : 0), t > 1.8, 0.06);
CHECK(d.Lock(begin, 0.035, r, age) && std::fabs(YawDeg(r) - 10) < 1e-6, "only frames with the ray up");
AimHistory e;
for (double t = 1.0; t <= begin + 1e-9; t += kFrame) e.Add(t, RayAt(0), t < 1.5, 0.06);
CHECK(!e.Lock(begin, 0.035, r, age), "nothing up within 0.35 s: no lock");
// the ray just showed (less than 0.1 s of it, fingers closed): its first frame
AimHistory f;
for (double t = 1.0; t <= begin + 1e-9; t += kFrame) f.Add(t, RayAt(t), t > begin - 0.07, 0.01);
CHECK(f.Lock(begin, 0.035, r, age) && age <= 0.07 + 1e-9 && age > 0.03, "a ray just up: its oldest frame (%.3f)", age);
}
// fh_ray's ShowForPinch: a pinch brings back a ray that just dropped, not one sent away.
fhray::HandFrame Pointing(double t) {
fhray::HandFrame f;
f.t = t, f.stereo = true;
f.knuckle = {0.18, 1.35, -0.45};
f.wrist = {0.17, 1.33, -0.36};
f.palmNormal = fhray::Norm({0, -0.5, -1});
f.elbow = {0.2, 1.15, -0.1};
return f;
}
void TestShowForPinch() {
const fhray::V3 eye{0, 1.6, 0}, fwd{0, 0, -1};
fhray::Body b;
fhray::HandRay r(1);
double t = 0;
auto run = [&](double secs, const std::function<fhray::HandFrame(double)> &make, bool may = true) {
const double end = t + secs - 1e-9;
while (t < end) {
b.Update(t, eye, fwd);
r.Frame(b, make(t), may);
t += kFrame;
}
};
auto palmIn = [](double tt) {
fhray::HandFrame f = Pointing(tt);
f.palmNormal = {0, 0, 1};
return f;
};
run(0.5, Pointing);
CHECK(r.Shown(), "pointing: shown");
run(0.4, palmIn);
CHECK(!r.Shown() && r.event == "hide palm", "palm toward you: hidden (%s)", r.event.c_str());
CHECK(r.CanShowForPinch(t, 0.5), "just hid: a pinch may bring it back");
r.ShowForPinch();
r.Hold(true, t);
CHECK(r.Shown(), "brought back");
run(1, palmIn);
CHECK(r.Shown(), "held by the pinch whatever the pose");
r.Hold(false, t);
run(0.8, palmIn);
CHECK(!r.Shown(), "after the pinch: hides again");
run(0.6, palmIn);
CHECK(!r.CanShowForPinch(t, 0.5), "hid more than the grace ago: no");
// hidden for "off" (its controller in use): never
fhray::HandRay q(1);
t = 0;
while (t < 0.5) b.Update(t, eye, fwd), q.Frame(b, Pointing(t), true), t += kFrame;
b.Update(t, eye, fwd), q.Frame(b, Pointing(t), false);
CHECK(!q.Shown() && !q.CanShowForPinch(t, 0.5), "hidden off: a pinch can't bring it back");
// not shown yet but every show test passing (raised and pinched within 120 ms): yes
fhray::HandRay s(1);
t = 0;
b.Update(t, eye, fwd), s.Frame(b, Pointing(t), true);
CHECK(!s.Shown() && s.CanShowForPinch(t, 0.5), "on its way up: a pinch may show it");
fhray::HandRay lap(1);
lap.Frame(b, palmIn(0), true);
CHECK(!lap.CanShowForPinch(0, 0.5), "never up, failing a test: no");
}
} // namespace
int main() {
TestFirstRaised();
TestBriefDropKeepsTheOwner();
TestGraceRunsOut();
TestFreshRaiseTakesAtOnce();
TestOffLeavesAtOnce();
TestPinches();
TestSlots();
TestAimLock();
TestShowForPinch();
if (failures) {
std::printf("%d failures\n", failures);
return 1;
}
std::printf("rayowner-test: ok\n");
return 0;
}
+1
View File
@@ -70,6 +70,7 @@ POINTER_RAY_DEADZONE=2.5 # hand ray: degrees the ray turns during a pinch befo
POINTER_RAY_PUSH=1 # hand ray: while carrying a panel, reaching out pushes it away and pulling in brings it near
POINTER_RAY_SCROLL=1 # hand ray: scroll speed for a held middle pinch moved up or down (-1 turns the direction round)
POINTER_RAY_RELEASE=2 # hand ray: seconds after the ray goes before the pointer it woke lets go of the laser
POINTER_RAY_GRACE=0.5 # hand ray: seconds a hand keeps the laser after its ray drops (the other hand's ray takes it after this, or at once by pinching or raising)
SHARE_KEYS=0 # 1 = keys of keyboards grabbed for the desktop also go to @frametop_keys, for hotkey tools (any local process can listen)
DISPLAY_OFF_MIN=0 # minutes without use before frametop-power turns the displays off, even if the headset seems worn (0 = never)
DISPLAY_MOVE_MM=5 # displays off: the headset or a controller moving this far (mm) within 10 s counts as use