mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-27 00:01:03 +01:00
From the 2026-10-10 19:28 session (ten flags, ft-screens' and the helper's logs): - Tracking gates (Params::gate, HANDS_TRACK_GATE): a held hand's frame that no hand makes (faster than 4 m/s, or more than 0.5*100 m/s^2*dt^2 + 2 cm off where its velocity had it) is dropped unless three in a row agree; its frame turning faster than 540 deg/s likewise. One camera's frames and frames near the edge of view (50-80 deg off the head's forward) are smoothed harder and don't turn the panel; past 80 deg the hand counts as unseen (the panel holds). On the side-grab flag (203635) the left hand read alternately at two places 10-20 cm apart with its frame flipping 100-180 deg. - CarryPose faces the head from a point between the hand and the panel's centre when the panel is held near the eyes by a far point (kCarryShare): facing from the centre had no steady answer there and spun 0.6 m panels held by an edge at arm's reach. A centre near the eyes stops turning. - A pinch by a closed hand (index <= 1.15, the others <= 1.05) is a fist grab, not a pinch click: ft-hands called the user's two fists pinches (flag 203215) and the content was clicked. - Two hands turn a panel on every axis with the line between them and roll it with their wrists (StretchTurn, Params::stretchTurn, HANDS_STRETCH_TURN); fists find a panel by its edge too. - A pinch the touch zone claimed takes the border 3 cm past the band's depth (Params::pinchSlack). - The push (Params::push, HANDS_PUSH): an open hand, palm to the panel, pushed straight into it at 0.5 m/s (5 cm in) or 15 cm in slowly, throws it 3 m out like a flick. A flat or pushing open hand doesn't poke. throwAims() says which panel a push or flick would throw. - The palm menu: facing 45 deg (60 to leave), the palm still, not at a panel (Input::atPanel, HandAtPanel) nor for a second after it or after a hold; a pinch with the middle finger in it does nothing on the ring. - "Grab that": Aim() for the target's glow. Tests: gates (alternating misreads, real moves, a real jump, the edge of view, flipping frames, one camera), fist pinches and two-fist stretch and turn, StretchTurn, push vs poke, rest, wave and the back of the hand, pinch slack, the stricter palm menu; replays: --carries (steadiness of carried panels on the flags, gates on and off), --grabthat with pushes and the palm menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
7.7 KiB
C++
192 lines
7.7 KiB
C++
// The palm menu (see palmmenu.h).
|
|
#include "palmmenu.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
|
|
namespace palmmenu {
|
|
namespace {
|
|
|
|
using handcut::Hand;
|
|
|
|
double Dot(const double a[3], const double b[3]) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
|
|
|
|
double Dist3(const float a[3], const float b[3]) {
|
|
const double d[3] = {double(a[0]) - b[0], double(a[1]) - b[1], double(a[2]) - b[2]};
|
|
return std::sqrt(Dot(d, d));
|
|
}
|
|
|
|
double Deg(double cosine) { return std::acos(std::clamp(cosine, -1.0, 1.0)) * 180 / M_PI; }
|
|
|
|
// How open the hand is: the middle, ring and little fingers' tip-to-wrist over knuckle-to-wrist.
|
|
double Openness(const Hand &h) {
|
|
double sum = 0;
|
|
for (int mcp : {9, 13, 17}) sum += Dist3(h.p[mcp + 3], h.p[0]) / std::max(1e-6, Dist3(h.p[mcp], h.p[0]));
|
|
return sum / 3;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void PalmNormal(const Hand &h, double out[3]) {
|
|
const double a[3] = {double(h.p[5][0]) - h.p[0][0], double(h.p[5][1]) - h.p[0][1], double(h.p[5][2]) - h.p[0][2]};
|
|
const double b[3] = {double(h.p[17][0]) - h.p[0][0], double(h.p[17][1]) - h.p[0][1], double(h.p[17][2]) - h.p[0][2]};
|
|
double n[3] = {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]};
|
|
const double l = std::sqrt(Dot(n, n)) * (h.right ? 1 : -1);
|
|
for (int i = 0; i < 3; ++i) out[i] = l != 0 ? n[i] / l : 0;
|
|
}
|
|
|
|
bool PalmUp(const Hand &h, const Mat &head, const Params &k, bool was, Pose *pose) {
|
|
double n[3], palm[3];
|
|
PalmNormal(h, n);
|
|
h.Palm(palm);
|
|
double to[3] = {head.m[0][3] - palm[0], head.m[1][3] - palm[1], head.m[2][3] - palm[2]};
|
|
const double dist = std::sqrt(Dot(to, to));
|
|
if (dist < 1e-6) return false;
|
|
for (double &v : to) v /= dist;
|
|
const double forward[3] = {-head.m[0][2], -head.m[1][2], -head.m[2][2]};
|
|
Pose p;
|
|
p.dist = dist;
|
|
p.facingDeg = Deg(Dot(n, to));
|
|
p.lookDeg = Deg(-Dot(forward, to));
|
|
p.open = Openness(h);
|
|
if (pose) *pose = p;
|
|
return dist >= k.nearest && dist <= k.farthest && p.facingDeg <= (was ? k.facingLeave : k.facing) &&
|
|
p.lookDeg <= (was ? k.lookLeave : k.look) && p.open >= (was ? k.openLeave : k.open);
|
|
}
|
|
|
|
const Hand *PalmMenu::HandFor(int side, const std::vector<Hand> &hands) const {
|
|
const Side &sd = side_[side];
|
|
if (sd.state != State::Idle && sd.handId)
|
|
for (const Hand &h : hands)
|
|
if (h.id == sd.handId) return &h; // the hand it follows, whatever its side reads now
|
|
const Hand *best = nullptr;
|
|
for (const Hand &h : hands)
|
|
if (h.right == (side == 1) && (!best || h.confidence > best->confidence)) best = &h;
|
|
return best;
|
|
}
|
|
|
|
bool PalmMenu::Plain(const Hand *h) const {
|
|
if (!h) return false;
|
|
const double mid = Dist3(h->p[12], h->p[0]) / std::max(1e-6, Dist3(h->p[9], h->p[0]));
|
|
return Dist3(h->p[4], h->p[12]) >= p_.plainMiddle && mid >= p_.plainExtended && h->speed() < p_.pinchStill;
|
|
}
|
|
|
|
void PalmMenu::Release(int s, bool lost, int64_t now) {
|
|
Side &sd = side_[s];
|
|
sd.state = State::Shown, sd.awaySince = -1;
|
|
// lost, or the hand turned away before letting go: nothing
|
|
if (lost || !sd.up) {
|
|
++stats.cancels;
|
|
return;
|
|
}
|
|
if (now - lastToggle_ < int64_t(p_.againMs * 1e6)) return;
|
|
lastToggle_ = now, toggled_ = s;
|
|
++stats.toggles;
|
|
}
|
|
|
|
void PalmMenu::Update(const Input &in) {
|
|
toggled_ = -1;
|
|
const handcut::Hands &H = *in.hands;
|
|
const bool newFrame = H.frame() != frame_;
|
|
frame_ = H.frame();
|
|
for (int s = 0; s < 2; ++s) {
|
|
Side &sd = side_[s];
|
|
if (!p_.on || !in.allowed[s] || in.busy[s] || !in.haveHead) {
|
|
if (sd.state == State::Pinched) ++stats.cancels;
|
|
sd = Side{};
|
|
sd.state = State::Off;
|
|
if (in.busy[s]) sd.quietUntil = in.nowNs + int64_t(p_.quietMs * 1e6);
|
|
continue;
|
|
}
|
|
// at a panel: no ring now, nor for quietMs (a pinch on the ring held goes on)
|
|
if (in.atPanel[s]) {
|
|
sd.quietUntil = in.nowNs + int64_t(p_.quietMs * 1e6);
|
|
if (sd.state == State::Shown) sd.state = State::Idle;
|
|
sd.upSince = -1;
|
|
}
|
|
if (sd.state == State::Off) sd.state = State::Idle;
|
|
const Hand *h = HandFor(s, H.seen());
|
|
if (h) sd.lastSeen = in.nowNs, sd.handId = h->id;
|
|
if (!h && in.nowNs - sd.lastSeen > int64_t(p_.lostMs * 1e6)) {
|
|
if (sd.state == State::Pinched) ++stats.cancels;
|
|
sd.state = State::Idle, sd.up = false, sd.upSince = -1;
|
|
}
|
|
if (newFrame) sd.up = h && PalmUp(*h, in.head, p_, shown(s));
|
|
const handcut::Gesture &g = H.pinch(s);
|
|
if (!sd.knowBegins) sd.begins = g.begins, sd.knowBegins = true; // not a pinch that began before
|
|
const bool began = g.begins != sd.begins;
|
|
sd.begins = g.begins;
|
|
switch (sd.state) {
|
|
case State::Off:
|
|
break;
|
|
case State::Idle:
|
|
// a pinch now is the helper's (gaze); the ring shows after showMs of palm up
|
|
if (!newFrame) break;
|
|
if (!sd.up || in.nowNs < sd.quietUntil || (h && h->speed() >= p_.still)) {
|
|
sd.upSince = -1;
|
|
break;
|
|
}
|
|
if (sd.upSince < 0) sd.upSince = in.nowNs;
|
|
if (in.nowNs - sd.upSince < int64_t(p_.showMs * 1e6)) break;
|
|
sd.state = State::Shown, sd.shownNs = H.captureNs(), sd.awaySince = -1;
|
|
++stats.shows;
|
|
// a pinch beginning on this very frame: the near report for it says ours
|
|
if (began && g.down && g.beginNs >= sd.shownNs) {
|
|
if (Plain(h)) sd.state = State::Pinched, ++stats.pinches;
|
|
else ++stats.notPlain;
|
|
}
|
|
break;
|
|
case State::Shown:
|
|
// A pinch that began while the ring showed (the helper saw this hand near then).
|
|
if (began && g.beginNs >= sd.shownNs) {
|
|
if (!Plain(h)) { // the middle finger in it, the fingers bunched, or the hand moving: not ours to toggle
|
|
++stats.notPlain;
|
|
} else {
|
|
++stats.pinches;
|
|
if (g.down) {
|
|
sd.state = State::Pinched;
|
|
break;
|
|
}
|
|
Release(s, g.lost, in.nowNs); // began and ended between two reads: a tap
|
|
}
|
|
}
|
|
if (!newFrame) break;
|
|
if (sd.up) {
|
|
sd.awaySince = -1;
|
|
} else {
|
|
if (sd.awaySince < 0) sd.awaySince = in.nowNs;
|
|
if (in.nowNs - sd.awaySince >= int64_t(p_.hideMs * 1e6)) sd.state = State::Idle, sd.upSince = -1;
|
|
}
|
|
break;
|
|
case State::Pinched:
|
|
// held: the ring stays whatever the palm does; the release decides
|
|
if (began || !g.down) {
|
|
Release(s, g.lost, in.nowNs);
|
|
if (began && g.down && Plain(h)) sd.state = State::Pinched, ++stats.pinches; // and another began
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool PalmMenu::Ring(int side, const std::vector<Hand> &hands, double out[3]) const {
|
|
if (!shown(side)) return false;
|
|
const Hand *h = HandFor(side, hands);
|
|
if (!h) return false;
|
|
double n[3];
|
|
PalmNormal(*h, n);
|
|
h->Palm(out);
|
|
for (int i = 0; i < 3; ++i) out[i] += n[i] * p_.lift;
|
|
return true;
|
|
}
|
|
|
|
std::string PalmMenu::Describe() const {
|
|
static const char *names[] = {"off", "idle", "shown", "pinched"};
|
|
char b[64];
|
|
std::snprintf(b, sizeof b, "left %s, right %s", names[int(side_[0].state)], names[int(side_[1].state)]);
|
|
return b;
|
|
}
|
|
|
|
} // namespace palmmenu
|