diff --git a/hands/README.md b/hands/README.md index a12da22..88f0cf2 100644 --- a/hands/README.md +++ b/hands/README.md @@ -229,7 +229,7 @@ ft-hands detects a pinch per hand (`track/pinch.h`) and publishes it to the gest With `POINTER_HANDS=1`, the pointer helper (`pointer/helper/ft-pointer.cpp`) reads the gestures file every frame. It's off by default. -**The hand ray** (`POINTER_HAND_RAY=1`, the default with `POINTER_HANDS`): a bare hand raised in front and pointing, palm away, shows a ray from it, Quest-style (`include/fh_ray.h`: from a shoulder estimated from the head, through the knuckles), and the ray drives the pointer's virtual controller, so it works on SteamVR's dashboard, Steam's pages, overlays and Frametop's panels like a controller's laser. The ray comes up as the thumb nears the index or middle tip (`POINTER_RAY_READY`). With the ray up, a pinch clicks (a held pinch drags), a middle pinch right-clicks (held and moved, it scrolls), a three-finger pinch (thumb, index and middle together) scrolls as the hand moves, and while a pinch carries a panel, reaching out pushes it away and pulling in brings it near. With the hand low or the palm toward you, there's no ray, and pinches and grips work as below. In gaze mode there's no ray at all unless `POINTER_GAZE_HAND_RAY=1`. A hand holding a controller, or whose controller was used in the last seconds, has no ray and no gestures (the helper's handoff, `pointer/helper/handoff.h`). The rules, timings and measurements are at the top of `ft-pointer.cpp` ("Handoff", "Hand ray") and in frame-hands' `notes/hand-interaction.md`. `pointer/tools/rayeval` runs the ray over a replay (`ft-handreplay --ray`). +**The hand ray** (`POINTER_HAND_RAY=1`, the default with `POINTER_HANDS`): a bare hand in front and pointing, palm away, high or low (not while it types: the typing guard; `POINTER_RAY_RAISED=30` brings back the old limit, the knuckles within 30 cm under the eyes), shows a ray from it, Quest-style (`include/fh_ray.h`: from a shoulder estimated from the head, through the knuckles), and the ray drives the pointer's virtual controller, so it works on SteamVR's dashboard, Steam's pages, overlays and Frametop's panels like a controller's laser. The ray comes up as the thumb nears the index or middle tip (`POINTER_RAY_READY`). With the ray up, a pinch clicks (a held pinch drags), a middle pinch right-clicks (held and moved, it scrolls), a three-finger pinch (thumb, index and middle together) scrolls as the hand moves, and while a pinch carries a panel, reaching out pushes it away and pulling in brings it near. With the hand low or the palm toward you, there's no ray, and pinches and grips work as below. In gaze mode there's no ray at all unless `POINTER_GAZE_HAND_RAY=1`. A hand holding a controller, or whose controller was used in the last seconds, has no ray and no gestures (the helper's handoff, `pointer/helper/handoff.h`). The rules, timings and measurements are at the top of `ft-pointer.cpp` ("Handoff", "Hand ray") and in frame-hands' `notes/hand-interaction.md`. `pointer/tools/rayeval` runs the ray over a replay (`ft-handreplay --ray`). - **Pinch to click, in gaze mode.** Your eyes aim and your hands click, Vision Pro style, and the hand ray stays off (`POINTER_GAZE_HAND_RAY`). A pinch locks the pointer where you were looking just before the fingers began to close (75 ms before ft-hands saw the pinch, since the eyes often move on as the hand closes), and a quick tap clicks there. Pinch, hold and move the hand to nudge the pointer: nothing moves until the hand has moved 4 degrees (about 3 cm at arm's length), then the pointer follows at 0.3 times the hand's movement. Letting go clicks where the pointer is, and the correction is a lesson for the gaze tracker, as with the mouse. Held still for `POINTER_GAZE_HOLD` (0.5 s) instead, the pinch is a real press at that point, and the hand drags at `POINTER_GRIP_GAIN`. A middle pinch is a right click the same way. Hold a pinch with one hand and pinch with the other: both buttons are down, left for the first hand and right for the second, each until its own pinch opens (a first pinch not yet pressed becomes a press at once, where the pointer is), as when you hold both of a mouse's buttons. The rules and timings are in `pointer/helper/gazehand.h`. - **Without gaze mode,** a pinch is a real press, like the mouse's button: pressed when it closes, released when it opens, and while it's held the hand drags the pointer. A tap is still a click where the pointer is. diff --git a/hands/include/fh_ray.h b/hands/include/fh_ray.h index 42978d9..af31acf 100644 --- a/hands/include/fh_ray.h +++ b/hands/include/fh_ray.h @@ -58,7 +58,11 @@ // tracked a two-camera (STEREO) frame within a frame within lostFor (250 ms); one-camera // stereoWithin (200 ms) depth is 2-6 cm off, fine for a short gap // raised K at most raisedShow (0.30 m) below raisedHide (0.40 m): gaze pinches sat -// the eyes 0.35-0.45 m below the eyes, elbow resting +// the eyes (0: off; the pointer helper 0.35-0.45 m below the eyes, elbow resting +// turns it off: user, 2026-10-19, "hands +// still recognized even if they are low, +// just not if they are typing", which its +// typing guard holds back) // forward K at least forwardShow (0.25 m) ahead forwardHide (0.20 m) // of the eyes, along the torso's yaw // palm away palm normal . D >= palmShow (0.3) palmHide (0.0): palm toward you is reserved @@ -324,7 +328,7 @@ inline Tests Evaluate(const Params &p, const Body &body, const HandFrame &f, con if (!s) r.fails += r.fails.empty() ? name : std::string(",") + name; show = show && s, keep = keep && k; }; - test(r.below <= p.raisedShow, r.below <= p.raisedHide, "raised"); + if (p.raisedShow > 0) test(r.below <= p.raisedShow, r.below <= std::max(p.raisedShow, p.raisedHide), "raised"); test(r.ahead >= p.forwardShow, r.ahead >= p.forwardHide, "forward"); test(r.palm >= p.palmShow, r.palm >= p.palmHide, "palm"); if (r.forearmUsed) test(r.forearm >= p.forearmShow, r.forearm >= p.forearmHide, "forearm"); diff --git a/input-settings/ft_input_settings.py b/input-settings/ft_input_settings.py index 9109850..2eb36f2 100644 --- a/input-settings/ft_input_settings.py +++ b/input-settings/ft_input_settings.py @@ -167,6 +167,9 @@ HAND_INTERACTIONS = [ ("POINTER_RAY_READY", True, "6", "Laser", "Laser only when about to pinch", "A hand's laser shows once its thumb and index are within 6 cm, and goes when they open past 10 cm. " "Off: whenever the hand is raised and pointing."), + ("POINTER_RAY_RAISED", False, "30", "Laser", "No laser for a low hand", + "A hand's laser shows only with the knuckles within 30 cm under your eyes, and goes past 40 cm. " + "Off: a low hand points too. Hands typing on a keyboard never do."), ("POINTER_RAY_FIST_CURL", True, "1.1", "Laser", "No laser or clicks in a fist", "Closing the hand drops its laser, and a pinch made while the hand closes into a fist doesn't click."), ("POINTER_PINCH_NO_RAY", False, "1", "Laser", "Pinch with no laser clicks at the pointer", diff --git a/pointer/helper/ft-pointer.cpp b/pointer/helper/ft-pointer.cpp index 210bd53..991b3f3 100644 --- a/pointer/helper/ft-pointer.cpp +++ b/pointer/helper/ft-pointer.cpp @@ -1123,6 +1123,11 @@ int main() { rayParams.readyShow = std::clamp(ConfDouble(conf, "POINTER_RAY_READY", 6), 0.0, 30.0) / 100; rayParams.readyHide = rayParams.readyShow > 0 ? std::max(rayParams.readyShow, std::clamp(ConfDouble(conf, "POINTER_RAY_READY_HIDE", 10), 0.0, 40.0) / 100) : 0; rayParams.fistCurl = std::clamp(ConfDouble(conf, "POINTER_RAY_FIST_CURL", 1.1), 0.0, 2.0); + // the laser only for a raised hand: its knuckles within this far under the eyes, and gone 10 cm + // past it (cm; 0, the default: a low hand points too, and a hand typing is held back by the + // typing guard instead; 30 until 2026-10-19, when aiming at a keyboard below lost the laser) + rayParams.raisedShow = std::clamp(ConfDouble(conf, "POINTER_RAY_RAISED", 0), 0.0, 100.0) / 100; + rayParams.raisedHide = rayParams.raisedShow > 0 ? rayParams.raisedShow + 0.10 : 0; rayParamsNew = true; rayTap = std::clamp(ConfDouble(conf, "POINTER_RAY_TAP", 0.3), 0.0, 2.0); rayTapDeadzone = std::clamp(ConfDouble(conf, "POINTER_RAY_TAP_DEADZONE", 6.0), 0.0, 30.0); diff --git a/pointer/tests/handray-test.cpp b/pointer/tests/handray-test.cpp index d73aa35..a65a1e6 100644 --- a/pointer/tests/handray-test.cpp +++ b/pointer/tests/handray-test.cpp @@ -122,6 +122,41 @@ void TestLapPinch() { r.Why().c_str()); } +// The raised test off (the pointer helper's default since 2026-10-19): a low hand keeps its ray, and a +// hand in the lap, palm up, still never shows (the palm test). +void TestLowHand() { + Params p; + p.raisedShow = p.raisedHide = 0; + Body b; + HandRay r(1); + r.SetParams(p); + double t = 0; + CHECK(RunUntil(r, b, t, 1, true, Pointing) >= 0, "a pointing hand shows"); + auto lower = [](double y) { + return [y](double tt) { + HandFrame f = Pointing(tt); + f.knuckle.y = 1.6 - y, f.wrist.y = 1.6 - y - 0.02; + return f; + }; + }; + t += kFrame; + CHECK(RunUntil(r, b, t, 1, false, lower(0.55)) < 0, "0.55 m below the eyes: still shown"); + HandRay lapRay(0); + lapRay.SetParams(p); + double t2 = 0; + auto lap = [](double tt) { + HandFrame f; + f.t = tt, f.stereo = true; + f.knuckle = {-0.15, 1.0, -0.35}; + f.wrist = {-0.15, 1.0, -0.27}; + f.palmNormal = {0, 1, 0}; + return f; + }; + CHECK(RunUntil(lapRay, b, t2, 2, true, lap) < 0, "a hand in the lap, palm up, never shows"); + CHECK(lapRay.Why().find("raised") == std::string::npos && lapRay.Why().find("palm") != std::string::npos, + "why: %s", lapRay.Why().c_str()); +} + void TestPalmTowardYou() { Body b; HandRay r(1); @@ -481,6 +516,7 @@ int main() { TestBody(); TestGateShowHide(); TestLapPinch(); + TestLowHand(); TestPalmTowardYou(); TestTrackingAndControllerAndNear(); TestHold();